From e21a0b985535c1a55326435d657849b04592670d Mon Sep 17 00:00:00 2001 From: alexlapa Date: Wed, 26 Jun 2024 12:20:22 +0300 Subject: [PATCH 1/6] init --- .github/FUNDING.yml | 12 - .github/actions-rs/grcov.yml | 15 - .github/workflows/cargo.yml | 120 - .github/workflows/ci.yml | 119 + .github/workflows/grcov.yml | 89 - .gitmodules | 0 .rustfmt.toml | 16 + turn/CHANGELOG.md => CHANGELOG.md | 0 Cargo.lock | 3095 ++--------------- Cargo.toml | 55 +- Makefile | 108 + README.md | 146 +- _typos.toml | 15 - codecov.yml | 23 - constraints/.gitignore | 88 - constraints/CHANGELOG.md | 7 - constraints/Cargo.toml | 30 - constraints/LICENSE-APACHE | 201 -- constraints/LICENSE-MIT | 21 - constraints/README.md | 32 - constraints/examples/json.rs | 80 - constraints/examples/macros.rs | 104 - constraints/examples/native.rs | 119 - constraints/src/algorithms.rs | 9 - .../src/algorithms/fitness_distance.rs | 132 - .../fitness_distance/empty_constraint.rs | 71 - .../algorithms/fitness_distance/setting.rs | 532 --- .../algorithms/fitness_distance/settings.rs | 47 - .../fitness_distance/value_constraint.rs | 218 -- .../value_constraint/tests.rs | 144 - .../value_constraint/tests/bool.rs | 283 -- .../value_constraint/tests/f64.rs | 243 -- .../value_constraint/tests/string.rs | 132 - .../value_constraint/tests/u64.rs | 243 -- .../value_range_constraint.rs | 172 - .../value_range_constraint/tests.rs | 143 - .../value_range_constraint/tests/empty.rs | 75 - .../value_range_constraint/tests/f64.rs | 312 -- .../value_range_constraint/tests/u64.rs | 312 -- .../value_sequence_constraint.rs | 173 - .../value_sequence_constraint/tests.rs | 144 - .../value_sequence_constraint/tests/bool.rs | 132 - .../value_sequence_constraint/tests/f64.rs | 245 -- .../value_sequence_constraint/tests/string.rs | 133 - .../value_sequence_constraint/tests/u64.rs | 244 -- constraints/src/algorithms/select_settings.rs | 121 - .../select_settings/apply_advanced.rs | 188 - .../select_settings/apply_mandatory.rs | 213 -- .../select_settings/select_optimal.rs | 118 - .../src/algorithms/select_settings/tests.rs | 778 ----- .../select_settings/tie_breaking.rs | 187 - constraints/src/capabilities.rs | 5 - constraints/src/capabilities/stream.rs | 58 - constraints/src/capabilities/track.rs | 167 - constraints/src/capability.rs | 219 -- constraints/src/capability/value.rs | 74 - constraints/src/capability/value_range.rs | 207 -- constraints/src/capability/value_sequence.rs | 99 - constraints/src/constraint.rs | 789 ----- constraints/src/constraint/value.rs | 419 --- constraints/src/constraint/value_range.rs | 513 --- constraints/src/constraint/value_sequence.rs | 440 --- constraints/src/constraints.rs | 22 - constraints/src/constraints/advanced.rs | 191 - constraints/src/constraints/constraint_set.rs | 200 -- constraints/src/constraints/mandatory.rs | 321 -- constraints/src/constraints/stream.rs | 90 - constraints/src/constraints/track.rs | 339 -- constraints/src/enumerations.rs | 131 - constraints/src/errors.rs | 111 - constraints/src/lib.rs | 46 - constraints/src/macros.rs | 450 --- constraints/src/property.rs | 301 -- constraints/src/setting.rs | 159 - constraints/src/settings.rs | 5 - constraints/src/settings/stream.rs | 58 - constraints/src/settings/track.rs | 169 - constraints/src/supported_constraints.rs | 252 -- constraints/tests/w3c_spec_examples.rs | 216 -- data/.gitignore | 11 - data/CHANGELOG.md | 30 - data/Cargo.toml | 36 - data/LICENSE-APACHE | 201 -- data/LICENSE-MIT | 21 - data/README.md | 30 - data/codecov.yml | 23 - data/doc/webrtc.rs.png | Bin 33276 -> 0 bytes data/src/data_channel/data_channel_test.rs | 670 ---- data/src/data_channel/mod.rs | 682 ---- data/src/error.rs | 72 - data/src/lib.rs | 8 - data/src/message/message_channel_ack.rs | 77 - data/src/message/message_channel_open.rs | 441 --- data/src/message/message_test.rs | 96 - data/src/message/message_type.rs | 125 - data/src/message/mod.rs | 76 - doc/AVStack.jpg | Bin 4842 -> 0 bytes doc/ChannelTalk_logo.png | Bin 9235 -> 0 bytes doc/KittyCAD.png | Bin 10400 -> 0 bytes doc/check.png | Bin 6142 -> 0 bytes doc/embark.jpg | Bin 12976 -> 0 bytes doc/parity.png | Bin 19513 -> 0 bytes doc/uncheck.png | Bin 5898 -> 0 bytes doc/webrtc.rs.png | Bin 33270 -> 0 bytes doc/webrtc.rs.xcf | Bin 54671 -> 0 bytes doc/webrtc_crab.png | Bin 37282 -> 0 bytes doc/webrtc_crates_dep_graph.odg | Bin 16421 -> 0 bytes doc/webrtc_crates_dep_graph.png | Bin 36071 -> 0 bytes doc/webrtc_stack.png | Bin 39555 -> 0 bytes dtls/.gitignore | 12 - dtls/CHANGELOG.md | 26 - dtls/Cargo.toml | 94 - dtls/LICENSE-APACHE | 201 -- dtls/LICENSE-MIT | 21 - dtls/README.md | 30 - dtls/codecov.yml | 23 - dtls/doc/webrtc.rs.png | Bin 33276 -> 0 bytes dtls/examples/certificates/README.md | 60 - dtls/examples/certificates/client.csr | 7 - dtls/examples/certificates/client.pem | 5 - .../certificates/client.pem.private_key.pem | 5 - dtls/examples/certificates/client.pub.pem | 9 - dtls/examples/certificates/extfile.conf | 1 - dtls/examples/certificates/server.csr | 7 - dtls/examples/certificates/server.pem | 5 - .../certificates/server.pem.private_key.pem | 5 - dtls/examples/certificates/server.pub.pem | 9 - dtls/examples/dial/psk/dial_psk.rs | 83 - dtls/examples/dial/selfsign/dial_selfsign.rs | 82 - dtls/examples/dial/verify/dial_verify.rs | 93 - dtls/examples/hub/Cargo.toml | 16 - dtls/examples/hub/src/lib.rs | 112 - dtls/examples/hub/src/utilities.rs | 113 - dtls/examples/listen/psk/listen_psk.rs | 89 - .../listen/selfsign/listen_selfsign.rs | 87 - dtls/examples/listen/verify/listen_verify.rs | 118 - dtls/src/alert/alert_test.rs | 50 - dtls/src/alert/mod.rs | 185 - dtls/src/application_data.rs | 37 - .../change_cipher_spec_test.rs | 37 - dtls/src/change_cipher_spec/mod.rs | 42 - .../cipher_suite/cipher_suite_aes_128_ccm.rs | 118 - .../cipher_suite_aes_128_gcm_sha256.rs | 113 - .../cipher_suite_aes_256_cbc_sha.rs | 113 - ..._suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs | 12 - ...suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs | 12 - .../cipher_suite_tls_psk_with_aes_128_ccm.rs | 12 - .../cipher_suite_tls_psk_with_aes_128_ccm8.rs | 12 - ...r_suite_tls_psk_with_aes_128_gcm_sha256.rs | 96 - dtls/src/cipher_suite/mod.rs | 227 -- dtls/src/client_certificate_type.rs | 16 - dtls/src/compression_methods.rs | 60 - dtls/src/config.rs | 190 - dtls/src/conn/conn_test.rs | 2458 ------------- dtls/src/conn/mod.rs | 1215 ------- dtls/src/content.rs | 81 - dtls/src/crypto/crypto_cbc.rs | 129 - dtls/src/crypto/crypto_ccm.rs | 188 - dtls/src/crypto/crypto_gcm.rs | 119 - dtls/src/crypto/crypto_test.rs | 221 -- dtls/src/crypto/mod.rs | 528 --- dtls/src/crypto/padding.rs | 122 - dtls/src/curve/mod.rs | 17 - dtls/src/curve/named_curve.rs | 83 - dtls/src/error.rs | 223 -- dtls/src/extension/extension_server_name.rs | 56 - .../extension_server_name_test.rs | 26 - .../extension_supported_elliptic_curves.rs | 46 - ...xtension_supported_elliptic_curves_test.rs | 32 - .../extension_supported_point_formats.rs | 49 - .../extension_supported_point_formats_test.rs | 33 - ...xtension_supported_signature_algorithms.rs | 50 - ...ion_supported_signature_algorithms_test.rs | 48 - .../extension_use_extended_master_secret.rs | 35 - ...tension_use_extended_master_secret_test.rs | 32 - dtls/src/extension/extension_use_srtp.rs | 80 - .../extension_use_srtp_test.rs | 32 - dtls/src/extension/mod.rs | 130 - dtls/src/extension/renegotiation_info.rs | 48 - .../renegotiation_info_test.rs | 26 - dtls/src/flight/flight0.rs | 203 -- dtls/src/flight/flight1.rs | 193 - dtls/src/flight/flight2.rs | 131 - dtls/src/flight/flight3.rs | 470 --- dtls/src/flight/flight4.rs | 852 ----- dtls/src/flight/flight5.rs | 778 ----- dtls/src/flight/flight6.rs | 190 - dtls/src/flight/mod.rs | 87 - .../fragment_buffer/fragment_buffer_test.rs | 174 - dtls/src/fragment_buffer/mod.rs | 160 - dtls/src/handshake/handshake_cache.rs | 241 -- .../handshake_cache/handshake_cache_test.rs | 658 ---- dtls/src/handshake/handshake_header.rs | 50 - .../handshake_message_certificate.rs | 70 - .../handshake_message_certificate_test.rs | 79 - .../handshake_message_certificate_request.rs | 77 - ...dshake_message_certificate_request_test.rs | 64 - .../handshake_message_certificate_verify.rs | 52 - ...ndshake_message_certificate_verify_test.rs | 40 - .../handshake_message_client_hello.rs | 196 -- .../handshake_message_client_hello_test.rs | 75 - .../handshake_message_client_key_exchange.rs | 70 - ...dshake_message_client_key_exchange_test.rs | 35 - .../handshake/handshake_message_finished.rs | 34 - .../handshake_message_finished_test.rs | 32 - .../handshake_message_hello_verify_request.rs | 72 - ...shake_message_hello_verify_request_test.rs | 40 - .../handshake_message_server_hello.rs | 151 - .../handshake_message_server_hello_test.rs | 56 - .../handshake_message_server_hello_done.rs | 27 - ...andshake_message_server_hello_done_test.rs | 28 - .../handshake_message_server_key_exchange.rs | 133 - ...dshake_message_server_key_exchange_test.rs | 50 - dtls/src/handshake/handshake_random.rs | 67 - dtls/src/handshake/handshake_test.rs | 70 - dtls/src/handshake/mod.rs | 238 -- dtls/src/handshaker.rs | 429 --- dtls/src/lib.rs | 57 - dtls/src/listener.rs | 95 - dtls/src/prf/mod.rs | 315 -- dtls/src/prf/prf_test.rs | 261 -- dtls/src/record_layer/mod.rs | 107 - dtls/src/record_layer/record_layer_header.rs | 90 - dtls/src/record_layer/record_layer_test.rs | 118 - dtls/src/signature_hash_algorithm/mod.rs | 215 -- .../signature_hash_algorithm_test.rs | 141 - dtls/src/state.rs | 306 -- examples/.gitignore | 11 - examples/Cargo.toml | 153 - examples/LICENSE-APACHE | 201 -- examples/LICENSE-MIT | 21 - examples/README.md | 30 - examples/codecov.yml | 23 - examples/doc/webrtc.rs.png | Bin 33276 -> 0 bytes examples/examples/README.md | 30 - examples/examples/broadcast/README.md | 47 - examples/examples/broadcast/broadcast.rs | 306 -- .../examples/data-channels-close/README.md | 2 - .../data-channels-close.rs | 240 -- .../examples/data-channels-create/README.md | 38 - .../data-channels-create.rs | 194 -- .../data-channels-detach-create/README.md | 16 - .../data-channels-detach-create.rs | 241 -- .../examples/data-channels-detach/README.md | 42 - .../data-channels-detach.rs | 249 -- .../data-channels-flow-control/README.md | 64 - .../data-channels-flow-control.rs | 245 -- examples/examples/data-channels/README.md | 42 - .../examples/data-channels/data-channels.rs | 207 -- examples/examples/ice-restart/README.md | 25 - examples/examples/ice-restart/ice-restart.rs | 258 -- examples/examples/ice-restart/index.html | 82 - .../examples/insertable-streams/README.md | 52 - .../insertable-streams/insertable-streams.rs | 269 -- examples/examples/offer-answer/README.md | 24 - examples/examples/offer-answer/answer.rs | 391 --- examples/examples/offer-answer/offer.rs | 377 -- examples/examples/ortc/README.md | 34 - examples/examples/ortc/ortc.rs | 278 -- .../examples/play-from-disk-h264/README.md | 45 - .../play-from-disk-h264.rs | 361 -- .../examples/play-from-disk-hevc/README.md | 29 - .../play-from-disk-hevc.rs | 599 ---- .../play-from-disk-renegotiation/README.md | 30 - .../play-from-disk-renegotiation/index.html | 80 - .../play-from-disk-renegotiation.rs | 410 --- .../examples/play-from-disk-vpx/README.md | 48 - .../play-from-disk-vpx/play-from-disk-vpx.rs | 372 -- examples/examples/rc-cycle/rc-cycle.rs | 33 - examples/examples/reflect/README.md | 38 - examples/examples/reflect/reflect.rs | 326 -- examples/examples/rtp-forwarder/README.md | 52 - .../examples/rtp-forwarder/rtp-forwarder.rs | 321 -- .../examples/rtp-forwarder/rtp-forwarder.sdp | 9 - examples/examples/rtp-to-webrtc/README.md | 56 - .../examples/rtp-to-webrtc/rtp-to-webrtc.rs | 220 -- examples/examples/save-to-disk-h264/README.md | 38 - .../save-to-disk-h264/save-to-disk-h264.rs | 318 -- examples/examples/save-to-disk-vpx/README.md | 38 - .../save-to-disk-vpx/save-to-disk-vpx.rs | 348 -- examples/examples/signal/Cargo.toml | 11 - examples/examples/signal/src/lib.rs | 149 - examples/examples/simulcast/README.md | 42 - examples/examples/simulcast/simulcast.rs | 266 -- examples/examples/swap-tracks/README.md | 38 - examples/examples/swap-tracks/swap-tracks.rs | 315 -- examples/examples/test-data/output.h264 | Bin 2566525 -> 0 bytes examples/examples/test-data/output.ogg | Bin 1398440 -> 0 bytes examples/examples/test-data/output_vp8.ivf | Bin 256134 -> 0 bytes examples/examples/test-data/output_vp9.ivf | Bin 224430 -> 0 bytes examples/src/lib.rs | 7 - ice/.gitignore | 11 - ice/CHANGELOG.md | 49 - ice/Cargo.toml | 57 - ice/LICENSE-APACHE | 201 -- ice/LICENSE-MIT | 21 - ice/README.md | 30 - ice/codecov.yml | 23 - ice/doc/webrtc.rs.png | Bin 33276 -> 0 bytes ice/examples/ping_pong.rs | 424 --- ice/src/agent/agent_config.rs | 255 -- ice/src/agent/agent_gather.rs | 892 ----- ice/src/agent/agent_gather_test.rs | 490 --- ice/src/agent/agent_internal.rs | 1198 ------- ice/src/agent/agent_selector.rs | 545 --- ice/src/agent/agent_stats.rs | 283 -- ice/src/agent/agent_test.rs | 2203 ------------ ice/src/agent/agent_transport.rs | 251 -- ice/src/agent/agent_transport_test.rs | 125 - ice/src/agent/agent_vnet_test.rs | 1019 ------ ice/src/agent/mod.rs | 517 --- ice/src/candidate/candidate_base.rs | 525 --- ice/src/candidate/candidate_host.rs | 45 - ice/src/candidate/candidate_pair_test.rs | 155 - ice/src/candidate/candidate_peer_reflexive.rs | 54 - ice/src/candidate/candidate_relay.rs | 57 - ice/src/candidate/candidate_relay_test.rs | 114 - .../candidate/candidate_server_reflexive.rs | 54 - .../candidate_server_reflexive_test.rs | 91 - ice/src/candidate/candidate_test.rs | 411 --- ice/src/candidate/mod.rs | 325 -- ice/src/control/control_test.rs | 168 - ice/src/control/mod.rs | 143 - ice/src/error.rs | 238 -- .../external_ip_mapper_test.rs | 251 -- ice/src/external_ip_mapper/mod.rs | 133 - ice/src/lib.rs | 22 - ice/src/mdns/mdns_test.rs | 151 - ice/src/mdns/mod.rs | 71 - ice/src/network_type/mod.rs | 148 - ice/src/network_type/network_type_test.rs | 95 - ice/src/priority/mod.rs | 36 - ice/src/priority/priority_test.rs | 39 - ice/src/rand/mod.rs | 48 - ice/src/rand/rand_test.rs | 77 - ice/src/state/mod.rs | 112 - ice/src/state/state_test.rs | 46 - ice/src/stats/mod.rs | 178 - ice/src/tcp_type/mod.rs | 48 - ice/src/tcp_type/tcp_type_test.rs | 18 - ice/src/udp_mux/mod.rs | 338 -- ice/src/udp_mux/socket_addr_ext.rs | 246 -- ice/src/udp_mux/udp_mux_conn.rs | 320 -- ice/src/udp_mux/udp_mux_test.rs | 292 -- ice/src/udp_network.rs | 116 - ice/src/url/mod.rs | 266 -- ice/src/url/url_test.rs | 142 - ice/src/use_candidate/mod.rs | 31 - ice/src/use_candidate/use_candidate_test.rs | 19 - ice/src/util/mod.rs | 175 - ice/src/util/util_test.rs | 10 - interceptor/.gitignore | 11 - interceptor/CHANGELOG.md | 23 - interceptor/Cargo.toml | 29 - interceptor/LICENSE-APACHE | 201 -- interceptor/LICENSE-MIT | 21 - interceptor/README.md | 30 - interceptor/codecov.yml | 23 - interceptor/doc/webrtc.rs.png | Bin 33276 -> 0 bytes interceptor/src/chain.rs | 100 - interceptor/src/error.rs | 46 - interceptor/src/lib.rs | 228 -- interceptor/src/mock/mock_builder.rs | 23 - interceptor/src/mock/mock_interceptor.rs | 136 - interceptor/src/mock/mock_stream.rs | 355 -- interceptor/src/mock/mock_time.rs | 36 - interceptor/src/mock/mod.rs | 4 - .../src/nack/generator/generator_stream.rs | 314 -- .../src/nack/generator/generator_test.rs | 66 - interceptor/src/nack/generator/mod.rs | 255 -- interceptor/src/nack/mod.rs | 16 - interceptor/src/nack/responder/mod.rs | 203 -- .../src/nack/responder/responder_stream.rs | 176 - .../src/nack/responder/responder_test.rs | 76 - interceptor/src/noop.rs | 80 - interceptor/src/registry.rs | 44 - interceptor/src/report/mod.rs | 87 - interceptor/src/report/receiver/mod.rs | 225 -- .../src/report/receiver/receiver_stream.rs | 227 -- .../src/report/receiver/receiver_test.rs | 772 ---- interceptor/src/report/sender/mod.rs | 182 - .../src/report/sender/sender_stream.rs | 142 - interceptor/src/report/sender/sender_test.rs | 260 -- interceptor/src/stats/interceptor.rs | 1194 ------- interceptor/src/stats/mod.rs | 617 ---- interceptor/src/stream_info.rs | 37 - interceptor/src/stream_reader.rs | 27 - interceptor/src/twcc/mod.rs | 279 -- interceptor/src/twcc/receiver/mod.rs | 262 -- .../src/twcc/receiver/receiver_stream.rs | 57 - .../src/twcc/receiver/receiver_test.rs | 361 -- interceptor/src/twcc/sender/mod.rs | 132 - interceptor/src/twcc/sender/sender_stream.rs | 40 - interceptor/src/twcc/sender/sender_test.rs | 85 - interceptor/src/twcc/twcc_test.rs | 565 --- mdns/.gitignore | 11 - mdns/CHANGELOG.md | 23 - mdns/Cargo.toml | 53 - mdns/LICENSE-APACHE | 201 -- mdns/LICENSE-MIT | 21 - mdns/README.md | 30 - mdns/codecov.yml | 23 - mdns/doc/webrtc.rs.png | Bin 33276 -> 0 bytes mdns/examples/mdns_query.rs | 90 - mdns/examples/mdns_server.rs | 84 - mdns/examples/mdns_server_query.rs | 55 - mdns/src/config.rs | 14 - mdns/src/conn/conn_test.rs | 47 - mdns/src/conn/mod.rs | 436 --- mdns/src/error.rs | 86 - mdns/src/lib.rs | 9 - mdns/src/message/builder.rs | 212 -- mdns/src/message/header.rs | 165 - mdns/src/message/message_test.rs | 1311 ------- mdns/src/message/mod.rs | 349 -- mdns/src/message/name.rs | 237 -- mdns/src/message/packer.rs | 92 - mdns/src/message/parser.rs | 347 -- mdns/src/message/question.rs | 38 - mdns/src/message/resource/a.rs | 34 - mdns/src/message/resource/aaaa.rs | 34 - mdns/src/message/resource/cname.rs | 34 - mdns/src/message/resource/mod.rs | 273 -- mdns/src/message/resource/mx.rs | 45 - mdns/src/message/resource/ns.rs | 35 - mdns/src/message/resource/opt.rs | 84 - mdns/src/message/resource/ptr.rs | 35 - mdns/src/message/resource/soa.rs | 80 - mdns/src/message/resource/srv.rs | 60 - mdns/src/message/resource/txt.rs | 56 - media/.gitignore | 11 - media/CHANGELOG.md | 23 - media/Cargo.toml | 26 - media/LICENSE-APACHE | 201 -- media/LICENSE-MIT | 21 - media/README.md | 30 - media/benches/audio_buffer.rs | 36 - media/codecov.yml | 23 - media/doc/webrtc.rs.png | Bin 33276 -> 0 bytes media/src/audio/buffer.rs | 443 --- media/src/audio/buffer/info.rs | 118 - media/src/audio/buffer/layout.rs | 179 - media/src/audio/mod.rs | 8 - media/src/audio/sample.rs | 197 -- media/src/error.rs | 71 - media/src/io/h264_reader/h264_reader_test.rs | 106 - media/src/io/h264_reader/mod.rs | 334 -- media/src/io/h264_writer/h264_writer_test.rs | 127 - media/src/io/h264_writer/mod.rs | 83 - media/src/io/ivf_reader/ivf_reader_test.rs | 164 - media/src/io/ivf_reader/mod.rs | 127 - media/src/io/ivf_writer/ivf_writer_test.rs | 194 -- media/src/io/ivf_writer/mod.rs | 122 - media/src/io/mod.rs | 21 - media/src/io/ogg_reader/mod.rs | 204 -- media/src/io/ogg_reader/ogg_reader_test.rs | 111 - media/src/io/ogg_writer/mod.rs | 206 -- media/src/io/ogg_writer/ogg_writer_test.rs | 229 -- media/src/io/sample_builder/mod.rs | 433 --- .../io/sample_builder/sample_builder_test.rs | 1499 -------- .../sample_sequence_location.rs | 81 - .../sample_sequence_location_test.rs | 45 - media/src/lib.rs | 110 - media/src/video/mod.rs | 1 - rtcp/.gitignore | 11 - rtcp/CHANGELOG.md | 19 - rtcp/Cargo.toml | 16 - rtcp/LICENSE-APACHE | 201 -- rtcp/LICENSE-MIT | 21 - rtcp/README.md | 30 - rtcp/codecov.yml | 23 - rtcp/doc/webrtc.rs.png | Bin 33276 -> 0 bytes .../compound_packet/compound_packet_test.rs | 329 -- rtcp/src/compound_packet/mod.rs | 195 -- rtcp/src/error.rs | 120 - rtcp/src/extended_report/dlrr.rs | 151 - .../extended_report/extended_report_test.rs | 184 - rtcp/src/extended_report/mod.rs | 302 -- rtcp/src/extended_report/prt.rs | 150 - rtcp/src/extended_report/rle.rs | 246 -- rtcp/src/extended_report/rrt.rs | 111 - rtcp/src/extended_report/ssr.rs | 235 -- rtcp/src/extended_report/unknown.rs | 98 - rtcp/src/extended_report/vm.rs | 209 -- rtcp/src/goodbye/goodbye_test.rs | 225 -- rtcp/src/goodbye/mod.rs | 197 -- rtcp/src/header.rs | 315 -- rtcp/src/lib.rs | 59 - rtcp/src/packet.rs | 276 -- .../full_intra_request_test.rs | 215 -- .../full_intra_request/mod.rs | 172 - rtcp/src/payload_feedbacks/mod.rs | 4 - .../picture_loss_indication/mod.rs | 141 - .../picture_loss_indication_test.rs | 162 - .../receiver_estimated_maximum_bitrate/mod.rs | 287 -- ...receiver_estimated_maximum_bitrate_test.rs | 121 - .../slice_loss_indication/mod.rs | 180 - .../slice_loss_indication_test.rs | 135 - rtcp/src/raw_packet.rs | 168 - rtcp/src/receiver_report/mod.rs | 230 -- .../receiver_report/receiver_report_test.rs | 242 -- rtcp/src/reception_report.rs | 206 -- rtcp/src/sender_report/mod.rs | 299 -- rtcp/src/sender_report/sender_report_test.rs | 252 -- rtcp/src/source_description/mod.rs | 440 --- .../source_description_test.rs | 359 -- rtcp/src/transport_feedbacks/mod.rs | 3 - .../rapid_resynchronization_request/mod.rs | 144 - .../rapid_resynchronization_request_test.rs | 116 - .../transport_layer_cc/mod.rs | 722 ---- .../transport_layer_cc_test.rs | 927 ----- .../transport_layer_nack/mod.rs | 275 -- .../transport_layer_nack_test.rs | 361 -- rtcp/src/util.rs | 118 - rtp/.gitignore | 11 - rtp/CHANGELOG.md | 20 - rtp/Cargo.toml | 29 - rtp/LICENSE-APACHE | 201 -- rtp/LICENSE-MIT | 21 - rtp/README.md | 30 - rtp/benches/packet_bench.rs | 62 - rtp/codecov.yml | 23 - rtp/doc/webrtc.rs.png | Bin 33276 -> 0 bytes rtp/src/codecs/av1/av1_test.rs | 454 --- rtp/src/codecs/av1/leb128.rs | 64 - rtp/src/codecs/av1/mod.rs | 122 - rtp/src/codecs/av1/obu.rs | 114 - rtp/src/codecs/av1/packetizer.rs | 258 -- rtp/src/codecs/g7xx/g7xx_test.rs | 50 - rtp/src/codecs/g7xx/mod.rs | 43 - rtp/src/codecs/h264/h264_test.rs | 263 -- rtp/src/codecs/h264/mod.rs | 310 -- rtp/src/codecs/h265/h265_test.rs | 889 ----- rtp/src/codecs/h265/mod.rs | 1020 ------ rtp/src/codecs/mod.rs | 7 - rtp/src/codecs/opus/mod.rs | 46 - rtp/src/codecs/opus/opus_test.rs | 51 - rtp/src/codecs/vp8/mod.rs | 246 -- rtp/src/codecs/vp8/vp8_test.rs | 225 -- rtp/src/codecs/vp9/mod.rs | 467 --- rtp/src/codecs/vp9/vp9_test.rs | 364 -- rtp/src/error.rs | 86 - .../abs_send_time_extension_test.rs | 121 - .../extension/abs_send_time_extension/mod.rs | 110 - .../audio_level_extension_test.rs | 66 - .../extension/audio_level_extension/mod.rs | 80 - rtp/src/extension/mod.rs | 97 - .../extension/playout_delay_extension/mod.rs | 82 - .../playout_delay_extension_test.rs | 38 - .../extension/transport_cc_extension/mod.rs | 61 - .../transport_cc_extension_test.rs | 44 - .../video_orientation_extension/mod.rs | 132 - .../video_orientation_extension_test.rs | 113 - rtp/src/header.rs | 477 --- rtp/src/lib.rs | 12 - rtp/src/packet/mod.rs | 122 - rtp/src/packet/packet_test.rs | 1243 ------- rtp/src/packetizer/mod.rs | 165 - rtp/src/packetizer/packetizer_test.rs | 110 - rtp/src/sequence.rs | 74 - sctp/.gitignore | 11 - sctp/CHANGELOG.md | 39 - sctp/Cargo.toml | 51 - sctp/LICENSE-APACHE | 201 -- sctp/LICENSE-MIT | 21 - sctp/README.md | 30 - sctp/codecov.yml | 23 - sctp/doc/webrtc.rs.png | Bin 33276 -> 0 bytes sctp/examples/ping.rs | 118 - sctp/examples/pong.rs | 110 - sctp/examples/throughput.rs | 147 - sctp/fuzz/.gitignore | 2 - sctp/fuzz/Cargo.toml | 32 - ...h-16cad30042bc4791bd62c630a780add5d1220779 | Bin 54 -> 0 bytes ...h-8b9b318a6b66ea23232a4e2aec91deeeca470af8 | Bin 341 -> 0 bytes ...h-8d90dfc8fc34fa06f161f69617ee8f48dec434cd | Bin 85 -> 0 bytes ...h-b836a20af7f8af85423dbe80565465b16bb7a16f | Bin 85 -> 0 bytes ...h-f940d9879efc88872145955bae11ca6ad6a4c044 | Bin 129 -> 0 bytes ...h-216833e417069f431d0617fb4e9f8abe6c9a6c1d | Bin 16 -> 0 bytes ...h-fb1b644bc0d365ce2dc3c3ff77cd3c4cd8da528d | Bin 16 -> 0 bytes sctp/fuzz/fuzz_targets/packet.rs | 10 - sctp/fuzz/fuzz_targets/param.rs | 10 - sctp/src/association/association_internal.rs | 2421 ------------- .../association_internal_test.rs | 544 --- sctp/src/association/association_stats.rs | 61 - sctp/src/association/association_test.rs | 2616 -------------- sctp/src/association/mod.rs | 626 ---- sctp/src/chunk/chunk_abort.rs | 97 - sctp/src/chunk/chunk_cookie_ack.rs | 61 - sctp/src/chunk/chunk_cookie_echo.rs | 68 - sctp/src/chunk/chunk_error.rs | 99 - sctp/src/chunk/chunk_forward_tsn.rs | 184 - sctp/src/chunk/chunk_header.rs | 111 - sctp/src/chunk/chunk_heartbeat.rs | 101 - sctp/src/chunk/chunk_heartbeat_ack.rs | 129 - sctp/src/chunk/chunk_init.rs | 304 -- sctp/src/chunk/chunk_payload_data.rs | 272 -- sctp/src/chunk/chunk_reconfig.rs | 133 - sctp/src/chunk/chunk_selective_ack.rs | 167 - sctp/src/chunk/chunk_shutdown.rs | 76 - sctp/src/chunk/chunk_shutdown_ack.rs | 61 - sctp/src/chunk/chunk_shutdown_complete.rs | 61 - sctp/src/chunk/chunk_test.rs | 752 ---- sctp/src/chunk/chunk_type.rs | 89 - sctp/src/chunk/chunk_unknown.rs | 55 - sctp/src/chunk/mod.rs | 47 - sctp/src/error.rs | 238 -- sctp/src/error_cause.rs | 136 - sctp/src/fuzz_artifact_test.rs | 44 - sctp/src/lib.rs | 18 - sctp/src/packet.rs | 304 -- sctp/src/param/mod.rs | 89 - sctp/src/param/param_chunk_list.rs | 73 - sctp/src/param/param_forward_tsn_supported.rs | 53 - sctp/src/param/param_header.rs | 65 - sctp/src/param/param_heartbeat_info.rs | 52 - .../src/param/param_outgoing_reset_request.rs | 124 - sctp/src/param/param_random.rs | 50 - sctp/src/param/param_reconfig_response.rs | 140 - .../param/param_requested_hmac_algorithm.rs | 115 - sctp/src/param/param_state_cookie.rs | 65 - sctp/src/param/param_supported_extensions.rs | 69 - sctp/src/param/param_test.rs | 270 -- sctp/src/param/param_type.rs | 167 - sctp/src/param/param_unknown.rs | 65 - sctp/src/param/param_unrecognized.rs | 65 - sctp/src/queue/control_queue.rs | 6 - sctp/src/queue/mod.rs | 7 - sctp/src/queue/payload_queue.rs | 184 - sctp/src/queue/pending_queue.rs | 260 -- sctp/src/queue/queue_test.rs | 997 ------ sctp/src/queue/reassembly_queue.rs | 353 -- sctp/src/stream/mod.rs | 852 ----- sctp/src/stream/stream_test.rs | 215 -- sctp/src/timer/ack_timer.rs | 74 - sctp/src/timer/mod.rs | 5 - sctp/src/timer/rtx_timer.rs | 208 -- sctp/src/timer/timer_test.rs | 511 --- sctp/src/util.rs | 241 -- sdp/.gitignore | 11 - sdp/CHANGELOG.md | 18 - sdp/Cargo.toml | 23 - sdp/LICENSE-APACHE | 201 -- sdp/LICENSE-MIT | 21 - sdp/README.md | 30 - sdp/benches/bench.rs | 52 - sdp/codecov.yml | 23 - sdp/doc/webrtc.rs.png | Bin 33276 -> 0 bytes sdp/fuzz/.gitignore | 4 - sdp/fuzz/Cargo.toml | 26 - sdp/fuzz/fuzz_targets/parse_session.rs | 7 - sdp/src/description/common.rs | 94 - sdp/src/description/description_test.rs | 607 ---- sdp/src/description/media.rs | 270 -- sdp/src/description/mod.rs | 6 - sdp/src/description/session.rs | 1365 -------- sdp/src/direction/direction_test.rs | 39 - sdp/src/direction/mod.rs | 51 - sdp/src/error.rs | 56 - sdp/src/extmap/extmap_test.rs | 77 - sdp/src/extmap/mod.rs | 120 - sdp/src/lexer/mod.rs | 66 - sdp/src/lib.rs | 14 - sdp/src/util/mod.rs | 246 -- sdp/src/util/util_test.rs | 177 - src/allocation/allocation_manager.rs | 632 ++++ src/allocation/channel_bind.rs | 143 + src/allocation/mod.rs | 769 ++++ src/allocation/permission.rs | 81 + src/attr.rs | 58 + src/chandata.rs | 296 ++ src/con/mod.rs | 137 + src/con/tcp.rs | 299 ++ src/lib.rs | 333 ++ src/relay.rs | 89 + src/server/config.rs | 88 + src/server/mod.rs | 310 ++ src/server/request.rs | 1069 ++++++ srtp/.gitignore | 11 - srtp/CHANGELOG.md | 19 - srtp/Cargo.toml | 57 - srtp/LICENSE-APACHE | 201 -- srtp/LICENSE-MIT | 21 - srtp/README.md | 30 - srtp/benches/srtp_bench.rs | 158 - srtp/codecov.yml | 23 - srtp/doc/webrtc.rs.png | Bin 33276 -> 0 bytes srtp/src/cipher/cipher_aead_aes_gcm.rs | 247 -- .../cipher_aes_cm_hmac_sha1/ctrcipher.rs | 220 -- .../src/cipher/cipher_aes_cm_hmac_sha1/mod.rs | 133 - .../cipher_aes_cm_hmac_sha1/opensslcipher.rs | 261 -- srtp/src/cipher/mod.rs | 61 - srtp/src/config.rs | 83 - srtp/src/context/context_test.rs | 305 -- srtp/src/context/mod.rs | 201 -- srtp/src/context/srtcp.rs | 50 - srtp/src/context/srtcp_test.rs | 257 -- srtp/src/context/srtp.rs | 70 - srtp/src/context/srtp_test.rs | 172 - srtp/src/error.rs | 120 - srtp/src/key_derivation.rs | 173 - srtp/src/lib.rs | 14 - srtp/src/option.rs | 36 - srtp/src/protection_profile.rs | 37 - srtp/src/session/mod.rs | 271 -- srtp/src/session/session_rtcp_test.rs | 239 -- srtp/src/session/session_rtp_test.rs | 308 -- srtp/src/stream.rs | 91 - stun/.gitignore | 11 - stun/CHANGELOG.md | 16 - stun/Cargo.toml | 59 - stun/LICENSE-APACHE | 201 -- stun/LICENSE-MIT | 21 - stun/README.md | 30 - stun/benches/bench.rs | 659 ---- stun/codecov.yml | 23 - stun/doc/webrtc.rs.png | Bin 33276 -> 0 bytes stun/examples/stun_client.rs | 65 - stun/examples/stun_decode.rs | 44 - stun/src/addr.rs | 128 - stun/src/addr/addr_test.rs | 183 - stun/src/agent.rs | 283 -- stun/src/agent/agent_test.rs | 195 -- stun/src/attributes.rs | 209 -- stun/src/attributes/attributes_test.rs | 86 - stun/src/checks.rs | 48 - stun/src/client.rs | 473 --- stun/src/client/client_test.rs | 12 - stun/src/error.rs | 98 - stun/src/error_code.rs | 162 - stun/src/fingerprint.rs | 64 - stun/src/fingerprint/fingerprint_test.rs | 73 - stun/src/integrity.rs | 118 - stun/src/integrity/integrity_test.rs | 93 - stun/src/lib.rs | 26 - stun/src/message.rs | 626 ---- stun/src/message/message_test.rs | 744 ---- stun/src/textattrs.rs | 95 - stun/src/textattrs/textattrs_test.rs | 307 -- stun/src/uattrs.rs | 62 - stun/src/uattrs/uattrs_test.rs | 37 - stun/src/uri.rs | 73 - stun/src/uri/uri_test.rs | 68 - stun/src/xoraddr.rs | 173 - stun/src/xoraddr/xoraddr_test.rs | 250 -- turn/.gitignore | 11 - turn/Cargo.toml | 62 - turn/LICENSE-APACHE | 201 -- turn/LICENSE-MIT | 21 - turn/README.md | 30 - turn/benches/bench.rs | 137 - turn/codecov.yml | 23 - turn/doc/webrtc.rs.png | Bin 33276 -> 0 bytes turn/examples/turn_client_udp.rs | 197 -- turn/examples/turn_server_udp.rs | 139 - turn/src/allocation/allocation_manager.rs | 198 -- .../allocation_manager_test.rs | 613 ---- turn/src/allocation/allocation_test.rs | 296 -- turn/src/allocation/channel_bind.rs | 87 - .../channel_bind/channel_bind_test.rs | 76 - turn/src/allocation/five_tuple.rs | 46 - .../allocation/five_tuple/five_tuple_test.rs | 100 - turn/src/allocation/mod.rs | 468 --- turn/src/allocation/permission.rs | 81 - turn/src/auth/auth_test.rs | 103 - turn/src/auth/mod.rs | 77 - turn/src/client/binding.rs | 136 - turn/src/client/binding/binding_test.rs | 83 - turn/src/client/client_test.rs | 191 - turn/src/client/mod.rs | 652 ---- turn/src/client/periodic_timer.rs | 93 - .../periodic_timer/periodic_timer_test.rs | 37 - turn/src/client/permission.rs | 73 - turn/src/client/relay_conn.rs | 630 ---- turn/src/client/relay_conn/relay_conn_test.rs | 84 - turn/src/client/transaction.rs | 282 -- turn/src/error.rs | 193 - turn/src/lib.rs | 13 - turn/src/proto/addr.rs | 62 - turn/src/proto/addr/addr_test.rs | 104 - turn/src/proto/chandata.rs | 110 - turn/src/proto/chandata/chandata_test.rs | 211 -- turn/src/proto/channum.rs | 70 - turn/src/proto/channum/channnum_test.rs | 81 - turn/src/proto/data.rs | 33 - turn/src/proto/data/data_test.rs | 33 - turn/src/proto/dontfrag.rs | 25 - turn/src/proto/dontfrag/dontfrag_test.rs | 27 - turn/src/proto/evenport.rs | 63 - turn/src/proto/evenport/evenport_test.rs | 78 - turn/src/proto/lifetime.rs | 59 - turn/src/proto/lifetime/lifetime_test.rs | 54 - turn/src/proto/mod.rs | 70 - turn/src/proto/peeraddr.rs | 71 - turn/src/proto/peeraddr/peeraddr_test.rs | 26 - turn/src/proto/proto_test.rs | 35 - turn/src/proto/relayaddr.rs | 69 - turn/src/proto/relayaddr/relayaddr_test.rs | 26 - turn/src/proto/reqfamily.rs | 61 - turn/src/proto/reqfamily/reqfamily_test.rs | 77 - turn/src/proto/reqtrans.rs | 54 - turn/src/proto/reqtrans/reqtrans_test.rs | 75 - turn/src/proto/rsrvtoken.rs | 40 - turn/src/proto/rsrvtoken/rsrvtoken_test.rs | 60 - turn/src/relay/mod.rs | 26 - turn/src/relay/relay_none.rs | 37 - turn/src/relay/relay_range.rs | 85 - turn/src/relay/relay_static.rs | 45 - turn/src/server/config.rs | 58 - turn/src/server/mod.rs | 261 -- turn/src/server/request.rs | 1031 ------ turn/src/server/request/request_test.rs | 119 - turn/src/server/server_test.rs | 338 -- util/.gitignore | 11 - util/CHANGELOG.md | 24 - util/Cargo.toml | 66 - util/LICENSE-APACHE | 201 -- util/LICENSE-MIT | 21 - util/README.md | 30 - util/benches/bench.rs | 33 - util/codecov.yml | 23 - util/doc/webrtc.rs.png | Bin 33276 -> 0 bytes util/examples/display-interfaces.rs | 11 - util/src/buffer/buffer_test.rs | 358 -- util/src/buffer/mod.rs | 322 -- util/src/conn/conn_bridge.rs | 236 -- util/src/conn/conn_bridge_test.rs | 255 -- util/src/conn/conn_disconnected_packet.rs | 65 - util/src/conn/conn_pipe.rs | 80 - util/src/conn/conn_pipe_test.rs | 27 - util/src/conn/conn_test.rs | 22 - util/src/conn/conn_udp.rs | 42 - util/src/conn/conn_udp_listener.rs | 298 -- util/src/conn/conn_udp_listener_test.rs | 220 -- util/src/conn/mod.rs | 73 - util/src/error.rs | 174 - util/src/fixed_big_int/fixed_big_int_test.rs | 78 - util/src/fixed_big_int/mod.rs | 96 - util/src/ifaces/ffi/mod.rs | 9 - util/src/ifaces/ffi/unix/mod.rs | 79 - util/src/ifaces/ffi/windows/mod.rs | 393 --- util/src/ifaces/mod.rs | 26 - util/src/lib.rs | 88 - util/src/marshal/exact_size_buf.rs | 96 - util/src/marshal/mod.rs | 34 - util/src/replay_detector/mod.rs | 177 - .../replay_detector/replay_detector_test.rs | 278 -- util/src/sync/mod.rs | 99 - util/src/vnet/chunk.rs | 352 -- util/src/vnet/chunk/chunk_test.rs | 59 - util/src/vnet/chunk_queue.rs | 44 - util/src/vnet/chunk_queue/chunk_queue_test.rs | 47 - util/src/vnet/conn.rs | 164 - util/src/vnet/conn/conn_test.rs | 208 -- util/src/vnet/conn_map.rs | 120 - util/src/vnet/conn_map/conn_map_test.rs | 314 -- util/src/vnet/interface.rs | 37 - util/src/vnet/mod.rs | 9 - util/src/vnet/nat.rs | 464 --- util/src/vnet/nat/nat_test.rs | 638 ---- util/src/vnet/net.rs | 566 --- util/src/vnet/net/net_test.rs | 903 ----- util/src/vnet/resolver.rs | 68 - util/src/vnet/resolver/resolver_test.rs | 71 - util/src/vnet/router.rs | 586 ---- util/src/vnet/router/router_test.rs | 810 ----- webrtc/CHANGELOG.md | 236 -- webrtc/Cargo.toml | 71 - webrtc/src/api/api_test.rs | 25 - .../interceptor_registry_test.rs | 278 -- webrtc/src/api/interceptor_registry/mod.rs | 171 - .../src/api/media_engine/media_engine_test.rs | 780 ----- webrtc/src/api/media_engine/mod.rs | 819 ----- webrtc/src/api/mod.rs | 238 -- webrtc/src/api/setting_engine/mod.rs | 327 -- .../api/setting_engine/setting_engine_test.rs | 271 -- webrtc/src/data_channel/data_channel_init.rs | 29 - .../src/data_channel/data_channel_message.rs | 11 - .../data_channel/data_channel_parameters.rs | 12 - webrtc/src/data_channel/data_channel_state.rs | 113 - webrtc/src/data_channel/data_channel_test.rs | 1504 -------- webrtc/src/data_channel/mod.rs | 556 --- webrtc/src/dtls_transport/dtls_fingerprint.rs | 15 - webrtc/src/dtls_transport/dtls_parameters.rs | 11 - webrtc/src/dtls_transport/dtls_role.rs | 170 - .../dtls_transport/dtls_transport_state.rs | 117 - .../src/dtls_transport/dtls_transport_test.rs | 204 -- webrtc/src/dtls_transport/mod.rs | 617 ---- webrtc/src/error.rs | 482 --- webrtc/src/ice_transport/ice_candidate.rs | 194 -- .../src/ice_transport/ice_candidate_pair.rs | 34 - .../src/ice_transport/ice_candidate_type.rs | 119 - .../src/ice_transport/ice_connection_state.rs | 142 - .../src/ice_transport/ice_credential_type.rs | 74 - webrtc/src/ice_transport/ice_gatherer.rs | 410 --- .../src/ice_transport/ice_gatherer_state.rs | 88 - .../src/ice_transport/ice_gathering_state.rs | 83 - webrtc/src/ice_transport/ice_parameters.rs | 10 - webrtc/src/ice_transport/ice_protocol.rs | 79 - webrtc/src/ice_transport/ice_role.rs | 73 - webrtc/src/ice_transport/ice_server.rs | 173 - .../src/ice_transport/ice_transport_state.rs | 185 - .../src/ice_transport/ice_transport_test.rs | 122 - webrtc/src/ice_transport/mod.rs | 356 -- webrtc/src/lib.rs | 44 - webrtc/src/mux/endpoint.rs | 77 - webrtc/src/mux/mod.rs | 157 - webrtc/src/mux/mux_func.rs | 63 - webrtc/src/mux/mux_test.rs | 142 - webrtc/src/peer_connection/certificate.rs | 291 -- webrtc/src/peer_connection/configuration.rs | 148 - webrtc/src/peer_connection/mod.rs | 2118 ----------- .../peer_connection/offer_answer_options.rs | 22 - webrtc/src/peer_connection/operation/mod.rs | 140 - .../operation/operation_test.rs | 47 - .../peer_connection_internal.rs | 1522 -------- .../peer_connection/peer_connection_state.rs | 151 - .../peer_connection/peer_connection_test.rs | 640 ---- .../peer_connection/policy/bundle_policy.rs | 94 - .../policy/ice_transport_policy.rs | 77 - webrtc/src/peer_connection/policy/mod.rs | 4 - .../peer_connection/policy/rtcp_mux_policy.rs | 79 - .../peer_connection/policy/sdp_semantics.rs | 112 - webrtc/src/peer_connection/sdp/mod.rs | 1129 ------ webrtc/src/peer_connection/sdp/sdp_test.rs | 1378 -------- webrtc/src/peer_connection/sdp/sdp_type.rs | 101 - .../sdp/session_description.rs | 238 -- webrtc/src/peer_connection/signaling_state.rs | 365 -- .../fmtp/generic/generic_test.rs | 160 - .../src/rtp_transceiver/fmtp/generic/mod.rs | 65 - .../rtp_transceiver/fmtp/h264/h264_test.rs | 163 - webrtc/src/rtp_transceiver/fmtp/h264/mod.rs | 102 - webrtc/src/rtp_transceiver/fmtp/mod.rs | 58 - webrtc/src/rtp_transceiver/mod.rs | 561 --- webrtc/src/rtp_transceiver/rtp_codec.rs | 165 - .../src/rtp_transceiver/rtp_receiver/mod.rs | 853 ----- .../rtp_receiver/rtp_receiver_test.rs | 233 -- webrtc/src/rtp_transceiver/rtp_sender/mod.rs | 593 ---- .../rtp_sender/rtp_sender_test.rs | 622 ---- .../rtp_transceiver_direction.rs | 210 -- .../rtp_transceiver/rtp_transceiver_test.rs | 356 -- .../src/rtp_transceiver/srtp_writer_future.rs | 290 -- webrtc/src/sctp_transport/mod.rs | 442 --- .../sctp_transport_capabilities.rs | 7 - .../sctp_transport/sctp_transport_state.rs | 99 - .../src/sctp_transport/sctp_transport_test.rs | 43 - webrtc/src/stats/mod.rs | 688 ---- webrtc/src/stats/serialize.rs | 50 - webrtc/src/stats/stats_collector.rs | 32 - webrtc/src/track/mod.rs | 29 - webrtc/src/track/track_local/mod.rs | 170 - .../track_local/track_local_static_rtp.rs | 295 -- .../track_local/track_local_static_sample.rs | 324 -- .../track_local/track_local_static_test.rs | 434 --- webrtc/src/track/track_remote/mod.rs | 321 -- 955 files changed, 4932 insertions(+), 168901 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/actions-rs/grcov.yml delete mode 100644 .github/workflows/cargo.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/grcov.yml delete mode 100644 .gitmodules create mode 100644 .rustfmt.toml rename turn/CHANGELOG.md => CHANGELOG.md (100%) create mode 100644 Makefile delete mode 100644 _typos.toml delete mode 100644 codecov.yml delete mode 100644 constraints/.gitignore delete mode 100644 constraints/CHANGELOG.md delete mode 100644 constraints/Cargo.toml delete mode 100644 constraints/LICENSE-APACHE delete mode 100644 constraints/LICENSE-MIT delete mode 100644 constraints/README.md delete mode 100644 constraints/examples/json.rs delete mode 100644 constraints/examples/macros.rs delete mode 100644 constraints/examples/native.rs delete mode 100644 constraints/src/algorithms.rs delete mode 100644 constraints/src/algorithms/fitness_distance.rs delete mode 100644 constraints/src/algorithms/fitness_distance/empty_constraint.rs delete mode 100644 constraints/src/algorithms/fitness_distance/setting.rs delete mode 100644 constraints/src/algorithms/fitness_distance/settings.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint/tests.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint/tests/bool.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint/tests/f64.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint/tests/string.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_constraint/tests/u64.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_range_constraint.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_range_constraint/tests.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_range_constraint/tests/empty.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_range_constraint/tests/f64.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_range_constraint/tests/u64.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/bool.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/f64.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/string.rs delete mode 100644 constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/u64.rs delete mode 100644 constraints/src/algorithms/select_settings.rs delete mode 100644 constraints/src/algorithms/select_settings/apply_advanced.rs delete mode 100644 constraints/src/algorithms/select_settings/apply_mandatory.rs delete mode 100644 constraints/src/algorithms/select_settings/select_optimal.rs delete mode 100644 constraints/src/algorithms/select_settings/tests.rs delete mode 100644 constraints/src/algorithms/select_settings/tie_breaking.rs delete mode 100644 constraints/src/capabilities.rs delete mode 100644 constraints/src/capabilities/stream.rs delete mode 100644 constraints/src/capabilities/track.rs delete mode 100644 constraints/src/capability.rs delete mode 100644 constraints/src/capability/value.rs delete mode 100644 constraints/src/capability/value_range.rs delete mode 100644 constraints/src/capability/value_sequence.rs delete mode 100644 constraints/src/constraint.rs delete mode 100644 constraints/src/constraint/value.rs delete mode 100644 constraints/src/constraint/value_range.rs delete mode 100644 constraints/src/constraint/value_sequence.rs delete mode 100644 constraints/src/constraints.rs delete mode 100644 constraints/src/constraints/advanced.rs delete mode 100644 constraints/src/constraints/constraint_set.rs delete mode 100644 constraints/src/constraints/mandatory.rs delete mode 100644 constraints/src/constraints/stream.rs delete mode 100644 constraints/src/constraints/track.rs delete mode 100644 constraints/src/enumerations.rs delete mode 100644 constraints/src/errors.rs delete mode 100644 constraints/src/lib.rs delete mode 100644 constraints/src/macros.rs delete mode 100644 constraints/src/property.rs delete mode 100644 constraints/src/setting.rs delete mode 100644 constraints/src/settings.rs delete mode 100644 constraints/src/settings/stream.rs delete mode 100644 constraints/src/settings/track.rs delete mode 100644 constraints/src/supported_constraints.rs delete mode 100644 constraints/tests/w3c_spec_examples.rs delete mode 100644 data/.gitignore delete mode 100644 data/CHANGELOG.md delete mode 100644 data/Cargo.toml delete mode 100644 data/LICENSE-APACHE delete mode 100644 data/LICENSE-MIT delete mode 100644 data/README.md delete mode 100644 data/codecov.yml delete mode 100644 data/doc/webrtc.rs.png delete mode 100644 data/src/data_channel/data_channel_test.rs delete mode 100644 data/src/data_channel/mod.rs delete mode 100644 data/src/error.rs delete mode 100644 data/src/lib.rs delete mode 100644 data/src/message/message_channel_ack.rs delete mode 100644 data/src/message/message_channel_open.rs delete mode 100644 data/src/message/message_test.rs delete mode 100644 data/src/message/message_type.rs delete mode 100644 data/src/message/mod.rs delete mode 100644 doc/AVStack.jpg delete mode 100644 doc/ChannelTalk_logo.png delete mode 100644 doc/KittyCAD.png delete mode 100644 doc/check.png delete mode 100644 doc/embark.jpg delete mode 100644 doc/parity.png delete mode 100644 doc/uncheck.png delete mode 100644 doc/webrtc.rs.png delete mode 100644 doc/webrtc.rs.xcf delete mode 100644 doc/webrtc_crab.png delete mode 100644 doc/webrtc_crates_dep_graph.odg delete mode 100644 doc/webrtc_crates_dep_graph.png delete mode 100644 doc/webrtc_stack.png delete mode 100644 dtls/.gitignore delete mode 100644 dtls/CHANGELOG.md delete mode 100644 dtls/Cargo.toml delete mode 100644 dtls/LICENSE-APACHE delete mode 100644 dtls/LICENSE-MIT delete mode 100644 dtls/README.md delete mode 100644 dtls/codecov.yml delete mode 100644 dtls/doc/webrtc.rs.png delete mode 100644 dtls/examples/certificates/README.md delete mode 100644 dtls/examples/certificates/client.csr delete mode 100644 dtls/examples/certificates/client.pem delete mode 100644 dtls/examples/certificates/client.pem.private_key.pem delete mode 100644 dtls/examples/certificates/client.pub.pem delete mode 100644 dtls/examples/certificates/extfile.conf delete mode 100644 dtls/examples/certificates/server.csr delete mode 100644 dtls/examples/certificates/server.pem delete mode 100644 dtls/examples/certificates/server.pem.private_key.pem delete mode 100644 dtls/examples/certificates/server.pub.pem delete mode 100644 dtls/examples/dial/psk/dial_psk.rs delete mode 100644 dtls/examples/dial/selfsign/dial_selfsign.rs delete mode 100644 dtls/examples/dial/verify/dial_verify.rs delete mode 100644 dtls/examples/hub/Cargo.toml delete mode 100644 dtls/examples/hub/src/lib.rs delete mode 100644 dtls/examples/hub/src/utilities.rs delete mode 100644 dtls/examples/listen/psk/listen_psk.rs delete mode 100644 dtls/examples/listen/selfsign/listen_selfsign.rs delete mode 100644 dtls/examples/listen/verify/listen_verify.rs delete mode 100644 dtls/src/alert/alert_test.rs delete mode 100644 dtls/src/alert/mod.rs delete mode 100644 dtls/src/application_data.rs delete mode 100644 dtls/src/change_cipher_spec/change_cipher_spec_test.rs delete mode 100644 dtls/src/change_cipher_spec/mod.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs delete mode 100644 dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs delete mode 100644 dtls/src/cipher_suite/mod.rs delete mode 100644 dtls/src/client_certificate_type.rs delete mode 100644 dtls/src/compression_methods.rs delete mode 100644 dtls/src/config.rs delete mode 100644 dtls/src/conn/conn_test.rs delete mode 100644 dtls/src/conn/mod.rs delete mode 100644 dtls/src/content.rs delete mode 100644 dtls/src/crypto/crypto_cbc.rs delete mode 100644 dtls/src/crypto/crypto_ccm.rs delete mode 100644 dtls/src/crypto/crypto_gcm.rs delete mode 100644 dtls/src/crypto/crypto_test.rs delete mode 100644 dtls/src/crypto/mod.rs delete mode 100644 dtls/src/crypto/padding.rs delete mode 100644 dtls/src/curve/mod.rs delete mode 100644 dtls/src/curve/named_curve.rs delete mode 100644 dtls/src/error.rs delete mode 100644 dtls/src/extension/extension_server_name.rs delete mode 100644 dtls/src/extension/extension_server_name/extension_server_name_test.rs delete mode 100644 dtls/src/extension/extension_supported_elliptic_curves.rs delete mode 100644 dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs delete mode 100644 dtls/src/extension/extension_supported_point_formats.rs delete mode 100644 dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs delete mode 100644 dtls/src/extension/extension_supported_signature_algorithms.rs delete mode 100644 dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs delete mode 100644 dtls/src/extension/extension_use_extended_master_secret.rs delete mode 100644 dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs delete mode 100644 dtls/src/extension/extension_use_srtp.rs delete mode 100644 dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs delete mode 100644 dtls/src/extension/mod.rs delete mode 100644 dtls/src/extension/renegotiation_info.rs delete mode 100644 dtls/src/extension/renegotiation_info/renegotiation_info_test.rs delete mode 100644 dtls/src/flight/flight0.rs delete mode 100644 dtls/src/flight/flight1.rs delete mode 100644 dtls/src/flight/flight2.rs delete mode 100644 dtls/src/flight/flight3.rs delete mode 100644 dtls/src/flight/flight4.rs delete mode 100644 dtls/src/flight/flight5.rs delete mode 100644 dtls/src/flight/flight6.rs delete mode 100644 dtls/src/flight/mod.rs delete mode 100644 dtls/src/fragment_buffer/fragment_buffer_test.rs delete mode 100644 dtls/src/fragment_buffer/mod.rs delete mode 100644 dtls/src/handshake/handshake_cache.rs delete mode 100644 dtls/src/handshake/handshake_cache/handshake_cache_test.rs delete mode 100644 dtls/src/handshake/handshake_header.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate_request.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate_verify.rs delete mode 100644 dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs delete mode 100644 dtls/src/handshake/handshake_message_client_hello.rs delete mode 100644 dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs delete mode 100644 dtls/src/handshake/handshake_message_client_key_exchange.rs delete mode 100644 dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs delete mode 100644 dtls/src/handshake/handshake_message_finished.rs delete mode 100644 dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs delete mode 100644 dtls/src/handshake/handshake_message_hello_verify_request.rs delete mode 100644 dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs delete mode 100644 dtls/src/handshake/handshake_message_server_hello.rs delete mode 100644 dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs delete mode 100644 dtls/src/handshake/handshake_message_server_hello_done.rs delete mode 100644 dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs delete mode 100644 dtls/src/handshake/handshake_message_server_key_exchange.rs delete mode 100644 dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs delete mode 100644 dtls/src/handshake/handshake_random.rs delete mode 100644 dtls/src/handshake/handshake_test.rs delete mode 100644 dtls/src/handshake/mod.rs delete mode 100644 dtls/src/handshaker.rs delete mode 100644 dtls/src/lib.rs delete mode 100644 dtls/src/listener.rs delete mode 100644 dtls/src/prf/mod.rs delete mode 100644 dtls/src/prf/prf_test.rs delete mode 100644 dtls/src/record_layer/mod.rs delete mode 100644 dtls/src/record_layer/record_layer_header.rs delete mode 100644 dtls/src/record_layer/record_layer_test.rs delete mode 100644 dtls/src/signature_hash_algorithm/mod.rs delete mode 100644 dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs delete mode 100644 dtls/src/state.rs delete mode 100644 examples/.gitignore delete mode 100644 examples/Cargo.toml delete mode 100644 examples/LICENSE-APACHE delete mode 100644 examples/LICENSE-MIT delete mode 100644 examples/README.md delete mode 100644 examples/codecov.yml delete mode 100644 examples/doc/webrtc.rs.png delete mode 100644 examples/examples/README.md delete mode 100644 examples/examples/broadcast/README.md delete mode 100644 examples/examples/broadcast/broadcast.rs delete mode 100644 examples/examples/data-channels-close/README.md delete mode 100644 examples/examples/data-channels-close/data-channels-close.rs delete mode 100644 examples/examples/data-channels-create/README.md delete mode 100644 examples/examples/data-channels-create/data-channels-create.rs delete mode 100644 examples/examples/data-channels-detach-create/README.md delete mode 100644 examples/examples/data-channels-detach-create/data-channels-detach-create.rs delete mode 100644 examples/examples/data-channels-detach/README.md delete mode 100644 examples/examples/data-channels-detach/data-channels-detach.rs delete mode 100644 examples/examples/data-channels-flow-control/README.md delete mode 100644 examples/examples/data-channels-flow-control/data-channels-flow-control.rs delete mode 100644 examples/examples/data-channels/README.md delete mode 100644 examples/examples/data-channels/data-channels.rs delete mode 100644 examples/examples/ice-restart/README.md delete mode 100644 examples/examples/ice-restart/ice-restart.rs delete mode 100644 examples/examples/ice-restart/index.html delete mode 100644 examples/examples/insertable-streams/README.md delete mode 100644 examples/examples/insertable-streams/insertable-streams.rs delete mode 100644 examples/examples/offer-answer/README.md delete mode 100644 examples/examples/offer-answer/answer.rs delete mode 100644 examples/examples/offer-answer/offer.rs delete mode 100644 examples/examples/ortc/README.md delete mode 100644 examples/examples/ortc/ortc.rs delete mode 100644 examples/examples/play-from-disk-h264/README.md delete mode 100644 examples/examples/play-from-disk-h264/play-from-disk-h264.rs delete mode 100644 examples/examples/play-from-disk-hevc/README.md delete mode 100644 examples/examples/play-from-disk-hevc/play-from-disk-hevc.rs delete mode 100644 examples/examples/play-from-disk-renegotiation/README.md delete mode 100644 examples/examples/play-from-disk-renegotiation/index.html delete mode 100644 examples/examples/play-from-disk-renegotiation/play-from-disk-renegotiation.rs delete mode 100644 examples/examples/play-from-disk-vpx/README.md delete mode 100644 examples/examples/play-from-disk-vpx/play-from-disk-vpx.rs delete mode 100644 examples/examples/rc-cycle/rc-cycle.rs delete mode 100644 examples/examples/reflect/README.md delete mode 100644 examples/examples/reflect/reflect.rs delete mode 100644 examples/examples/rtp-forwarder/README.md delete mode 100644 examples/examples/rtp-forwarder/rtp-forwarder.rs delete mode 100644 examples/examples/rtp-forwarder/rtp-forwarder.sdp delete mode 100644 examples/examples/rtp-to-webrtc/README.md delete mode 100644 examples/examples/rtp-to-webrtc/rtp-to-webrtc.rs delete mode 100644 examples/examples/save-to-disk-h264/README.md delete mode 100644 examples/examples/save-to-disk-h264/save-to-disk-h264.rs delete mode 100644 examples/examples/save-to-disk-vpx/README.md delete mode 100644 examples/examples/save-to-disk-vpx/save-to-disk-vpx.rs delete mode 100644 examples/examples/signal/Cargo.toml delete mode 100644 examples/examples/signal/src/lib.rs delete mode 100644 examples/examples/simulcast/README.md delete mode 100644 examples/examples/simulcast/simulcast.rs delete mode 100644 examples/examples/swap-tracks/README.md delete mode 100644 examples/examples/swap-tracks/swap-tracks.rs delete mode 100644 examples/examples/test-data/output.h264 delete mode 100644 examples/examples/test-data/output.ogg delete mode 100644 examples/examples/test-data/output_vp8.ivf delete mode 100644 examples/examples/test-data/output_vp9.ivf delete mode 100644 examples/src/lib.rs delete mode 100644 ice/.gitignore delete mode 100644 ice/CHANGELOG.md delete mode 100644 ice/Cargo.toml delete mode 100644 ice/LICENSE-APACHE delete mode 100644 ice/LICENSE-MIT delete mode 100644 ice/README.md delete mode 100644 ice/codecov.yml delete mode 100644 ice/doc/webrtc.rs.png delete mode 100644 ice/examples/ping_pong.rs delete mode 100644 ice/src/agent/agent_config.rs delete mode 100644 ice/src/agent/agent_gather.rs delete mode 100644 ice/src/agent/agent_gather_test.rs delete mode 100644 ice/src/agent/agent_internal.rs delete mode 100644 ice/src/agent/agent_selector.rs delete mode 100644 ice/src/agent/agent_stats.rs delete mode 100644 ice/src/agent/agent_test.rs delete mode 100644 ice/src/agent/agent_transport.rs delete mode 100644 ice/src/agent/agent_transport_test.rs delete mode 100644 ice/src/agent/agent_vnet_test.rs delete mode 100644 ice/src/agent/mod.rs delete mode 100644 ice/src/candidate/candidate_base.rs delete mode 100644 ice/src/candidate/candidate_host.rs delete mode 100644 ice/src/candidate/candidate_pair_test.rs delete mode 100644 ice/src/candidate/candidate_peer_reflexive.rs delete mode 100644 ice/src/candidate/candidate_relay.rs delete mode 100644 ice/src/candidate/candidate_relay_test.rs delete mode 100644 ice/src/candidate/candidate_server_reflexive.rs delete mode 100644 ice/src/candidate/candidate_server_reflexive_test.rs delete mode 100644 ice/src/candidate/candidate_test.rs delete mode 100644 ice/src/candidate/mod.rs delete mode 100644 ice/src/control/control_test.rs delete mode 100644 ice/src/control/mod.rs delete mode 100644 ice/src/error.rs delete mode 100644 ice/src/external_ip_mapper/external_ip_mapper_test.rs delete mode 100644 ice/src/external_ip_mapper/mod.rs delete mode 100644 ice/src/lib.rs delete mode 100644 ice/src/mdns/mdns_test.rs delete mode 100644 ice/src/mdns/mod.rs delete mode 100644 ice/src/network_type/mod.rs delete mode 100644 ice/src/network_type/network_type_test.rs delete mode 100644 ice/src/priority/mod.rs delete mode 100644 ice/src/priority/priority_test.rs delete mode 100644 ice/src/rand/mod.rs delete mode 100644 ice/src/rand/rand_test.rs delete mode 100644 ice/src/state/mod.rs delete mode 100644 ice/src/state/state_test.rs delete mode 100644 ice/src/stats/mod.rs delete mode 100644 ice/src/tcp_type/mod.rs delete mode 100644 ice/src/tcp_type/tcp_type_test.rs delete mode 100644 ice/src/udp_mux/mod.rs delete mode 100644 ice/src/udp_mux/socket_addr_ext.rs delete mode 100644 ice/src/udp_mux/udp_mux_conn.rs delete mode 100644 ice/src/udp_mux/udp_mux_test.rs delete mode 100644 ice/src/udp_network.rs delete mode 100644 ice/src/url/mod.rs delete mode 100644 ice/src/url/url_test.rs delete mode 100644 ice/src/use_candidate/mod.rs delete mode 100644 ice/src/use_candidate/use_candidate_test.rs delete mode 100644 ice/src/util/mod.rs delete mode 100644 ice/src/util/util_test.rs delete mode 100644 interceptor/.gitignore delete mode 100644 interceptor/CHANGELOG.md delete mode 100644 interceptor/Cargo.toml delete mode 100644 interceptor/LICENSE-APACHE delete mode 100644 interceptor/LICENSE-MIT delete mode 100644 interceptor/README.md delete mode 100644 interceptor/codecov.yml delete mode 100644 interceptor/doc/webrtc.rs.png delete mode 100644 interceptor/src/chain.rs delete mode 100644 interceptor/src/error.rs delete mode 100644 interceptor/src/lib.rs delete mode 100644 interceptor/src/mock/mock_builder.rs delete mode 100644 interceptor/src/mock/mock_interceptor.rs delete mode 100644 interceptor/src/mock/mock_stream.rs delete mode 100644 interceptor/src/mock/mock_time.rs delete mode 100644 interceptor/src/mock/mod.rs delete mode 100644 interceptor/src/nack/generator/generator_stream.rs delete mode 100644 interceptor/src/nack/generator/generator_test.rs delete mode 100644 interceptor/src/nack/generator/mod.rs delete mode 100644 interceptor/src/nack/mod.rs delete mode 100644 interceptor/src/nack/responder/mod.rs delete mode 100644 interceptor/src/nack/responder/responder_stream.rs delete mode 100644 interceptor/src/nack/responder/responder_test.rs delete mode 100644 interceptor/src/noop.rs delete mode 100644 interceptor/src/registry.rs delete mode 100644 interceptor/src/report/mod.rs delete mode 100644 interceptor/src/report/receiver/mod.rs delete mode 100644 interceptor/src/report/receiver/receiver_stream.rs delete mode 100644 interceptor/src/report/receiver/receiver_test.rs delete mode 100644 interceptor/src/report/sender/mod.rs delete mode 100644 interceptor/src/report/sender/sender_stream.rs delete mode 100644 interceptor/src/report/sender/sender_test.rs delete mode 100644 interceptor/src/stats/interceptor.rs delete mode 100644 interceptor/src/stats/mod.rs delete mode 100644 interceptor/src/stream_info.rs delete mode 100644 interceptor/src/stream_reader.rs delete mode 100644 interceptor/src/twcc/mod.rs delete mode 100644 interceptor/src/twcc/receiver/mod.rs delete mode 100644 interceptor/src/twcc/receiver/receiver_stream.rs delete mode 100644 interceptor/src/twcc/receiver/receiver_test.rs delete mode 100644 interceptor/src/twcc/sender/mod.rs delete mode 100644 interceptor/src/twcc/sender/sender_stream.rs delete mode 100644 interceptor/src/twcc/sender/sender_test.rs delete mode 100644 interceptor/src/twcc/twcc_test.rs delete mode 100644 mdns/.gitignore delete mode 100644 mdns/CHANGELOG.md delete mode 100644 mdns/Cargo.toml delete mode 100644 mdns/LICENSE-APACHE delete mode 100644 mdns/LICENSE-MIT delete mode 100644 mdns/README.md delete mode 100644 mdns/codecov.yml delete mode 100644 mdns/doc/webrtc.rs.png delete mode 100644 mdns/examples/mdns_query.rs delete mode 100644 mdns/examples/mdns_server.rs delete mode 100644 mdns/examples/mdns_server_query.rs delete mode 100644 mdns/src/config.rs delete mode 100644 mdns/src/conn/conn_test.rs delete mode 100644 mdns/src/conn/mod.rs delete mode 100644 mdns/src/error.rs delete mode 100644 mdns/src/lib.rs delete mode 100644 mdns/src/message/builder.rs delete mode 100644 mdns/src/message/header.rs delete mode 100644 mdns/src/message/message_test.rs delete mode 100644 mdns/src/message/mod.rs delete mode 100644 mdns/src/message/name.rs delete mode 100644 mdns/src/message/packer.rs delete mode 100644 mdns/src/message/parser.rs delete mode 100644 mdns/src/message/question.rs delete mode 100644 mdns/src/message/resource/a.rs delete mode 100644 mdns/src/message/resource/aaaa.rs delete mode 100644 mdns/src/message/resource/cname.rs delete mode 100644 mdns/src/message/resource/mod.rs delete mode 100644 mdns/src/message/resource/mx.rs delete mode 100644 mdns/src/message/resource/ns.rs delete mode 100644 mdns/src/message/resource/opt.rs delete mode 100644 mdns/src/message/resource/ptr.rs delete mode 100644 mdns/src/message/resource/soa.rs delete mode 100644 mdns/src/message/resource/srv.rs delete mode 100644 mdns/src/message/resource/txt.rs delete mode 100644 media/.gitignore delete mode 100644 media/CHANGELOG.md delete mode 100644 media/Cargo.toml delete mode 100644 media/LICENSE-APACHE delete mode 100644 media/LICENSE-MIT delete mode 100644 media/README.md delete mode 100644 media/benches/audio_buffer.rs delete mode 100644 media/codecov.yml delete mode 100644 media/doc/webrtc.rs.png delete mode 100644 media/src/audio/buffer.rs delete mode 100644 media/src/audio/buffer/info.rs delete mode 100644 media/src/audio/buffer/layout.rs delete mode 100644 media/src/audio/mod.rs delete mode 100644 media/src/audio/sample.rs delete mode 100644 media/src/error.rs delete mode 100644 media/src/io/h264_reader/h264_reader_test.rs delete mode 100644 media/src/io/h264_reader/mod.rs delete mode 100644 media/src/io/h264_writer/h264_writer_test.rs delete mode 100644 media/src/io/h264_writer/mod.rs delete mode 100644 media/src/io/ivf_reader/ivf_reader_test.rs delete mode 100644 media/src/io/ivf_reader/mod.rs delete mode 100644 media/src/io/ivf_writer/ivf_writer_test.rs delete mode 100644 media/src/io/ivf_writer/mod.rs delete mode 100644 media/src/io/mod.rs delete mode 100644 media/src/io/ogg_reader/mod.rs delete mode 100644 media/src/io/ogg_reader/ogg_reader_test.rs delete mode 100644 media/src/io/ogg_writer/mod.rs delete mode 100644 media/src/io/ogg_writer/ogg_writer_test.rs delete mode 100644 media/src/io/sample_builder/mod.rs delete mode 100644 media/src/io/sample_builder/sample_builder_test.rs delete mode 100644 media/src/io/sample_builder/sample_sequence_location.rs delete mode 100644 media/src/io/sample_builder/sample_sequence_location_test.rs delete mode 100644 media/src/lib.rs delete mode 100644 media/src/video/mod.rs delete mode 100644 rtcp/.gitignore delete mode 100644 rtcp/CHANGELOG.md delete mode 100644 rtcp/Cargo.toml delete mode 100644 rtcp/LICENSE-APACHE delete mode 100644 rtcp/LICENSE-MIT delete mode 100644 rtcp/README.md delete mode 100644 rtcp/codecov.yml delete mode 100644 rtcp/doc/webrtc.rs.png delete mode 100644 rtcp/src/compound_packet/compound_packet_test.rs delete mode 100644 rtcp/src/compound_packet/mod.rs delete mode 100644 rtcp/src/error.rs delete mode 100644 rtcp/src/extended_report/dlrr.rs delete mode 100644 rtcp/src/extended_report/extended_report_test.rs delete mode 100644 rtcp/src/extended_report/mod.rs delete mode 100644 rtcp/src/extended_report/prt.rs delete mode 100644 rtcp/src/extended_report/rle.rs delete mode 100644 rtcp/src/extended_report/rrt.rs delete mode 100644 rtcp/src/extended_report/ssr.rs delete mode 100644 rtcp/src/extended_report/unknown.rs delete mode 100644 rtcp/src/extended_report/vm.rs delete mode 100644 rtcp/src/goodbye/goodbye_test.rs delete mode 100644 rtcp/src/goodbye/mod.rs delete mode 100644 rtcp/src/header.rs delete mode 100644 rtcp/src/lib.rs delete mode 100644 rtcp/src/packet.rs delete mode 100644 rtcp/src/payload_feedbacks/full_intra_request/full_intra_request_test.rs delete mode 100644 rtcp/src/payload_feedbacks/full_intra_request/mod.rs delete mode 100644 rtcp/src/payload_feedbacks/mod.rs delete mode 100644 rtcp/src/payload_feedbacks/picture_loss_indication/mod.rs delete mode 100644 rtcp/src/payload_feedbacks/picture_loss_indication/picture_loss_indication_test.rs delete mode 100644 rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs delete mode 100644 rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/receiver_estimated_maximum_bitrate_test.rs delete mode 100644 rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs delete mode 100644 rtcp/src/payload_feedbacks/slice_loss_indication/slice_loss_indication_test.rs delete mode 100644 rtcp/src/raw_packet.rs delete mode 100644 rtcp/src/receiver_report/mod.rs delete mode 100644 rtcp/src/receiver_report/receiver_report_test.rs delete mode 100644 rtcp/src/reception_report.rs delete mode 100644 rtcp/src/sender_report/mod.rs delete mode 100644 rtcp/src/sender_report/sender_report_test.rs delete mode 100644 rtcp/src/source_description/mod.rs delete mode 100644 rtcp/src/source_description/source_description_test.rs delete mode 100644 rtcp/src/transport_feedbacks/mod.rs delete mode 100644 rtcp/src/transport_feedbacks/rapid_resynchronization_request/mod.rs delete mode 100644 rtcp/src/transport_feedbacks/rapid_resynchronization_request/rapid_resynchronization_request_test.rs delete mode 100644 rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs delete mode 100644 rtcp/src/transport_feedbacks/transport_layer_cc/transport_layer_cc_test.rs delete mode 100644 rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs delete mode 100644 rtcp/src/transport_feedbacks/transport_layer_nack/transport_layer_nack_test.rs delete mode 100644 rtcp/src/util.rs delete mode 100644 rtp/.gitignore delete mode 100644 rtp/CHANGELOG.md delete mode 100644 rtp/Cargo.toml delete mode 100644 rtp/LICENSE-APACHE delete mode 100644 rtp/LICENSE-MIT delete mode 100644 rtp/README.md delete mode 100644 rtp/benches/packet_bench.rs delete mode 100644 rtp/codecov.yml delete mode 100644 rtp/doc/webrtc.rs.png delete mode 100644 rtp/src/codecs/av1/av1_test.rs delete mode 100644 rtp/src/codecs/av1/leb128.rs delete mode 100644 rtp/src/codecs/av1/mod.rs delete mode 100644 rtp/src/codecs/av1/obu.rs delete mode 100644 rtp/src/codecs/av1/packetizer.rs delete mode 100644 rtp/src/codecs/g7xx/g7xx_test.rs delete mode 100644 rtp/src/codecs/g7xx/mod.rs delete mode 100644 rtp/src/codecs/h264/h264_test.rs delete mode 100644 rtp/src/codecs/h264/mod.rs delete mode 100644 rtp/src/codecs/h265/h265_test.rs delete mode 100644 rtp/src/codecs/h265/mod.rs delete mode 100644 rtp/src/codecs/mod.rs delete mode 100644 rtp/src/codecs/opus/mod.rs delete mode 100644 rtp/src/codecs/opus/opus_test.rs delete mode 100644 rtp/src/codecs/vp8/mod.rs delete mode 100644 rtp/src/codecs/vp8/vp8_test.rs delete mode 100644 rtp/src/codecs/vp9/mod.rs delete mode 100644 rtp/src/codecs/vp9/vp9_test.rs delete mode 100644 rtp/src/error.rs delete mode 100644 rtp/src/extension/abs_send_time_extension/abs_send_time_extension_test.rs delete mode 100644 rtp/src/extension/abs_send_time_extension/mod.rs delete mode 100644 rtp/src/extension/audio_level_extension/audio_level_extension_test.rs delete mode 100644 rtp/src/extension/audio_level_extension/mod.rs delete mode 100644 rtp/src/extension/mod.rs delete mode 100644 rtp/src/extension/playout_delay_extension/mod.rs delete mode 100644 rtp/src/extension/playout_delay_extension/playout_delay_extension_test.rs delete mode 100644 rtp/src/extension/transport_cc_extension/mod.rs delete mode 100644 rtp/src/extension/transport_cc_extension/transport_cc_extension_test.rs delete mode 100644 rtp/src/extension/video_orientation_extension/mod.rs delete mode 100644 rtp/src/extension/video_orientation_extension/video_orientation_extension_test.rs delete mode 100644 rtp/src/header.rs delete mode 100644 rtp/src/lib.rs delete mode 100644 rtp/src/packet/mod.rs delete mode 100644 rtp/src/packet/packet_test.rs delete mode 100644 rtp/src/packetizer/mod.rs delete mode 100644 rtp/src/packetizer/packetizer_test.rs delete mode 100644 rtp/src/sequence.rs delete mode 100644 sctp/.gitignore delete mode 100644 sctp/CHANGELOG.md delete mode 100644 sctp/Cargo.toml delete mode 100644 sctp/LICENSE-APACHE delete mode 100644 sctp/LICENSE-MIT delete mode 100644 sctp/README.md delete mode 100644 sctp/codecov.yml delete mode 100644 sctp/doc/webrtc.rs.png delete mode 100644 sctp/examples/ping.rs delete mode 100644 sctp/examples/pong.rs delete mode 100644 sctp/examples/throughput.rs delete mode 100644 sctp/fuzz/.gitignore delete mode 100644 sctp/fuzz/Cargo.toml delete mode 100644 sctp/fuzz/artifacts/packet/crash-16cad30042bc4791bd62c630a780add5d1220779 delete mode 100644 sctp/fuzz/artifacts/packet/crash-8b9b318a6b66ea23232a4e2aec91deeeca470af8 delete mode 100644 sctp/fuzz/artifacts/packet/crash-8d90dfc8fc34fa06f161f69617ee8f48dec434cd delete mode 100644 sctp/fuzz/artifacts/packet/crash-b836a20af7f8af85423dbe80565465b16bb7a16f delete mode 100644 sctp/fuzz/artifacts/packet/crash-f940d9879efc88872145955bae11ca6ad6a4c044 delete mode 100644 sctp/fuzz/artifacts/param/crash-216833e417069f431d0617fb4e9f8abe6c9a6c1d delete mode 100644 sctp/fuzz/artifacts/param/crash-fb1b644bc0d365ce2dc3c3ff77cd3c4cd8da528d delete mode 100644 sctp/fuzz/fuzz_targets/packet.rs delete mode 100644 sctp/fuzz/fuzz_targets/param.rs delete mode 100644 sctp/src/association/association_internal.rs delete mode 100644 sctp/src/association/association_internal/association_internal_test.rs delete mode 100644 sctp/src/association/association_stats.rs delete mode 100644 sctp/src/association/association_test.rs delete mode 100644 sctp/src/association/mod.rs delete mode 100644 sctp/src/chunk/chunk_abort.rs delete mode 100644 sctp/src/chunk/chunk_cookie_ack.rs delete mode 100644 sctp/src/chunk/chunk_cookie_echo.rs delete mode 100644 sctp/src/chunk/chunk_error.rs delete mode 100644 sctp/src/chunk/chunk_forward_tsn.rs delete mode 100644 sctp/src/chunk/chunk_header.rs delete mode 100644 sctp/src/chunk/chunk_heartbeat.rs delete mode 100644 sctp/src/chunk/chunk_heartbeat_ack.rs delete mode 100644 sctp/src/chunk/chunk_init.rs delete mode 100644 sctp/src/chunk/chunk_payload_data.rs delete mode 100644 sctp/src/chunk/chunk_reconfig.rs delete mode 100644 sctp/src/chunk/chunk_selective_ack.rs delete mode 100644 sctp/src/chunk/chunk_shutdown.rs delete mode 100644 sctp/src/chunk/chunk_shutdown_ack.rs delete mode 100644 sctp/src/chunk/chunk_shutdown_complete.rs delete mode 100644 sctp/src/chunk/chunk_test.rs delete mode 100644 sctp/src/chunk/chunk_type.rs delete mode 100644 sctp/src/chunk/chunk_unknown.rs delete mode 100644 sctp/src/chunk/mod.rs delete mode 100644 sctp/src/error.rs delete mode 100644 sctp/src/error_cause.rs delete mode 100644 sctp/src/fuzz_artifact_test.rs delete mode 100644 sctp/src/lib.rs delete mode 100644 sctp/src/packet.rs delete mode 100644 sctp/src/param/mod.rs delete mode 100644 sctp/src/param/param_chunk_list.rs delete mode 100644 sctp/src/param/param_forward_tsn_supported.rs delete mode 100644 sctp/src/param/param_header.rs delete mode 100644 sctp/src/param/param_heartbeat_info.rs delete mode 100644 sctp/src/param/param_outgoing_reset_request.rs delete mode 100644 sctp/src/param/param_random.rs delete mode 100644 sctp/src/param/param_reconfig_response.rs delete mode 100644 sctp/src/param/param_requested_hmac_algorithm.rs delete mode 100644 sctp/src/param/param_state_cookie.rs delete mode 100644 sctp/src/param/param_supported_extensions.rs delete mode 100644 sctp/src/param/param_test.rs delete mode 100644 sctp/src/param/param_type.rs delete mode 100644 sctp/src/param/param_unknown.rs delete mode 100644 sctp/src/param/param_unrecognized.rs delete mode 100644 sctp/src/queue/control_queue.rs delete mode 100644 sctp/src/queue/mod.rs delete mode 100644 sctp/src/queue/payload_queue.rs delete mode 100644 sctp/src/queue/pending_queue.rs delete mode 100644 sctp/src/queue/queue_test.rs delete mode 100644 sctp/src/queue/reassembly_queue.rs delete mode 100644 sctp/src/stream/mod.rs delete mode 100644 sctp/src/stream/stream_test.rs delete mode 100644 sctp/src/timer/ack_timer.rs delete mode 100644 sctp/src/timer/mod.rs delete mode 100644 sctp/src/timer/rtx_timer.rs delete mode 100644 sctp/src/timer/timer_test.rs delete mode 100644 sctp/src/util.rs delete mode 100644 sdp/.gitignore delete mode 100644 sdp/CHANGELOG.md delete mode 100644 sdp/Cargo.toml delete mode 100644 sdp/LICENSE-APACHE delete mode 100644 sdp/LICENSE-MIT delete mode 100644 sdp/README.md delete mode 100644 sdp/benches/bench.rs delete mode 100644 sdp/codecov.yml delete mode 100644 sdp/doc/webrtc.rs.png delete mode 100644 sdp/fuzz/.gitignore delete mode 100644 sdp/fuzz/Cargo.toml delete mode 100644 sdp/fuzz/fuzz_targets/parse_session.rs delete mode 100644 sdp/src/description/common.rs delete mode 100644 sdp/src/description/description_test.rs delete mode 100644 sdp/src/description/media.rs delete mode 100644 sdp/src/description/mod.rs delete mode 100644 sdp/src/description/session.rs delete mode 100644 sdp/src/direction/direction_test.rs delete mode 100644 sdp/src/direction/mod.rs delete mode 100644 sdp/src/error.rs delete mode 100644 sdp/src/extmap/extmap_test.rs delete mode 100644 sdp/src/extmap/mod.rs delete mode 100644 sdp/src/lexer/mod.rs delete mode 100644 sdp/src/lib.rs delete mode 100644 sdp/src/util/mod.rs delete mode 100644 sdp/src/util/util_test.rs create mode 100644 src/allocation/allocation_manager.rs create mode 100644 src/allocation/channel_bind.rs create mode 100644 src/allocation/mod.rs create mode 100644 src/allocation/permission.rs create mode 100644 src/attr.rs create mode 100644 src/chandata.rs create mode 100644 src/con/mod.rs create mode 100644 src/con/tcp.rs create mode 100644 src/lib.rs create mode 100644 src/relay.rs create mode 100644 src/server/config.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/request.rs delete mode 100644 srtp/.gitignore delete mode 100644 srtp/CHANGELOG.md delete mode 100644 srtp/Cargo.toml delete mode 100644 srtp/LICENSE-APACHE delete mode 100644 srtp/LICENSE-MIT delete mode 100644 srtp/README.md delete mode 100644 srtp/benches/srtp_bench.rs delete mode 100644 srtp/codecov.yml delete mode 100644 srtp/doc/webrtc.rs.png delete mode 100644 srtp/src/cipher/cipher_aead_aes_gcm.rs delete mode 100644 srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs delete mode 100644 srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs delete mode 100644 srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs delete mode 100644 srtp/src/cipher/mod.rs delete mode 100644 srtp/src/config.rs delete mode 100644 srtp/src/context/context_test.rs delete mode 100644 srtp/src/context/mod.rs delete mode 100644 srtp/src/context/srtcp.rs delete mode 100644 srtp/src/context/srtcp_test.rs delete mode 100644 srtp/src/context/srtp.rs delete mode 100644 srtp/src/context/srtp_test.rs delete mode 100644 srtp/src/error.rs delete mode 100644 srtp/src/key_derivation.rs delete mode 100644 srtp/src/lib.rs delete mode 100644 srtp/src/option.rs delete mode 100644 srtp/src/protection_profile.rs delete mode 100644 srtp/src/session/mod.rs delete mode 100644 srtp/src/session/session_rtcp_test.rs delete mode 100644 srtp/src/session/session_rtp_test.rs delete mode 100644 srtp/src/stream.rs delete mode 100644 stun/.gitignore delete mode 100644 stun/CHANGELOG.md delete mode 100644 stun/Cargo.toml delete mode 100644 stun/LICENSE-APACHE delete mode 100644 stun/LICENSE-MIT delete mode 100644 stun/README.md delete mode 100644 stun/benches/bench.rs delete mode 100644 stun/codecov.yml delete mode 100644 stun/doc/webrtc.rs.png delete mode 100644 stun/examples/stun_client.rs delete mode 100644 stun/examples/stun_decode.rs delete mode 100644 stun/src/addr.rs delete mode 100644 stun/src/addr/addr_test.rs delete mode 100644 stun/src/agent.rs delete mode 100644 stun/src/agent/agent_test.rs delete mode 100644 stun/src/attributes.rs delete mode 100644 stun/src/attributes/attributes_test.rs delete mode 100644 stun/src/checks.rs delete mode 100644 stun/src/client.rs delete mode 100644 stun/src/client/client_test.rs delete mode 100644 stun/src/error.rs delete mode 100644 stun/src/error_code.rs delete mode 100644 stun/src/fingerprint.rs delete mode 100644 stun/src/fingerprint/fingerprint_test.rs delete mode 100644 stun/src/integrity.rs delete mode 100644 stun/src/integrity/integrity_test.rs delete mode 100644 stun/src/lib.rs delete mode 100644 stun/src/message.rs delete mode 100644 stun/src/message/message_test.rs delete mode 100644 stun/src/textattrs.rs delete mode 100644 stun/src/textattrs/textattrs_test.rs delete mode 100644 stun/src/uattrs.rs delete mode 100644 stun/src/uattrs/uattrs_test.rs delete mode 100644 stun/src/uri.rs delete mode 100644 stun/src/uri/uri_test.rs delete mode 100644 stun/src/xoraddr.rs delete mode 100644 stun/src/xoraddr/xoraddr_test.rs delete mode 100644 turn/.gitignore delete mode 100644 turn/Cargo.toml delete mode 100644 turn/LICENSE-APACHE delete mode 100644 turn/LICENSE-MIT delete mode 100644 turn/README.md delete mode 100644 turn/benches/bench.rs delete mode 100644 turn/codecov.yml delete mode 100644 turn/doc/webrtc.rs.png delete mode 100644 turn/examples/turn_client_udp.rs delete mode 100644 turn/examples/turn_server_udp.rs delete mode 100644 turn/src/allocation/allocation_manager.rs delete mode 100644 turn/src/allocation/allocation_manager/allocation_manager_test.rs delete mode 100644 turn/src/allocation/allocation_test.rs delete mode 100644 turn/src/allocation/channel_bind.rs delete mode 100644 turn/src/allocation/channel_bind/channel_bind_test.rs delete mode 100644 turn/src/allocation/five_tuple.rs delete mode 100644 turn/src/allocation/five_tuple/five_tuple_test.rs delete mode 100644 turn/src/allocation/mod.rs delete mode 100644 turn/src/allocation/permission.rs delete mode 100644 turn/src/auth/auth_test.rs delete mode 100644 turn/src/auth/mod.rs delete mode 100644 turn/src/client/binding.rs delete mode 100644 turn/src/client/binding/binding_test.rs delete mode 100644 turn/src/client/client_test.rs delete mode 100644 turn/src/client/mod.rs delete mode 100644 turn/src/client/periodic_timer.rs delete mode 100644 turn/src/client/periodic_timer/periodic_timer_test.rs delete mode 100644 turn/src/client/permission.rs delete mode 100644 turn/src/client/relay_conn.rs delete mode 100644 turn/src/client/relay_conn/relay_conn_test.rs delete mode 100644 turn/src/client/transaction.rs delete mode 100644 turn/src/error.rs delete mode 100644 turn/src/lib.rs delete mode 100644 turn/src/proto/addr.rs delete mode 100644 turn/src/proto/addr/addr_test.rs delete mode 100644 turn/src/proto/chandata.rs delete mode 100644 turn/src/proto/chandata/chandata_test.rs delete mode 100644 turn/src/proto/channum.rs delete mode 100644 turn/src/proto/channum/channnum_test.rs delete mode 100644 turn/src/proto/data.rs delete mode 100644 turn/src/proto/data/data_test.rs delete mode 100644 turn/src/proto/dontfrag.rs delete mode 100644 turn/src/proto/dontfrag/dontfrag_test.rs delete mode 100644 turn/src/proto/evenport.rs delete mode 100644 turn/src/proto/evenport/evenport_test.rs delete mode 100644 turn/src/proto/lifetime.rs delete mode 100644 turn/src/proto/lifetime/lifetime_test.rs delete mode 100644 turn/src/proto/mod.rs delete mode 100644 turn/src/proto/peeraddr.rs delete mode 100644 turn/src/proto/peeraddr/peeraddr_test.rs delete mode 100644 turn/src/proto/proto_test.rs delete mode 100644 turn/src/proto/relayaddr.rs delete mode 100644 turn/src/proto/relayaddr/relayaddr_test.rs delete mode 100644 turn/src/proto/reqfamily.rs delete mode 100644 turn/src/proto/reqfamily/reqfamily_test.rs delete mode 100644 turn/src/proto/reqtrans.rs delete mode 100644 turn/src/proto/reqtrans/reqtrans_test.rs delete mode 100644 turn/src/proto/rsrvtoken.rs delete mode 100644 turn/src/proto/rsrvtoken/rsrvtoken_test.rs delete mode 100644 turn/src/relay/mod.rs delete mode 100644 turn/src/relay/relay_none.rs delete mode 100644 turn/src/relay/relay_range.rs delete mode 100644 turn/src/relay/relay_static.rs delete mode 100644 turn/src/server/config.rs delete mode 100644 turn/src/server/mod.rs delete mode 100644 turn/src/server/request.rs delete mode 100644 turn/src/server/request/request_test.rs delete mode 100644 turn/src/server/server_test.rs delete mode 100644 util/.gitignore delete mode 100644 util/CHANGELOG.md delete mode 100644 util/Cargo.toml delete mode 100644 util/LICENSE-APACHE delete mode 100644 util/LICENSE-MIT delete mode 100644 util/README.md delete mode 100644 util/benches/bench.rs delete mode 100644 util/codecov.yml delete mode 100644 util/doc/webrtc.rs.png delete mode 100644 util/examples/display-interfaces.rs delete mode 100644 util/src/buffer/buffer_test.rs delete mode 100644 util/src/buffer/mod.rs delete mode 100644 util/src/conn/conn_bridge.rs delete mode 100644 util/src/conn/conn_bridge_test.rs delete mode 100644 util/src/conn/conn_disconnected_packet.rs delete mode 100644 util/src/conn/conn_pipe.rs delete mode 100644 util/src/conn/conn_pipe_test.rs delete mode 100644 util/src/conn/conn_test.rs delete mode 100644 util/src/conn/conn_udp.rs delete mode 100644 util/src/conn/conn_udp_listener.rs delete mode 100644 util/src/conn/conn_udp_listener_test.rs delete mode 100644 util/src/conn/mod.rs delete mode 100644 util/src/error.rs delete mode 100644 util/src/fixed_big_int/fixed_big_int_test.rs delete mode 100644 util/src/fixed_big_int/mod.rs delete mode 100644 util/src/ifaces/ffi/mod.rs delete mode 100644 util/src/ifaces/ffi/unix/mod.rs delete mode 100644 util/src/ifaces/ffi/windows/mod.rs delete mode 100644 util/src/ifaces/mod.rs delete mode 100644 util/src/lib.rs delete mode 100644 util/src/marshal/exact_size_buf.rs delete mode 100644 util/src/marshal/mod.rs delete mode 100644 util/src/replay_detector/mod.rs delete mode 100644 util/src/replay_detector/replay_detector_test.rs delete mode 100644 util/src/sync/mod.rs delete mode 100644 util/src/vnet/chunk.rs delete mode 100644 util/src/vnet/chunk/chunk_test.rs delete mode 100644 util/src/vnet/chunk_queue.rs delete mode 100644 util/src/vnet/chunk_queue/chunk_queue_test.rs delete mode 100644 util/src/vnet/conn.rs delete mode 100644 util/src/vnet/conn/conn_test.rs delete mode 100644 util/src/vnet/conn_map.rs delete mode 100644 util/src/vnet/conn_map/conn_map_test.rs delete mode 100644 util/src/vnet/interface.rs delete mode 100644 util/src/vnet/mod.rs delete mode 100644 util/src/vnet/nat.rs delete mode 100644 util/src/vnet/nat/nat_test.rs delete mode 100644 util/src/vnet/net.rs delete mode 100644 util/src/vnet/net/net_test.rs delete mode 100644 util/src/vnet/resolver.rs delete mode 100644 util/src/vnet/resolver/resolver_test.rs delete mode 100644 util/src/vnet/router.rs delete mode 100644 util/src/vnet/router/router_test.rs delete mode 100644 webrtc/CHANGELOG.md delete mode 100644 webrtc/Cargo.toml delete mode 100644 webrtc/src/api/api_test.rs delete mode 100644 webrtc/src/api/interceptor_registry/interceptor_registry_test.rs delete mode 100644 webrtc/src/api/interceptor_registry/mod.rs delete mode 100644 webrtc/src/api/media_engine/media_engine_test.rs delete mode 100644 webrtc/src/api/media_engine/mod.rs delete mode 100644 webrtc/src/api/mod.rs delete mode 100644 webrtc/src/api/setting_engine/mod.rs delete mode 100644 webrtc/src/api/setting_engine/setting_engine_test.rs delete mode 100644 webrtc/src/data_channel/data_channel_init.rs delete mode 100644 webrtc/src/data_channel/data_channel_message.rs delete mode 100644 webrtc/src/data_channel/data_channel_parameters.rs delete mode 100644 webrtc/src/data_channel/data_channel_state.rs delete mode 100644 webrtc/src/data_channel/data_channel_test.rs delete mode 100644 webrtc/src/data_channel/mod.rs delete mode 100644 webrtc/src/dtls_transport/dtls_fingerprint.rs delete mode 100644 webrtc/src/dtls_transport/dtls_parameters.rs delete mode 100644 webrtc/src/dtls_transport/dtls_role.rs delete mode 100644 webrtc/src/dtls_transport/dtls_transport_state.rs delete mode 100644 webrtc/src/dtls_transport/dtls_transport_test.rs delete mode 100644 webrtc/src/dtls_transport/mod.rs delete mode 100644 webrtc/src/error.rs delete mode 100644 webrtc/src/ice_transport/ice_candidate.rs delete mode 100644 webrtc/src/ice_transport/ice_candidate_pair.rs delete mode 100644 webrtc/src/ice_transport/ice_candidate_type.rs delete mode 100644 webrtc/src/ice_transport/ice_connection_state.rs delete mode 100644 webrtc/src/ice_transport/ice_credential_type.rs delete mode 100644 webrtc/src/ice_transport/ice_gatherer.rs delete mode 100644 webrtc/src/ice_transport/ice_gatherer_state.rs delete mode 100644 webrtc/src/ice_transport/ice_gathering_state.rs delete mode 100644 webrtc/src/ice_transport/ice_parameters.rs delete mode 100644 webrtc/src/ice_transport/ice_protocol.rs delete mode 100644 webrtc/src/ice_transport/ice_role.rs delete mode 100644 webrtc/src/ice_transport/ice_server.rs delete mode 100644 webrtc/src/ice_transport/ice_transport_state.rs delete mode 100644 webrtc/src/ice_transport/ice_transport_test.rs delete mode 100644 webrtc/src/ice_transport/mod.rs delete mode 100644 webrtc/src/lib.rs delete mode 100644 webrtc/src/mux/endpoint.rs delete mode 100644 webrtc/src/mux/mod.rs delete mode 100644 webrtc/src/mux/mux_func.rs delete mode 100644 webrtc/src/mux/mux_test.rs delete mode 100644 webrtc/src/peer_connection/certificate.rs delete mode 100644 webrtc/src/peer_connection/configuration.rs delete mode 100644 webrtc/src/peer_connection/mod.rs delete mode 100644 webrtc/src/peer_connection/offer_answer_options.rs delete mode 100644 webrtc/src/peer_connection/operation/mod.rs delete mode 100644 webrtc/src/peer_connection/operation/operation_test.rs delete mode 100644 webrtc/src/peer_connection/peer_connection_internal.rs delete mode 100644 webrtc/src/peer_connection/peer_connection_state.rs delete mode 100644 webrtc/src/peer_connection/peer_connection_test.rs delete mode 100644 webrtc/src/peer_connection/policy/bundle_policy.rs delete mode 100644 webrtc/src/peer_connection/policy/ice_transport_policy.rs delete mode 100644 webrtc/src/peer_connection/policy/mod.rs delete mode 100644 webrtc/src/peer_connection/policy/rtcp_mux_policy.rs delete mode 100644 webrtc/src/peer_connection/policy/sdp_semantics.rs delete mode 100644 webrtc/src/peer_connection/sdp/mod.rs delete mode 100644 webrtc/src/peer_connection/sdp/sdp_test.rs delete mode 100644 webrtc/src/peer_connection/sdp/sdp_type.rs delete mode 100644 webrtc/src/peer_connection/sdp/session_description.rs delete mode 100644 webrtc/src/peer_connection/signaling_state.rs delete mode 100644 webrtc/src/rtp_transceiver/fmtp/generic/generic_test.rs delete mode 100644 webrtc/src/rtp_transceiver/fmtp/generic/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/fmtp/h264/h264_test.rs delete mode 100644 webrtc/src/rtp_transceiver/fmtp/h264/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/fmtp/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_codec.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_receiver/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_receiver/rtp_receiver_test.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_sender/mod.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_sender/rtp_sender_test.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_transceiver_direction.rs delete mode 100644 webrtc/src/rtp_transceiver/rtp_transceiver_test.rs delete mode 100644 webrtc/src/rtp_transceiver/srtp_writer_future.rs delete mode 100644 webrtc/src/sctp_transport/mod.rs delete mode 100644 webrtc/src/sctp_transport/sctp_transport_capabilities.rs delete mode 100644 webrtc/src/sctp_transport/sctp_transport_state.rs delete mode 100644 webrtc/src/sctp_transport/sctp_transport_test.rs delete mode 100644 webrtc/src/stats/mod.rs delete mode 100644 webrtc/src/stats/serialize.rs delete mode 100644 webrtc/src/stats/stats_collector.rs delete mode 100644 webrtc/src/track/mod.rs delete mode 100644 webrtc/src/track/track_local/mod.rs delete mode 100644 webrtc/src/track/track_local/track_local_static_rtp.rs delete mode 100644 webrtc/src/track/track_local/track_local_static_sample.rs delete mode 100644 webrtc/src/track/track_local/track_local_static_test.rs delete mode 100644 webrtc/src/track/track_remote/mod.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0e43501f7..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: webrtc-rs # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: WebRTCrs # Replace with a single Patreon username -open_collective: webrtc-rs # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml deleted file mode 100644 index 94b11b067..000000000 --- a/.github/actions-rs/grcov.yml +++ /dev/null @@ -1,15 +0,0 @@ -branch: true -ignore-not-existing: true -llvm: true -filter: covered -output-type: lcov -output-path: ./lcov.info -source-dir: . -ignore: - - "/*" - - "C:/*" - - "../*" -excl-line: "#\\[derive\\(" -excl-start: "mod tests \\{" -excl-br-line: "#\\[derive\\(" -excl-br-start: "mod tests \\{" \ No newline at end of file diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml deleted file mode 100644 index 9ece39a19..000000000 --- a/.github/workflows/cargo.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: cargo - -on: - push: - branches: [master] - pull_request: - branches: [master] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - test: - name: Test - strategy: - matrix: - os: ["ubuntu-latest", "macos-latest"] - toolchain: - # - 1.65.0 # min supported version (https://github.com/webrtc-rs/webrtc/#toolchain) - - stable - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - name: Install Rust ${{ matrix.toolchain }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - override: true - - name: Install Rust - run: rustup update stable - - name: 📦 Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-registry- - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - profile: minimal - override: true - - name: 🏭 Cache dependencies - uses: Swatinem/rust-cache@v2 - - name: Test - run: cargo test - - name: Test with all features enabled - run: cargo test --all-features - - test_windows: - name: Test (windows) - strategy: - matrix: - toolchain: - # - 1.63.0 # min supported version (https://github.com/webrtc-rs/webrtc/#toolchain) - - stable - runs-on: windows-latest - steps: - - uses: actions/checkout@v3 - - name: Install Rust ${{ matrix.toolchain }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - override: true - - name: Install Rust - run: rustup update stable - - name: 📦 Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-registry- - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - profile: minimal - override: true - - name: Copy to C drive - run: cp D:\a C:\ -Recurse - # - name: 🏭 Cache dependencies - # uses: Swatinem/rust-cache@v2 - - name: Test - working-directory: "C:\\a\\webrtc\\webrtc" - run: cargo test --features metrics - - quality: - name: Check formatting style and run clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: clippy, rustfmt - override: true - - name: 📦 Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-registry- - - name: 📎 Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets --all-features --all -- -D warnings - - name: 💬 Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - # - name: Check for typos - # uses: crate-ci/typos@master diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f9bc41971 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CACHE: ${{ (github.event_name == 'push' + || github.event_name == 'pull_request') + && github.ref != 'refs/heads/main' + && !contains(github.event.head_commit.message, '[fresh ci]') }} + RUST_BACKTRACE: 1 + RUST_VER: "1.79" + +jobs: + + ################ + # Pull Request # + ################ + + pr: + if: ${{ github.event_name == 'pull_request' }} + needs: + - clippy + - rustdoc + - rustfmt + - test-unit + runs-on: ubuntu-latest + steps: + - run: true + + + + + ########################## + # Linting and formatting # + ########################## + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VER }} + components: clippy + - uses: Swatinem/rust-cache@v2 + if: ${{ env.CACHE == 'true' }} + + - run: make cargo.lint + + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: nightly + components: rustfmt + + - run: make cargo.fmt check=yes + + + + + ########### + # Testing # + ########### + + test-unit: + name: test (unit, ${{ matrix.toolchain }}) + strategy: + fail-fast: false + matrix: + toolchain: ["stable", "nightly"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ (matrix.toolchain == 'stable' && env.RUST_VER) + || matrix.toolchain }} + components: rust-src + - uses: Swatinem/rust-cache@v2 + if: ${{ env.CACHE == 'true' }} + + - run: cargo install cargo-careful + if: ${{ matrix.toolchain == 'nightly' }} + + - run: make test.unit + careful=${{ (matrix.toolchain == 'nightly' && 'yes') + || 'no' }} + + + + + ############ + # Building # + ############ + + rustdoc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VER }} + - uses: Swatinem/rust-cache@v2 + if: ${{ env.CACHE == 'true' }} + + - run: make cargo.doc private=yes open=no + env: + RUSTFLAGS: -D warnings diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml deleted file mode 100644 index 1d4f25884..000000000 --- a/.github/workflows/grcov.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: coverage - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - grcov: - name: Coverage - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - toolchain: - - nightly - cargo_flags: - - "--all-features" - steps: - - name: Checkout source code - uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.toolchain }} - override: true - - - name: Install grcov - uses: actions-rs/install@v0.1 - with: - crate: grcov - version: latest - use-tool-cache: true - - - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all --no-fail-fast ${{ matrix.cargo_flags }} - env: - CARGO_INCREMENTAL: "0" - RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' - RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' - - - name: Generate coverage data - id: grcov - # uses: actions-rs/grcov@v0.1 - run: | - grcov target/debug/ \ - --branch \ - --llvm \ - --source-dir . \ - --output-path lcov.info \ - --ignore='/**' \ - --ignore='C:/**' \ - --ignore='../**' \ - --ignore-not-existing \ - --excl-line "#\\[derive\\(" \ - --excl-br-line "#\\[derive\\(" \ - --excl-start "#\\[cfg\\(test\\)\\]" \ - --excl-br-start "#\\[cfg\\(test\\)\\]" \ - --commit-sha ${{ github.sha }} \ - --service-job-id ${{ github.job }} \ - --service-name "GitHub Actions" \ - --service-number ${{ github.run_id }} - - name: Upload coverage as artifact - uses: actions/upload-artifact@v2 - with: - name: lcov.info - # path: ${{ steps.grcov.outputs.report }} - path: lcov.info - - - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v1 - with: - # file: ${{ steps.grcov.outputs.report }} - file: lcov.info - fail_ci_if_error: true diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb..000000000 diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..0f9c48ca3 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,16 @@ +# See full list at: +# https://github.com/rust-lang-nursery/rustfmt/blob/master/Configurations.md + +max_width = 80 +format_strings = false +imports_granularity = "Crate" +normalize_comments = true +wrap_comments = true + +reorder_impl_items = true +use_try_shorthand = true + +error_on_line_overflow = true +error_on_unformatted = true + +unstable_features = true diff --git a/turn/CHANGELOG.md b/CHANGELOG.md similarity index 100% rename from turn/CHANGELOG.md rename to CHANGELOG.md diff --git a/Cargo.lock b/Cargo.lock index 5fe1f90e5..d07ba7dc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,2996 +17,648 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" - -[[package]] -name = "anyhow" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "asn1-rs" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-channel" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" -dependencies = [ - "concurrent-queue", - "event-listener 5.3.0", - "event-listener-strategy 0.5.1", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" -dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", - "pin-project-lite", -] - [[package]] name = "async-stream" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-task" -version = "4.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" - -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" - -[[package]] -name = "backtrace" -version = "0.3.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" -dependencies = [ - "async-channel", - "async-lock", - "async-task", - "fastrand", - "futures-io", - "futures-lite", - "piper", - "tracing", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" - -[[package]] -name = "ccm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" -dependencies = [ - "aead", - "cipher", - "ctr", - "subtle", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.52.5", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap" -version = "4.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" -dependencies = [ - "anstyle", - "clap_lex 0.7.0", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "clap_lex" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" - -[[package]] -name = "concurrent-queue" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap 4.5.4", - "criterion-plot", - "futures", - "is-terminal", - "itertools", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "fiat-crypto", - "platforms", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "data-encoding" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" -dependencies = [ - "event-listener 5.3.0", - "pin-project-lite", -] - -[[package]] -name = "examples" -version = "0.5.0" -dependencies = [ - "anyhow", - "bytes", - "chrono", - "clap 3.2.25", - "env_logger", - "hyper", - "lazy_static", - "log", - "memchr", - "rand", - "serde", - "serde_json", - "signal", - "tokio", - "tokio-util", - "webrtc", -] - -[[package]] -name = "fastrand" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" - -[[package]] -name = "ff" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.2.6", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" -dependencies = [ - "cfg-if", - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hub" -version = "0.1.0" -dependencies = [ - "rcgen", - "rustls", - "rustls-pemfile", - "thiserror", - "tokio", - "webrtc-dtls", - "webrtc-util", -] - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown 0.14.3", - "serde", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "interceptor" -version = "0.12.0" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "log", - "portable-atomic", - "rand", - "rtcp", - "rtp", - "thiserror", - "tokio", - "tokio-test", - "waitgroup", - "webrtc-srtp", - "webrtc-util", -] - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "is-terminal" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "nearly_eq" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a629868a433328c35d654e1e1fb4648a68a042e3c71de4e507a9bcf4602c5635" - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", - "pin-utils", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "oid-registry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" -dependencies = [ - "asn1-rs", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" -dependencies = [ - "bitflags 2.5.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-src" -version = "300.2.3+3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-float" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "pem" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" -dependencies = [ - "base64 0.22.0", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - -[[package]] -name = "plotters" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" - -[[package]] -name = "plotters-svg" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "polling" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.3.9", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro2" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rcgen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "x509-parser", - "yasna", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "regex" -version = "1.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rtcp" -version = "0.11.0" -dependencies = [ - "bytes", - "thiserror", - "webrtc-util", -] - -[[package]] -name = "rtp" -version = "0.11.0" -dependencies = [ - "bytes", - "chrono", - "criterion", - "memchr", - "portable-atomic", - "rand", - "serde", - "thiserror", - "webrtc-util", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "0.38.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" -dependencies = [ - "bitflags 2.5.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.0", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" - -[[package]] -name = "rustls-webpki" -version = "0.102.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sdp" -version = "0.6.2" -dependencies = [ - "criterion", - "rand", - "substring", - "thiserror", - "url", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" - -[[package]] -name = "serde" -version = "1.0.198" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" -dependencies = [ - "serde_derive", + "async-stream-impl", + "futures-core", + "pin-project-lite", ] [[package]] -name = "serde_derive" -version = "1.0.198" +name = "async-stream-impl" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] -name = "serde_json" -version = "1.0.116" +name = "async-trait" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "indexmap 2.2.6", - "itoa", - "ryu", - "serde", + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] -name = "sha1" -version = "0.10.6" +name = "autocfg" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] -name = "sha2" -version = "0.10.8" +name = "backtrace" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ + "addr2line", + "cc", "cfg-if", - "cpufeatures", - "digest", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", ] [[package]] -name = "signal" -version = "0.1.0" +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "anyhow", - "base64 0.21.7", - "hyper", - "lazy_static", - "tokio", + "generic-array", ] [[package]] -name = "signal-hook-registry" -version = "1.4.1" +name = "bytecodec" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" dependencies = [ - "libc", + "byteorder", + "trackable 0.2.24", ] [[package]] -name = "signature" -version = "2.2.0" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "slab" -version = "0.4.9" +name = "bytes" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] -name = "smallvec" -version = "1.13.2" +name = "cc" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" [[package]] -name = "smol_str" -version = "0.2.1" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" -dependencies = [ - "serde", -] +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "socket2" -version = "0.5.6" +name = "cpufeatures" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", - "windows-sys 0.52.0", ] [[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" +name = "crc" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ - "base64ct", - "der", + "crc-catalog", ] [[package]] -name = "strsim" -version = "0.10.0" +name = "crc-catalog" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "stun" -version = "0.6.0" -dependencies = [ - "base64 0.21.7", - "clap 3.2.25", - "crc", - "criterion", - "lazy_static", - "md-5", - "rand", - "ring", - "subtle", - "thiserror", - "tokio", - "tokio-test", - "url", - "webrtc-util", -] +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] -name = "substring" -version = "1.4.5" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "autocfg", + "generic-array", + "typenum", ] [[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "2.0.60" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "synstructure" -version = "0.13.1" +name = "futures" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "futures-channel" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ - "winapi-util", + "futures-core", + "futures-sink", ] [[package]] -name = "textwrap" -version = "0.16.1" +name = "futures-core" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] -name = "thiserror" -version = "1.0.58" +name = "futures-executor" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ - "thiserror-impl", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "thiserror-impl" -version = "1.0.58" +name = "futures-io" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] -name = "time" -version = "0.3.36" +name = "futures-macro" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] -name = "time-core" -version = "0.1.2" +name = "futures-sink" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] -name = "time-macros" -version = "0.2.18" +name = "futures-task" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "futures-util" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "serde", - "serde_json", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "tinyvec_macros", + "typenum", + "version_check", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.37.0" +name = "getrandom" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "backtrace", - "bytes", + "cfg-if", "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", + "wasi", ] [[package]] -name = "tokio-macros" -version = "2.2.0" +name = "gimli" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] -name = "tokio-stream" -version = "0.1.15" +name = "hermit-abi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "tokio-test" -version = "0.4.4" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "tokio-util" -version = "0.7.10" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", + "digest", ] [[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" +name = "libc" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-core", -] +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] -name = "tracing-core" -version = "0.1.32" +name = "log" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", -] +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "try-lock" -version = "0.2.5" +name = "md5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] -name = "turn" +name = "medea-turn" version = "0.8.0" dependencies = [ "async-trait", - "base64 0.21.7", - "chrono", - "clap 3.2.25", - "criterion", - "env_logger", + "bytecodec", + "bytes", "futures", "hex", "log", - "md-5", - "portable-atomic", "rand", - "ring", - "stun", + "stun_codec", "thiserror", "tokio", "tokio-test", "tokio-util", - "webrtc-util", ] [[package]] -name = "typenum" -version = "1.17.0" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "unicode-bidi" -version = "0.3.15" +name = "miniz_oxide" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] [[package]] -name = "unicode-ident" -version = "1.0.12" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] [[package]] -name = "unicode-normalization" -version = "0.1.23" +name = "num_cpus" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "tinyvec", + "hermit-abi", + "libc", ] [[package]] -name = "universal-hash" -version = "0.5.1" +name = "object" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ - "crypto-common", - "subtle", + "memchr", ] [[package]] -name = "untrusted" -version = "0.9.0" +name = "pin-project-lite" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] -name = "url" -version = "2.5.0" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "uuid" -version = "1.8.0" +name = "ppv-lite86" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" -dependencies = [ - "getrandom", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "proc-macro2" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] [[package]] -name = "version_check" -version = "0.9.4" +name = "quote" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] [[package]] -name = "waitgroup" -version = "0.1.2" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "atomic-waker", + "libc", + "rand_chacha", + "rand_core", ] [[package]] -name = "walkdir" -version = "2.5.0" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "same-file", - "winapi-util", + "ppv-lite86", + "rand_core", ] [[package]] -name = "want" -version = "0.3.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "try-lock", + "getrandom", ] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "wasm-bindgen-macro", + "cpufeatures", + "digest", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "autocfg", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" +name = "socket2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" +name = "stun_codec" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "web-sys" -version = "0.3.69" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "js-sys", - "wasm-bindgen", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "webrtc" -version = "0.11.0" +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "cfg-if", - "env_logger", - "hex", - "interceptor", - "lazy_static", - "log", - "pem", - "portable-atomic", - "rand", - "rcgen", - "regex", - "ring", - "rtcp", - "rtp", - "rustls", - "sdp", - "serde", - "serde_json", - "sha2", - "smol_str", - "stun", - "thiserror", - "time", - "tokio", - "tokio-test", - "turn", - "url", - "waitgroup", - "webrtc-data", - "webrtc-dtls", - "webrtc-ice", - "webrtc-mdns", - "webrtc-media", - "webrtc-sctp", - "webrtc-srtp", - "webrtc-util", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "webrtc-constraints" -version = "0.1.0" +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "env_logger", - "indexmap 2.2.6", - "lazy_static", - "ordered-float", - "serde", - "serde_json", - "thiserror", + "thiserror-impl", ] [[package]] -name = "webrtc-data" -version = "0.9.0" +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "bytes", - "chrono", - "env_logger", - "log", - "portable-atomic", - "thiserror", - "tokio", - "tokio-test", - "webrtc-sctp", - "webrtc-util", + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] -name = "webrtc-dtls" -version = "0.10.0" +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ - "aes", - "aes-gcm", - "async-trait", - "bincode", - "byteorder", - "cbc", - "ccm", - "chrono", - "clap 3.2.25", - "der-parser", - "env_logger", - "hkdf", - "hmac", - "hub", - "log", - "p256", - "p384", - "pem", - "portable-atomic", - "rand", - "rand_core", - "rcgen", - "ring", - "rustls", - "sec1", - "serde", - "sha1", - "sha2", - "subtle", - "thiserror", - "tokio", - "tokio-test", - "webrtc-util", - "x25519-dalek", - "x509-parser", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", ] [[package]] -name = "webrtc-ice" -version = "0.11.0" +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ - "arc-swap", - "async-trait", - "chrono", - "clap 3.2.25", - "crc", - "env_logger", - "hyper", - "ipnet", - "lazy_static", - "log", - "portable-atomic", - "rand", - "regex", - "serde", - "serde_json", - "sha1", - "stun", - "thiserror", - "tokio", - "tokio-test", - "turn", - "url", - "uuid", - "waitgroup", - "webrtc-mdns", - "webrtc-util", + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] -name = "webrtc-mdns" -version = "0.7.0" +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ - "chrono", - "clap 3.2.25", - "env_logger", - "log", - "socket2", - "thiserror", + "futures-core", + "pin-project-lite", "tokio", - "webrtc-util", ] [[package]] -name = "webrtc-media" -version = "0.8.0" +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ - "byteorder", + "async-stream", "bytes", - "criterion", - "nearly_eq", - "rand", - "rtp", - "thiserror", + "futures-core", + "tokio", + "tokio-stream", ] [[package]] -name = "webrtc-sctp" -version = "0.10.0" +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ - "arc-swap", - "async-trait", "bytes", - "chrono", - "clap 3.2.25", - "crc", - "env_logger", - "lazy_static", - "log", - "portable-atomic", - "rand", - "thiserror", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", - "tokio-test", - "webrtc-util", ] [[package]] -name = "webrtc-srtp" -version = "0.13.0" +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" dependencies = [ - "aead", - "aes", - "aes-gcm", - "byteorder", - "bytes", - "criterion", - "ctr", - "hmac", - "lazy_static", - "log", - "openssl", - "rtcp", - "rtp", - "sha1", - "subtle", - "thiserror", - "tokio", - "tokio-test", - "webrtc-util", + "trackable 1.3.0", + "trackable_derive", ] [[package]] -name = "webrtc-util" -version = "0.9.0" +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" dependencies = [ - "async-global-executor", - "async-trait", - "bitflags 1.3.2", - "bytes", - "chrono", - "criterion", - "env_logger", - "ipnet", - "lazy_static", - "libc", - "log", - "nix", - "portable-atomic", - "rand", - "thiserror", - "tokio", - "tokio-test", - "winapi", + "trackable_derive", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "trackable_derive" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "quote", + "syn 1.0.109", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "winapi-util" -version = "0.1.6" +name = "unicode-ident" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "windows-core" -version = "0.52.0" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.5", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" @@ -3146,62 +798,3 @@ name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core", - "serde", - "zeroize", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "ring", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 371f7d08c..5fcf210ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,33 @@ -[workspace] -members = [ - "constraints", - "data", - "dtls", - "examples", - "ice", - "interceptor", - "mdns", - "media", - "rtcp", - "rtp", - "sctp", - "sdp", - "srtp", - "stun", - "turn", - "util", - "webrtc", -] -resolver = "2" +[package] +name = "medea-turn" +version = "0.8.0" +authors = ["Rain Liu ", "Instrumentisto Team "] +edition = "2021" +rust-version = "1.70" +description = "A pure Rust implementation of TURN" +license = "MIT OR Apache-2.0" +homepage = "https://github.com/instrumentisto/medea-turn-rs" +repository = "https://github.com/instrumentisto/medea-turn-rs" +publish = false -[profile.dev] -opt-level = 0 +[dependencies] +async-trait = "0.1" +bytecodec = "0.4" +bytes = "1.6" +futures = "0.3" +log = "0.4" +rand = "0.8" +stun_codec = "0.3" +thiserror = "1" +tokio = { version = "1.32.0", default-features = false, features = [ + "io-util", + "macros", + "net", + "rt-multi-thread", + "time", +] } +tokio-util = { version = "0.7", features = ["codec"] } + +[dev-dependencies] +tokio-test = "0.4" +hex = "0.4" diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..30df759cb --- /dev/null +++ b/Makefile @@ -0,0 +1,108 @@ +############################### +# Common defaults/definitions # +############################### + +comma := , + +# Checks two given strings for equality. +eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ + $(findstring $(2),$(1))),1) + + + + +########### +# Aliases # +########### + +all: fmt lint test.unit + + +docs: cargo.doc + + +fmt: cargo.fmt + + +lint: cargo.lint + + +test: test.unit + + + + +################## +# Cargo commands # +################## + +cargo-crate = $(if $(call eq,$(crate),),--workspace,-p $(crate)) + + +# Generate crates documentation from Rust sources. +# +# Usage: +# make cargo.doc [crate=] [private=(yes|no)] +# [open=(yes|no)] [clean=(no|yes)] + +cargo.doc: +ifeq ($(clean),yes) + @rm -rf target/doc/ +endif + cargo doc $(cargo-crate) --all-features \ + $(if $(call eq,$(private),no),,--document-private-items) \ + $(if $(call eq,$(open),no),,--open) + + +# Format Rust sources with rustfmt. +# +# Usage: +# make cargo.fmt [check=(no|yes)] + +cargo.fmt: + cargo +nightly fmt --all $(if $(call eq,$(check),yes),-- --check,) + + +# Lint Rust sources with Clippy. +# +# Usage: +# make cargo.lint [crate=] + +cargo.lint: + cargo clippy $(cargo-crate) --all-features -- -D warnings + + + + +#################### +# Testing commands # +#################### + + +# Run project unit tests. +# +# Usage: +# make test.unit [crate=] [careful=(no|yes)] + +test.unit: +ifeq ($(careful),yes) +ifeq ($(shell cargo install --list | grep cargo-careful),) + cargo install cargo-careful +endif +ifeq ($(shell rustup component list --toolchain=nightly \ + | grep 'rust-src (installed)'),) + rustup component add --toolchain=nightly rust-src +endif +endif + cargo $(if $(call eq,$(careful),yes),+nightly careful,) test --all-features + + + + +################## +# .PHONY section # +################## + +.PHONY: all docs mt lint test \ + cargo.doc cargo.fmt cargo.lint \ + test.unit diff --git a/README.md b/README.md index 90ec4957b..6ef759760 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,18 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - - - Twitter - -

-

- A pure Rust implementation of WebRTC stack. Rewrite Pion WebRTC stack in Rust -

+medea-turn-rs +==== -

-Sponsored with 💖 by
-

- -

-Silver Sponsors:
- -Stream Chat -
- -ChannelTalk -
-Bronze Sponsors:
- -KittyCAD -
-AdrianEddy
-

+[![CI](https://github.com/instrumentisto/medea-turn-rs/workflows/CI/badge.svg?branch=main "CI")](https://github.com/instrumentisto/medea-turn-rs/actions?query=workflow%3ACI+branch%3Amain) +[![Rust 1.70+](https://img.shields.io/badge/rustc-1.70+-lightgray.svg "Rust 1.70+")](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html) -
-Table of Content -- [Overview](#overview) -- [Features](#features) -- [Building](#building) - - [Toolchain](#toolchain) - - [Monorepo Setup](#monorepo-setup) -- [Open Source License](#open-source-license) -- [Contributing](#contributing) +[Changelog](https://github.com/instrumentisto/medea-turn-rs/blob/master/CHANGELOG.md) -
+A pure Rust implementation of TURN. Majorly refactored fork of +[webrtc-rs/turn](https://github.com/webrtc-rs/webrtc/tree/f9734b79b0b87b15f157780ef3c26480cebd5d84/turn). -## Overview -WebRTC.rs is a pure Rust implementation of WebRTC stack, which rewrites Pion stack in Rust. -This project is still in active and early development stage, please refer to the [Roadmap](https://github.com/webrtc-rs/webrtc/issues/1) to track the major milestones and releases. -[Examples](https://github.com/webrtc-rs/webrtc/blob/master/examples/examples/README.md) provide code samples to show how to use webrtc-rs to build media and data channel applications. -## Features -

- WebRTC -
- Media - Interceptor - Data -
- RTP - RTCP - SRTP - SCTP -
- DTLS -
- mDNS - STUN - TURN - ICE -
- SDP - Util -

-

- WebRTC Crates Dependency Graph -

-

- WebRTC Stack -

+## License -## Building - -### Toolchain - -**Minimum Supported Rust Version:** `1.65.0` - -Our minimum supported rust version(MSRV) policy is to support versions of the compiler released within the last six months. We don't eagerly bump the minimum version we support, instead the minimum will be bumped on a needed by needed basis, usually because downstream dependencies force us to. - -**Note:** Changes to the minimum supported version are not consider breaking from a [semver](https://semver.org/) perspective. - -### Monorepo Setup - -All webrtc dependent crates and examples are included in this repository at the top level in a Cargo workspace. - -To build all webrtc examples: - -```shell -cd examples -cargo test # build all examples (maybe very slow) -#[ or just build single example (much faster) -cargo build --example play-from-disk-vpx # build play-from-disk-vpx example only -cargo build --example play-from-disk-h264 # build play-from-disk-h264 example only -#... -#] -``` - -To build webrtc crate: - -```shell -cargo build [or clippy or test or fmt] -``` - -## Open Source License - -Dual licensing under both MIT and Apache-2.0 is the currently accepted standard by the Rust language community and has been used for both the compiler and many public libraries since (see ). In order to match the community standards, webrtc-rs is using the dual MIT+Apache-2.0 license. - -## Contributing - -Contributors or Pull Requests are Welcome!!! +Dual licensing under both MIT and Apache-2.0 is the currently accepted standard by the Rust language community and has been used for both the compiler and many public libraries since (see https://doc.rust-lang.org/1.6.0/complement-project-faq.html#why-dual-mitasl2-license). In order to match the community standards, webrtc-rs is using the dual MIT+Apache-2.0 license. diff --git a/_typos.toml b/_typos.toml deleted file mode 100644 index 5c7a08773..000000000 --- a/_typos.toml +++ /dev/null @@ -1,15 +0,0 @@ -[type.po] -extend-glob = ["*.csr"] -check-file = false - -[default.extend-words] -# Additionals is important for WebRTC -additionals = "additionals" -# STAP-A for WebRTC -stap = "stap" -# MIS value -mis = "mis" -# datas is used a lot for plural. -datas = "datas" -# 2nd for second -2nd = "2nd" diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 99256353c..000000000 --- a/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 9c7a93c8-b2b2-4da3-9990-7283701dec58 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/constraints/.gitignore b/constraints/.gitignore deleted file mode 100644 index 926fe98a1..000000000 --- a/constraints/.gitignore +++ /dev/null @@ -1,88 +0,0 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/rust -# Edit at https://www.toptal.com/developers/gitignore?templates=rust - -### Rust ### -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# End of https://www.toptal.com/developers/gitignore/api/rust - -# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode - -### VisualStudioCode ### -.vscode/* -# !.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# Support for Project snippet scope -.vscode/*.code-snippets - -# Ignore code-workspaces -*.code-workspace - -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode - -# Created by https://www.toptal.com/developers/gitignore/api/macos -# Edit at https://www.toptal.com/developers/gitignore?templates=macos - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -# End of https://www.toptal.com/developers/gitignore/api/macos diff --git a/constraints/CHANGELOG.md b/constraints/CHANGELOG.md deleted file mode 100644 index c2cce53a4..000000000 --- a/constraints/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# webrtc-constraints changelog - -## Unreleased - -## v0.1.0 - -Initial release. diff --git a/constraints/Cargo.toml b/constraints/Cargo.toml deleted file mode 100644 index 0eed74597..000000000 --- a/constraints/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "webrtc-constraints" -version = "0.1.0" -authors = ["Vincent Esche "] -edition = "2021" -description = "A pure Rust implementation of WebRTC Media Constraints API" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-constraints" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/constraints" - -[dependencies] -indexmap = "2" -serde = { version = "1", features = ["derive"], optional = true } -ordered-float = { version = "4", default-features = false } -thiserror = "1" - -[dev-dependencies] -env_logger = "0.10" -lazy_static = "1" -serde_json = { version = "1", features = ["preserve_order"] } - -[features] -default = ["serde"] -serde = ["dep:serde", "indexmap/serde"] - -[[example]] -name = "json" -path = "examples/json.rs" -required-features = ["serde"] diff --git a/constraints/LICENSE-APACHE b/constraints/LICENSE-APACHE deleted file mode 100644 index b2e847a43..000000000 --- a/constraints/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/constraints/LICENSE-MIT b/constraints/LICENSE-MIT deleted file mode 100644 index 5a980079b..000000000 --- a/constraints/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Vincent Esche - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/constraints/README.md b/constraints/README.md deleted file mode 100644 index 249f66e9f..000000000 --- a/constraints/README.md +++ /dev/null @@ -1,32 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of the SelectSettings algorithm from the WebRTC/W3C "Media Capture and Streams" spec. - - (Last synced with the spec against commit 8cea879 from 2023/01/09.) -

diff --git a/constraints/examples/json.rs b/constraints/examples/json.rs deleted file mode 100644 index 878cbb142..000000000 --- a/constraints/examples/json.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::iter::FromIterator; - -use webrtc_constraints::algorithms::{ - select_settings_candidates, ClosestToIdealPolicy, DeviceInformationExposureMode, - TieBreakingPolicy, -}; -use webrtc_constraints::property::all::name::*; -use webrtc_constraints::{ - MediaTrackConstraints, MediaTrackSettings, MediaTrackSupportedConstraints, -}; - -fn main() { - let supported_constraints = - MediaTrackSupportedConstraints::from_iter(vec![&DEVICE_ID, &HEIGHT, &WIDTH, &RESIZE_MODE]); - - // Deserialize possible settings from JSON: - let possible_settings: Vec = { - let json = serde_json::json!([ - { "deviceId": "480p", "width": 720, "height": 480, "resizeMode": "crop-and-scale" }, - { "deviceId": "720p", "width": 1280, "height": 720, "resizeMode": "crop-and-scale" }, - { "deviceId": "1080p", "width": 1920, "height": 1080, "resizeMode": "none" }, - { "deviceId": "1440p", "width": 2560, "height": 1440, "resizeMode": "none" }, - { "deviceId": "2160p", "width": 3840, "height": 2160, "resizeMode": "none" }, - ]); - serde_json::from_value(json).unwrap() - }; - - // Deserialize constraints from JSON: - let constraints: MediaTrackConstraints = { - let json = serde_json::json!({ - "width": { - "max": 2560, - }, - "height": { - "max": 1440, - }, - // Unsupported constraint, which should thus get ignored: - "frameRate": { - "exact": 30.0 - }, - // Ideal resize-mode: - "resizeMode": "none", - "advanced": [ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - { "height": 800 }, - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - { "resizeMode": "none" }, - ] - }); - serde_json::from_value(json).unwrap() - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.into_sanitized(&supported_constraints); - - let candidates = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Protected, - ) - .unwrap(); - - // Specify a tie-breaking policy - // - // A couple of basic policies are provided batteries-included, - // but for more sophisticated needs you can implement your own `TieBreakingPolicy`: - let tie_breaking_policy = - ClosestToIdealPolicy::new(possible_settings[2].clone(), &supported_constraints); - - let actual = tie_breaking_policy.select_candidate(candidates); - - let expected = &possible_settings[2]; - - assert_eq!(actual, expected); -} diff --git a/constraints/examples/macros.rs b/constraints/examples/macros.rs deleted file mode 100644 index 0cf9def33..000000000 --- a/constraints/examples/macros.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::iter::FromIterator; - -use webrtc_constraints::algorithms::{ - select_settings_candidates, ClosestToIdealPolicy, DeviceInformationExposureMode, - TieBreakingPolicy, -}; -use webrtc_constraints::macros::*; -use webrtc_constraints::property::all::name::*; -use webrtc_constraints::{settings, MediaTrackSupportedConstraints, ResizeMode}; - -fn main() { - let supported_constraints = - MediaTrackSupportedConstraints::from_iter(vec![&DEVICE_ID, &HEIGHT, &WIDTH, &RESIZE_MODE]); - - let possible_settings = vec![ - settings![ - &DEVICE_ID => "480p", - &HEIGHT => 480, - &WIDTH => 720, - &RESIZE_MODE => ResizeMode::crop_and_scale(), - ], - settings![ - &DEVICE_ID => "720p", - &HEIGHT => 720, - &WIDTH => 1280, - &RESIZE_MODE => ResizeMode::crop_and_scale(), - ], - settings![ - &DEVICE_ID => "1080p", - &HEIGHT => 1080, - &WIDTH => 1920, - &RESIZE_MODE => ResizeMode::none(), - ], - settings![ - &DEVICE_ID => "1440p", - &HEIGHT => 1440, - &WIDTH => 2560, - &RESIZE_MODE => ResizeMode::none(), - ], - settings![ - &DEVICE_ID => "2160p", - &HEIGHT => 2160, - &WIDTH => 3840, - &RESIZE_MODE => ResizeMode::none(), - ], - ]; - - let constraints = constraints! { - mandatory: { - &WIDTH => value_range_constraint!{ - max: 2560 - }, - &HEIGHT => value_range_constraint!{ - max: 1440 - }, - // Unsupported constraint, which should thus get ignored: - &FRAME_RATE => value_range_constraint!{ - exact: 30.0 - }, - }, - advanced: [ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - { - &HEIGHT => value_range_constraint!{ - exact: 800 - } - }, - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - { - &RESIZE_MODE => value_constraint!{ - exact: ResizeMode::none() - } - }, - ] - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.to_sanitized(&supported_constraints); - - let candidates = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - // Specify a tie-breaking policy - // - // A couple of basic policies are provided batteries-included, - // but for more sophisticated needs you can implement your own `TieBreakingPolicy`: - let tie_breaking_policy = - ClosestToIdealPolicy::new(possible_settings[2].clone(), &supported_constraints); - - let actual = tie_breaking_policy.select_candidate(candidates); - - let expected = &possible_settings[2]; - - assert_eq!(actual, expected); -} diff --git a/constraints/examples/native.rs b/constraints/examples/native.rs deleted file mode 100644 index 6324ade18..000000000 --- a/constraints/examples/native.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::iter::FromIterator; - -use webrtc_constraints::algorithms::{ - select_settings_candidates, ClosestToIdealPolicy, DeviceInformationExposureMode, - TieBreakingPolicy, -}; -use webrtc_constraints::property::all::name::*; -use webrtc_constraints::{ - AdvancedMediaTrackConstraints, MandatoryMediaTrackConstraints, MediaTrackConstraintSet, - MediaTrackConstraints, MediaTrackSettings, MediaTrackSupportedConstraints, ResizeMode, - ResolvedValueConstraint, ResolvedValueRangeConstraint, ValueConstraint, ValueRangeConstraint, -}; - -fn main() { - let supported_constraints = - MediaTrackSupportedConstraints::from_iter(vec![&DEVICE_ID, &HEIGHT, &WIDTH, &RESIZE_MODE]); - - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&HEIGHT, 480.into()), - (&WIDTH, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&HEIGHT, 720.into()), - (&WIDTH, 1280.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&HEIGHT, 1080.into()), - (&WIDTH, 1920.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&HEIGHT, 1440.into()), - (&WIDTH, 2560.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&HEIGHT, 2160.into()), - (&WIDTH, 3840.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - ]; - - let constraints = MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - ( - &WIDTH, - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint::default().max(2560)) - .into(), - ), - ( - &HEIGHT, - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint::default().max(1440)) - .into(), - ), - // Unsupported constraint, which should thus get ignored: - ( - &FRAME_RATE, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().exact(30.0), - ) - .into(), - ), - ]), - advanced: AdvancedMediaTrackConstraints::from_iter([ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - MediaTrackConstraintSet::from_iter([( - &HEIGHT, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().exact(800), - ) - .into(), - )]), - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - MediaTrackConstraintSet::from_iter([( - &RESIZE_MODE, - ValueConstraint::Constraint( - ResolvedValueConstraint::default().exact(ResizeMode::none()), - ) - .into(), - )]), - ]), - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.to_sanitized(&supported_constraints); - - let candidates = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - // Specify a tie-breaking policy - // - // A couple of basic policies are provided batteries-included, - // but for more sophisticated needs you can implement your own `TieBreakingPolicy`: - let tie_breaking_policy = - ClosestToIdealPolicy::new(possible_settings[2].clone(), &supported_constraints); - - let actual = tie_breaking_policy.select_candidate(candidates); - - let expected = &possible_settings[2]; - - assert_eq!(actual, expected); -} diff --git a/constraints/src/algorithms.rs b/constraints/src/algorithms.rs deleted file mode 100644 index 46d1f4043..000000000 --- a/constraints/src/algorithms.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Algorithms as defined in the ["Media Capture and Streams"][mediacapture_streams] spec. -//! -//! [mediacapture_streams]: https://www.w3.org/TR/mediacapture-streams/ - -mod fitness_distance; -mod select_settings; - -pub use self::fitness_distance::*; -pub use self::select_settings::*; diff --git a/constraints/src/algorithms/fitness_distance.rs b/constraints/src/algorithms/fitness_distance.rs deleted file mode 100644 index 973fb2e8a..000000000 --- a/constraints/src/algorithms/fitness_distance.rs +++ /dev/null @@ -1,132 +0,0 @@ -/// The function used to compute the "fitness distance" of a [setting][media_track_settings] value of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// The trait corresponds to the ["fitness distance"][fitness_distance] function in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_settings]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksettings -/// [fitness_distance]: https://www.w3.org/TR/mediacapture-streams/#dfn-fitness-distance -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -pub trait FitnessDistance { - /// The type returned in the event of a computation error. - type Error; - - /// Computes the fitness distance of the given `subject` in the range of `0.0..=1.0`. - /// - /// A distance of `0.0` denotes it maximally fit, one of `1.0` as maximally unfit. - fn fitness_distance(&self, subject: Subject) -> Result; -} - -mod empty_constraint; -mod setting; -mod settings; -mod value_constraint; -mod value_range_constraint; -mod value_sequence_constraint; - -use std::cmp::Ordering; - -pub use self::setting::{SettingFitnessDistanceError, SettingFitnessDistanceErrorKind}; -pub use self::settings::SettingsFitnessDistanceError; - -fn nearly_cmp(lhs: f64, rhs: f64) -> Ordering { - // Based on: https://stackoverflow.com/a/32334103/227536 - - let epsilon: f64 = 128.0 * f64::EPSILON; - let abs_th: f64 = f64::MIN; - - debug_assert!(epsilon < 1.0); - - if lhs == rhs { - return Ordering::Equal; - } - - let diff = (lhs - rhs).abs(); - let norm = (lhs.abs() + rhs.abs()).min(f64::MAX); - - if diff < (epsilon * norm).max(abs_th) { - Ordering::Equal - } else if lhs < rhs { - Ordering::Less - } else { - Ordering::Greater - } -} - -fn is_nearly_greater_than_or_equal_to(actual: f64, min: f64) -> bool { - nearly_cmp(actual, min) != Ordering::Less -} - -fn is_nearly_less_than_or_equal_to(actual: f64, max: f64) -> bool { - nearly_cmp(actual, max) != Ordering::Greater -} - -fn is_nearly_equal_to(actual: f64, exact: f64) -> bool { - nearly_cmp(actual, exact) == Ordering::Equal -} - -fn relative_fitness_distance(actual: f64, ideal: f64) -> f64 { - // As specified in step 7 of the `fitness distance` algorithm: - // - // - // > For all positive numeric constraints […], - // > the fitness distance is the result of the formula - // > - // > ``` - // > (actual == ideal) ? 0 : |actual - ideal| / max(|actual|, |ideal|) - // > ``` - if (actual - ideal).abs() < f64::EPSILON { - 0.0 - } else { - let numerator = (actual - ideal).abs(); - let denominator = actual.abs().max(ideal.abs()); - if denominator.abs() < f64::EPSILON { - // Avoid division by zero crashes: - 0.0 - } else { - numerator / denominator - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod relative_fitness_distance { - #[test] - fn zero_distance() { - // Make sure we're not dividing by zero: - assert_eq!(super::relative_fitness_distance(0.0, 0.0), 0.0); - - assert_eq!(super::relative_fitness_distance(0.5, 0.5), 0.0); - assert_eq!(super::relative_fitness_distance(1.0, 1.0), 0.0); - assert_eq!(super::relative_fitness_distance(2.0, 2.0), 0.0); - } - - #[test] - fn fract_distance() { - assert_eq!(super::relative_fitness_distance(1.0, 2.0), 0.5); - assert_eq!(super::relative_fitness_distance(2.0, 1.0), 0.5); - - assert_eq!(super::relative_fitness_distance(0.5, 1.0), 0.5); - assert_eq!(super::relative_fitness_distance(1.0, 0.5), 0.5); - - assert_eq!(super::relative_fitness_distance(0.25, 0.5), 0.5); - assert_eq!(super::relative_fitness_distance(0.5, 0.25), 0.5); - } - - #[test] - fn one_distance() { - assert_eq!(super::relative_fitness_distance(0.0, 0.5), 1.0); - assert_eq!(super::relative_fitness_distance(0.5, 0.0), 1.0); - - assert_eq!(super::relative_fitness_distance(0.0, 1.0), 1.0); - assert_eq!(super::relative_fitness_distance(1.0, 0.0), 1.0); - - assert_eq!(super::relative_fitness_distance(0.0, 2.0), 1.0); - assert_eq!(super::relative_fitness_distance(2.0, 0.0), 1.0); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/empty_constraint.rs b/constraints/src/algorithms/fitness_distance/empty_constraint.rs deleted file mode 100644 index f27329a9e..000000000 --- a/constraints/src/algorithms/fitness_distance/empty_constraint.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::setting::SettingFitnessDistanceError; -use super::FitnessDistance; -use crate::constraint::EmptyConstraint; - -impl<'a, T> FitnessDistance> for EmptyConstraint { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, _setting: Option<&'a T>) -> Result { - // As specified in step 1 of the `SelectSettings` algorithm: - // - // - // > If an empty list has been given as the value for a constraint, - // > it MUST be interpreted as if the constraint were not specified - // > (in other words, an empty constraint == no constraint). - Ok(0.0) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Constraint = EmptyConstraint; - - macro_rules! test_empty_constraint { - ( - settings: $t:ty => $s:expr, - expected: $e:expr $(,)? - ) => { - let settings: &[Option<$t>] = $s; - let constraint = &Constraint {}; - for setting in settings { - let actual = constraint.fitness_distance(setting.as_ref()); - - assert_eq!(actual, $e); - } - }; - } - - #[test] - fn bool() { - test_empty_constraint!( - settings: bool => &[None, Some(false)], - expected: Ok(0.0) - ); - } - - #[test] - fn string() { - test_empty_constraint!( - settings: String => &[None, Some("foo".to_owned())], - expected: Ok(0.0) - ); - } - - #[test] - fn i64() { - test_empty_constraint!( - settings: i64 => &[None, Some(42)], - expected: Ok(0.0) - ); - } - - #[test] - fn f64() { - test_empty_constraint!( - settings: f64 => &[None, Some(42.0)], - expected: Ok(0.0) - ); - } -} diff --git a/constraints/src/algorithms/fitness_distance/setting.rs b/constraints/src/algorithms/fitness_distance/setting.rs deleted file mode 100644 index 440a65b1c..000000000 --- a/constraints/src/algorithms/fitness_distance/setting.rs +++ /dev/null @@ -1,532 +0,0 @@ -use super::FitnessDistance; -use crate::{MediaTrackSetting, ResolvedMediaTrackConstraint}; - -/// An error indicating a rejected fitness distance computation, -/// likely caused by a mismatched yet required constraint. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct SettingFitnessDistanceError { - /// The kind of the error (e.g. missing value, mismatching value, …). - pub kind: SettingFitnessDistanceErrorKind, - /// The required constraint value. - pub constraint: String, - /// The offending setting value. - pub setting: Option, -} - -/// The kind of the error (e.g. missing value, mismatching value, …). -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum SettingFitnessDistanceErrorKind { - /// Settings value is missing. - Missing, - /// Settings value is a mismatch. - Mismatch, - /// Settings value is too small. - TooSmall, - /// Settings value is too large. - TooLarge, -} - -impl<'a> FitnessDistance> for ResolvedMediaTrackConstraint { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a MediaTrackSetting>) -> Result { - type Setting = MediaTrackSetting; - type Constraint = ResolvedMediaTrackConstraint; - - let setting = match setting { - Some(setting) => setting, - None => { - return if self.is_required() { - Err(Self::Error { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } else { - Ok(1.0) - } - } - }; - - let result = match (self, setting) { - // Empty constraint: - (ResolvedMediaTrackConstraint::Empty(constraint), setting) => { - constraint.fitness_distance(Some(setting)) - } - - // Boolean constraint: - (Constraint::Bool(constraint), Setting::Bool(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::Bool(constraint), Setting::Integer(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::Bool(constraint), Setting::Float(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::Bool(constraint), Setting::String(setting)) => { - constraint.fitness_distance(Some(setting)) - } - - // Integer constraint: - (Constraint::IntegerRange(_constraint), Setting::Bool(_setting)) => Ok(0.0), - (Constraint::IntegerRange(constraint), Setting::Integer(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::IntegerRange(constraint), Setting::Float(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::IntegerRange(_constraint), Setting::String(_setting)) => Ok(0.0), - - // Float constraint: - (Constraint::FloatRange(_constraint), Setting::Bool(_setting)) => Ok(0.0), - (Constraint::FloatRange(constraint), Setting::Integer(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::FloatRange(constraint), Setting::Float(setting)) => { - constraint.fitness_distance(Some(setting)) - } - (Constraint::FloatRange(_constraint), Setting::String(_setting)) => Ok(0.0), - - // String constraint: - (Constraint::String(_constraint), Setting::Bool(_setting)) => Ok(0.0), - (Constraint::String(_constraint), Setting::Integer(_setting)) => Ok(0.0), - (Constraint::String(_constraint), Setting::Float(_setting)) => Ok(0.0), - (Constraint::String(constraint), Setting::String(setting)) => { - constraint.fitness_distance(Some(setting)) - } - - // String sequence constraint: - (Constraint::StringSequence(_constraint), Setting::Bool(_setting)) => Ok(0.0), - (Constraint::StringSequence(_constraint), Setting::Integer(_setting)) => Ok(0.0), - (Constraint::StringSequence(_constraint), Setting::Float(_setting)) => Ok(0.0), - (Constraint::StringSequence(constraint), Setting::String(setting)) => { - constraint.fitness_distance(Some(setting)) - } - }; - - #[cfg(debug_assertions)] - if let Ok(fitness_distance) = result { - debug_assert!({ fitness_distance.is_finite() }); - } - - result - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constraint::EmptyConstraint; - use crate::{MediaTrackSetting, ResolvedMediaTrackConstraint}; - - #[test] - fn empty_constraint() { - // As per step 1 of the `SelectSettings` algorithm from the W3C spec: - // - // - // > Each constraint specifies one or more values (or a range of values) for its property. - // > A property MAY appear more than once in the list of 'advanced' ConstraintSets. - // > If an empty list has been given as the value for a constraint, - // > it MUST be interpreted as if the constraint were not specified - // > (in other words, an empty constraint == no constraint). - let constraint = ResolvedMediaTrackConstraint::Empty(EmptyConstraint {}); - - let settings = [ - MediaTrackSetting::Bool(true), - MediaTrackSetting::Integer(42), - MediaTrackSetting::Float(4.2), - MediaTrackSetting::String("string".to_owned()), - ]; - - let expected = 0.0; - - for setting in settings { - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - assert_eq!(actual, expected); - } - } - - mod bool_constraint { - use super::*; - use crate::ResolvedValueConstraint; - - #[test] - fn bool_setting() { - // As per step 8 of the `fitness distance` function from the W3C spec: - // - // - // > For all string, enum and boolean constraints - // > (e.g. deviceId, groupId, facingMode, resizeMode, echoCancellation), - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - - let scenarios = [(false, false), (false, true), (true, false), (true, true)]; - - for (constraint_value, setting_value) in scenarios { - let constraint = ResolvedMediaTrackConstraint::Bool(ResolvedValueConstraint { - exact: None, - ideal: Some(constraint_value), - }); - - let setting = MediaTrackSetting::Bool(setting_value); - - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - let expected = if constraint_value == setting_value { - 0.0 - } else { - 1.0 - }; - - assert_eq!(actual, expected); - } - } - - #[test] - fn non_bool_settings() { - // As per step 4 of the `fitness distance` function from the W3C spec: - // - // - // > If constraintValue is a boolean, but the constrainable property is not, - // > then the fitness distance is based on whether the settings dictionary's - // > constraintName member exists or not, from the formula: - // > - // > ``` - // > (constraintValue == exists) ? 0 : 1 - // > ``` - - let settings = [ - MediaTrackSetting::Integer(42), - MediaTrackSetting::Float(4.2), - MediaTrackSetting::String("string".to_owned()), - ]; - - let scenarios = [(false, false), (false, true), (true, false), (true, true)]; - - for (constraint_value, setting_value) in scenarios { - let constraint = ResolvedMediaTrackConstraint::Bool(ResolvedValueConstraint { - exact: None, - ideal: Some(constraint_value), - }); - - for setting in settings.iter() { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let setting = if setting_value { Some(setting) } else { None }; - let actual = constraint.fitness_distance(setting).unwrap(); - - let expected = if setting_value { 0.0 } else { 1.0 }; - - assert_eq!(actual, expected); - } - } - } - } - - mod numeric_constraint { - use super::*; - use crate::ResolvedValueRangeConstraint; - - #[test] - fn missing_settings() { - // As per step 5 of the `fitness distance` function from the W3C spec: - // - // - // > If the settings dictionary's constraintName member does not exist, - // > the fitness distance is 1. - - let constraints = [ - ResolvedMediaTrackConstraint::IntegerRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42), - min: None, - max: None, - }), - ResolvedMediaTrackConstraint::FloatRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42.0), - min: None, - max: None, - }), - ]; - - for constraint in constraints { - let actual = constraint.fitness_distance(None).unwrap(); - - let expected = 1.0; - - assert_eq!(actual, expected); - } - } - - #[test] - fn compatible_settings() { - // As per step 7 of the `fitness distance` function from the W3C spec: - // - // - // > For all positive numeric constraints - // > (such as height, width, frameRate, aspectRatio, sampleRate and sampleSize), - // > the fitness distance is the result of the formula - // > - // > ``` - // > (actual == ideal) ? 0 : |actual - ideal| / max(|actual|, |ideal|) - // > ``` - - let settings = [ - MediaTrackSetting::Integer(21), - MediaTrackSetting::Float(21.0), - ]; - - let constraints = [ - ResolvedMediaTrackConstraint::IntegerRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42), - min: None, - max: None, - }), - ResolvedMediaTrackConstraint::FloatRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42.0), - min: None, - max: None, - }), - ]; - - for constraint in constraints { - for setting in settings.iter() { - let actual = constraint.fitness_distance(Some(setting)).unwrap(); - - let expected = 0.5; - - assert_eq!(actual, expected); - } - } - } - - #[test] - fn incompatible_settings() { - // As per step 3 of the `fitness distance` function from the W3C spec: - // - // - // > If the constraint does not apply for this type of object, the fitness distance is 0 - // > (that is, the constraint does not influence the fitness distance). - - let settings = [ - MediaTrackSetting::Bool(true), - MediaTrackSetting::String("string".to_owned()), - ]; - - let constraints = [ - ResolvedMediaTrackConstraint::IntegerRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42), - min: None, - max: None, - }), - ResolvedMediaTrackConstraint::FloatRange(ResolvedValueRangeConstraint { - exact: None, - ideal: Some(42.0), - min: None, - max: None, - }), - ]; - - for constraint in constraints { - for setting in settings.iter() { - let actual = constraint.fitness_distance(Some(setting)).unwrap(); - - let expected = 0.0; - - println!("constraint: {constraint:?}"); - println!("setting: {setting:?}"); - println!("actual: {actual:?}"); - println!("expected: {expected:?}"); - - assert_eq!(actual, expected); - } - } - } - } - - mod string_constraint { - use super::*; - use crate::ResolvedValueConstraint; - - #[test] - fn missing_settings() { - // As per step 5 of the `fitness distance` function from the W3C spec: - // - // - // > If the settings dictionary's constraintName member does not exist, - // > the fitness distance is 1. - - let constraint = ResolvedMediaTrackConstraint::String(ResolvedValueConstraint { - exact: None, - ideal: Some("constraint".to_owned()), - }); - - let actual = constraint.fitness_distance(None).unwrap(); - - let expected = 1.0; - - assert_eq!(actual, expected); - } - - #[test] - fn compatible_settings() { - // As per step 8 of the `fitness distance` function from the W3C spec: - // - // - // > For all string, enum and boolean constraints - // > (e.g. deviceId, groupId, facingMode, resizeMode, echoCancellation), - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - - let constraint = ResolvedMediaTrackConstraint::String(ResolvedValueConstraint { - exact: None, - ideal: Some("constraint".to_owned()), - }); - - let settings = [MediaTrackSetting::String("setting".to_owned())]; - - for setting in settings { - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - let expected = 1.0; - - assert_eq!(actual, expected); - } - } - - #[test] - fn incompatible_settings() { - // As per step 3 of the `fitness distance` function from the W3C spec: - // - // - // > If the constraint does not apply for this type of object, the fitness distance is 0 - // > (that is, the constraint does not influence the fitness distance). - - let constraint = ResolvedMediaTrackConstraint::String(ResolvedValueConstraint { - exact: None, - ideal: Some("string".to_owned()), - }); - - let settings = [ - MediaTrackSetting::Bool(true), - MediaTrackSetting::Integer(42), - MediaTrackSetting::Float(4.2), - ]; - - for setting in settings { - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - let expected = 0.0; - - println!("constraint: {constraint:?}"); - println!("setting: {setting:?}"); - println!("actual: {actual:?}"); - println!("expected: {expected:?}"); - - assert_eq!(actual, expected); - } - } - } - - mod string_sequence_constraint { - use super::*; - use crate::ResolvedValueSequenceConstraint; - - #[test] - fn missing_settings() { - // As per step 5 of the `fitness distance` function from the W3C spec: - // - // - // > If the settings dictionary's constraintName member does not exist, - // > the fitness distance is 1. - - let constraint = - ResolvedMediaTrackConstraint::StringSequence(ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec!["constraint".to_owned()]), - }); - - let actual = constraint.fitness_distance(None).unwrap(); - - let expected = 1.0; - - assert_eq!(actual, expected); - } - - #[test] - fn compatible_settings() { - // As per step 8 of the `fitness distance` function from the W3C spec: - // - // - // > For all string, enum and boolean constraints - // > (e.g. deviceId, groupId, facingMode, resizeMode, echoCancellation), - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - // - // As well as the preliminary definition: - // - // > For string valued constraints, we define "==" below to be true if one of the - // > values in the sequence is exactly the same as the value being compared against. - - let constraint = - ResolvedMediaTrackConstraint::StringSequence(ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec!["constraint".to_owned()]), - }); - - let settings = [MediaTrackSetting::String("setting".to_owned())]; - - for setting in settings { - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - let expected = 1.0; - - assert_eq!(actual, expected); - } - } - - #[test] - fn incompatible_settings() { - // As per step 3 of the `fitness distance` function from the W3C spec: - // - // - // > If the constraint does not apply for this type of object, the fitness distance is 0 - // > (that is, the constraint does not influence the fitness distance). - - let constraint = - ResolvedMediaTrackConstraint::StringSequence(ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec!["constraint".to_owned()]), - }); - - let settings = [ - MediaTrackSetting::Bool(true), - MediaTrackSetting::Integer(42), - MediaTrackSetting::Float(4.2), - ]; - - for setting in settings { - let actual = constraint.fitness_distance(Some(&setting)).unwrap(); - - let expected = 0.0; - - assert_eq!(actual, expected); - } - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/settings.rs b/constraints/src/algorithms/fitness_distance/settings.rs deleted file mode 100644 index d37f7c3ec..000000000 --- a/constraints/src/algorithms/fitness_distance/settings.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::collections::HashMap; - -use super::setting::SettingFitnessDistanceError; -use super::FitnessDistance; -use crate::{MediaTrackProperty, MediaTrackSettings, SanitizedMediaTrackConstraintSet}; - -/// A list of media track properties and their corresponding fitness distance errors. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SettingsFitnessDistanceError { - /// Setting errors per media track property. - pub setting_errors: HashMap, -} - -impl<'a> FitnessDistance<&'a MediaTrackSettings> for SanitizedMediaTrackConstraintSet { - type Error = SettingsFitnessDistanceError; - - fn fitness_distance(&self, settings: &'a MediaTrackSettings) -> Result { - let results: HashMap = self - .iter() - .map(|(property, constraint)| { - let setting = settings.get(property); - let result = constraint.fitness_distance(setting); - (property.clone(), result) - }) - .collect(); - - let mut total_fitness_distance = 0.0; - - let mut setting_errors: HashMap = - Default::default(); - - for (property, result) in results.into_iter() { - match result { - Ok(fitness_distance) => total_fitness_distance += fitness_distance, - Err(error) => { - setting_errors.insert(property, error); - } - } - } - - if setting_errors.is_empty() { - Ok(total_fitness_distance) - } else { - Err(SettingsFitnessDistanceError { setting_errors }) - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_constraint.rs b/constraints/src/algorithms/fitness_distance/value_constraint.rs deleted file mode 100644 index 3ed6755d9..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint.rs +++ /dev/null @@ -1,218 +0,0 @@ -use super::setting::SettingFitnessDistanceError; -use super::{FitnessDistance, SettingFitnessDistanceErrorKind}; -use crate::constraint::ResolvedValueConstraint; - -// Standard implementation for value constraints of arbitrary `Setting` and `Constraint` -// types where `Setting: PartialEq`: -macro_rules! impl_non_numeric_value_constraint { - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueConstraint<$c> - where - $s: PartialEq<$c>, - { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - if let Some(exact) = self.exact.as_ref() { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'exact' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(actual) if actual == exact => {} - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(ideal) = self.ideal.as_ref() { - match setting { - Some(actual) if actual == ideal => { - // As specified in step 8 of the `fitness distance` algorithm: - // - // - // > For all string, enum and boolean constraints […], - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - Ok(0.0) - } - _ => { - // As specified in step 5 of the `fitness distance` algorithm: - // - // - // > If the settings dictionary's `constraintName` member - // > does not exist, the fitness distance is 1. - Ok(1.0) - } - } - } else { - // As specified in step 6 of the `fitness distance` algorithm: - // - // - // > If no ideal value is specified (constraintValue either - // > contains no member named 'ideal', or, if bare values are to be - // > treated as 'ideal', isn't a bare value), the fitness distance is 0. - Ok(0.0) - } - } - } - }; -} - -impl_non_numeric_value_constraint!(setting: bool, constraint: bool); -impl_non_numeric_value_constraint!(setting: String, constraint: String); - -// Specialized implementations for floating-point value constraints (and settings): - -macro_rules! impl_numeric_value_constraint { - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueConstraint<$c> { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - if let Some(exact) = self.exact { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'exact' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(&actual) if super::is_nearly_equal_to(actual as f64, exact as f64) => { - } - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(ideal) = self.ideal { - match setting { - Some(&actual) => { - let actual: f64 = actual as f64; - let ideal: f64 = ideal as f64; - // As specified in step 7 of the `fitness distance` algorithm: - // - // - // > For all positive numeric constraints […], - // > the fitness distance is the result of the formula - // > - // > ``` - // > (actual == ideal) ? 0 : |actual - ideal| / max(|actual|, |ideal|) - // > ``` - Ok(super::relative_fitness_distance(actual, ideal)) - } - None => { - // As specified in step 5 of the `fitness distance` algorithm: - // - // - // > If the settings dictionary's `constraintName` member - // > does not exist, the fitness distance is 1. - Ok(1.0) - } - } - } else { - // As specified in step 6 of the `fitness distance` algorithm: - // - // - // > If no ideal value is specified (constraintValue either - // > contains no member named 'ideal', or, if bare values are to be - // > treated as 'ideal', isn't a bare value), the fitness distance is 0. - Ok(0.0) - } - } - } - }; -} - -impl_numeric_value_constraint!(setting: f64, constraint: f64); -impl_numeric_value_constraint!(setting: i64, constraint: u64); -impl_numeric_value_constraint!(setting: i64, constraint: f64); -impl_numeric_value_constraint!(setting: f64, constraint: u64); - -// Specialized implementations for boolean value constraints of mismatching -// and thus either "existence"-checked or ignored setting types: -macro_rules! impl_exists_value_constraint { - (settings: [$($s:ty),+], constraint: bool) => { - $(impl_exists_value_constraint!(setting: $s, constraint: bool);)+ - }; - (setting: $s:ty, constraint: bool) => { - impl<'a> FitnessDistance> for ResolvedValueConstraint { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - // A bare boolean value (as described in step 4 of the - // `fitness distance` algorithm) gets parsed as: - // ``` - // ResolvedValueConstraint:: { - // exact: Some(bare), - // ideal: None, - // } - // ``` - // - // For all other configurations we just interpret it as an incompatible constraint. - match self.exact { - // As specified in step 4 of the `fitness distance` algorithm: - // - // - // > If constraintValue is a boolean, but the constrainable property is not, - // > then the fitness distance is based on whether the settings dictionary's - // > `constraintName` member exists or not, from the formula: - // > - // > ``` - // > (constraintValue == exists) ? 0 : 1 - // > ``` - Some(expected) => { - if setting.is_some() == expected { - Ok(0.0) - } else { - Ok(1.0) - } - } - // As specified in step 3 of the `fitness distance` algorithm: - // - // - // > If the constraint does not apply for this type of object, - // > the fitness distance is 0 (that is, the constraint does not - // > influence the fitness distance). - None => Ok(0.0), - } - } - } - }; -} - -impl_exists_value_constraint!(settings: [String, i64, f64], constraint: bool); - -#[cfg(test)] -mod tests; diff --git a/constraints/src/algorithms/fitness_distance/value_constraint/tests.rs b/constraints/src/algorithms/fitness_distance/value_constraint/tests.rs deleted file mode 100644 index 7090c6626..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint/tests.rs +++ /dev/null @@ -1,144 +0,0 @@ -use super::*; - -macro_rules! generate_value_constraint_tests { - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr $(,)? - }),+ $(,)? - ], - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - generate_value_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - expected: $e - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - expected: $e:expr $(,)? - ) => { - generate_value_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - validate: |$a:ident| $b:block - ) => { - $( - #[test] - fn $ti() { - test_value_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |$a| $b - ); - } - )+ - }; -} - -macro_rules! test_value_constraint { - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - test_value_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - validate: |$a:ident| $b:block - ) => {{ - let settings: &[Option<$st>] = $se; - let constraints: &[ResolvedValueConstraint<$ct>] = $ce; - - for constraint in constraints { - for setting in settings { - let closure = |$a| $b; - let actual = constraint.fitness_distance(setting.as_ref()); - closure(actual); - } - } - }}; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ] - ) => { - test_value_constraint!( - checks: [ - $({ - setting: $st => $se, - constraint: $ct => $ce, - expected: $ee, - }),+ - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - }; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ], - validate: |$ai:ident, $ei:ident| $b:block - ) => {{ - $({ - let closure = |$ai, $ei| $b; - let actual = $ce.fitness_distance($se.as_ref()); - closure(actual, $ee); - })+ - }}; -} - -mod bool; -mod f64; -mod string; -mod u64; diff --git a/constraints/src/algorithms/fitness_distance/value_constraint/tests/bool.rs b/constraints/src/algorithms/fitness_distance/value_constraint/tests/bool.rs deleted file mode 100644 index aec57d984..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint/tests/bool.rs +++ /dev/null @@ -1,283 +0,0 @@ -use super::*; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: None, - ideal: None, - }, - ResolvedValueConstraint { - exact: None, - ideal: Some(true), - }, - ], - expected: Ok(0.0) - ); - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: None, - ideal: Some(false), - }, - ], - expected: Ok(0.0) - ); - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[None, Some(false)], - }, - ], - constraints: bool => &[ResolvedValueConstraint { - exact: None, - ideal: Some(true), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - // A constraint that does apply for a type of setting, - // is expected to return a fitness distance of `0`, - // iff the setting matches the constraint: - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - ], - constraints: bool => &[ResolvedValueConstraint { - exact: Some(true), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[None], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(true), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(true), - ideal: Some(true), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == true)".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(false)], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(true), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(true), - ideal: Some(true), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == true)".to_owned(), - setting: Some("false".to_owned()), - }) - ); - } - } - - // Required boolean constraints have specialized logic as per - // rule 4 of the fitness distance algorithm specification: - // - - mod specialization { - use super::*; - - mod expected { - use super::*; - - mod existing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(true), - ideal: None, - }, - ], - expected: Ok(0.0) - ); - } - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None], - }, - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(true), - ideal: None, - }, - ], - expected: Ok(1.0) - ); - } - } - - mod unexpected { - use super::*; - - mod existing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(false), - ideal: None, - }, - ], - expected: Ok(1.0) - ); - } - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None], - }, - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: bool => &[ - ResolvedValueConstraint { - exact: Some(false), - ideal: None, - }, - ], - expected: Ok(0.0) - ); - } - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_constraint/tests/f64.rs b/constraints/src/algorithms/fitness_distance/value_constraint/tests/f64.rs deleted file mode 100644 index 0ad486021..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint/tests/f64.rs +++ /dev/null @@ -1,243 +0,0 @@ -use super::*; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ - ResolvedValueConstraint { - exact: None, - ideal: None, - }, - ResolvedValueConstraint { - exact: None, - ideal: Some(42.0), - }, - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: f64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ResolvedValueConstraint { - exact: None, - ideal: Some(42.0), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ResolvedValueConstraint { - exact: Some(42.0), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: f64 => &[ - ResolvedValueConstraint { - exact: Some(42.0), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == 42.0)".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: f64 => &[ - ResolvedValueConstraint { - exact: Some(42.0), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42.0)".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ - ResolvedValueConstraint { - exact: Some(42.0), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42.0)".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_constraint/tests/string.rs b/constraints/src/algorithms/fitness_distance/value_constraint/tests/string.rs deleted file mode 100644 index ca663a446..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint/tests/string.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::*; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: String => &[ - ResolvedValueConstraint { - exact: None, - ideal: None, - }, - ResolvedValueConstraint { - exact: None, - ideal: Some("foo".to_owned()), - }, - ], - expected: Ok(0.0) - ); - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None, Some("bar".to_owned())], - }, - ], - constraints: String => &[ResolvedValueConstraint { - exact: None, - ideal: Some("foo".to_owned()), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - // A constraint that does apply for a type of setting, - // is expected to return a fitness distance of `0`, - // iff the setting matches the constraint: - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: String => &[ResolvedValueConstraint { - exact: Some("foo".to_owned()), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None], - }, - ], - constraints: String => &[ - ResolvedValueConstraint { - exact: Some("foo".to_owned()), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some("foo".to_owned()), - ideal: Some("foo".to_owned()), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == \"foo\")".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("bar".to_owned())], - }, - ], - constraints: String => &[ - ResolvedValueConstraint { - exact: Some("foo".to_owned()), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some("foo".to_owned()), - ideal: Some("foo".to_owned()), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == \"foo\")".to_owned(), - setting: Some("\"bar\"".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_constraint/tests/u64.rs b/constraints/src/algorithms/fitness_distance/value_constraint/tests/u64.rs deleted file mode 100644 index 81be604ee..000000000 --- a/constraints/src/algorithms/fitness_distance/value_constraint/tests/u64.rs +++ /dev/null @@ -1,243 +0,0 @@ -use super::*; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ - ResolvedValueConstraint { - exact: None, - ideal: None, - }, - ResolvedValueConstraint { - exact: None, - ideal: Some(42), - }, - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: i64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: i64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: i64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: u64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: u64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: u64 => ResolvedValueConstraint { - exact: None, - ideal: Some(4), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None, Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[None, Some(0.0)], - }, - ], - constraints: u64 => &[ResolvedValueConstraint { - exact: None, - ideal: Some(42), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ResolvedValueConstraint { - exact: Some(42), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: u64 => &[ - ResolvedValueConstraint { - exact: Some(42), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == 42)".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: u64 => &[ - ResolvedValueConstraint { - exact: Some(42), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42)".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: u64 => &[ - ResolvedValueConstraint { - exact: Some(42), - ideal: None, - }, - ResolvedValueConstraint { - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42)".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_range_constraint.rs b/constraints/src/algorithms/fitness_distance/value_range_constraint.rs deleted file mode 100644 index 68147c768..000000000 --- a/constraints/src/algorithms/fitness_distance/value_range_constraint.rs +++ /dev/null @@ -1,172 +0,0 @@ -use super::setting::SettingFitnessDistanceError; -use super::{FitnessDistance, SettingFitnessDistanceErrorKind}; -use crate::ResolvedValueRangeConstraint; - -macro_rules! impl_value_range_constraint { - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueRangeConstraint<$c> { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - if let Some(exact) = self.exact { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'exact' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(&actual) if super::is_nearly_equal_to(actual as f64, exact as f64) => { - } - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(min) = self.min { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'min' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(&actual) - if super::is_nearly_greater_than_or_equal_to( - actual as f64, - min as f64, - ) => {} - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::TooSmall, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(max) = self.max { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'max' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(&actual) - if super::is_nearly_less_than_or_equal_to( - actual as f64, - max as f64, - ) => {} - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::TooLarge, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(ideal) = self.ideal { - match setting { - Some(&actual) => { - let actual: f64 = actual as f64; - let ideal: f64 = ideal as f64; - // As specified in step 7 of the `fitness distance` algorithm: - // - // - // > For all positive numeric constraints […], - // > the fitness distance is the result of the formula - // > - // > ``` - // > (actual == ideal) ? 0 : |actual - ideal| / max(|actual|, |ideal|) - // > ``` - Ok(super::relative_fitness_distance(actual, ideal)) - } - None => { - // As specified in step 5 of the `fitness distance` algorithm: - // - // - // > If the settings dictionary's `constraintName` member - // > does not exist, the fitness distance is 1. - Ok(1.0) - } - } - } else { - // As specified in step 6 of the `fitness distance` algorithm: - // - // - // > If no ideal value is specified (constraintValue either - // > contains no member named 'ideal', or, if bare values are to be - // > treated as 'ideal', isn't a bare value), the fitness distance is 0. - Ok(0.0) - } - } - } - }; -} - -impl_value_range_constraint!(setting: f64, constraint: f64); -impl_value_range_constraint!(setting: i64, constraint: u64); -impl_value_range_constraint!(setting: i64, constraint: f64); -impl_value_range_constraint!(setting: f64, constraint: u64); - -// Specialized implementations for non-boolean value constraints of mismatching, -// and thus ignored setting types: -macro_rules! impl_ignored_value_range_constraint { - (settings: [$($s:ty),+], constraint: $c:ty) => { - $(impl_ignored_value_range_constraint!(setting: $s, constraint: $c);)+ - }; - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueRangeConstraint<$c> { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, _setting: Option<&'a $s>) -> Result { - // As specified in step 3 of the `fitness distance` algorithm: - // - // - // > If the constraint does not apply for this type of object, - // > the fitness distance is 0 (that is, the constraint does not - // > influence the fitness distance). - Ok(0.0) - } - } - }; -} - -impl_ignored_value_range_constraint!(settings: [bool, String], constraint: u64); -impl_ignored_value_range_constraint!(settings: [bool, String], constraint: f64); - -#[cfg(test)] -mod tests; diff --git a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests.rs b/constraints/src/algorithms/fitness_distance/value_range_constraint/tests.rs deleted file mode 100644 index 97e495aee..000000000 --- a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests.rs +++ /dev/null @@ -1,143 +0,0 @@ -use super::*; - -macro_rules! generate_value_range_constraint_tests { - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr $(,)? - }),+ $(,)? - ], - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - generate_value_range_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - expected: $e - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - expected: $e:expr $(,)? - ) => { - generate_value_range_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - validate: |$a:ident| $b:block - ) => { - $( - #[test] - fn $ti() { - test_value_range_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |$a| $b - ); - } - )+ - }; -} - -macro_rules! test_value_range_constraint { - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - test_value_range_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - validate: |$a:ident| $b:block - ) => {{ - let settings: &[Option<$st>] = $se; - let constraints: &[ResolvedValueRangeConstraint<$ct>] = $ce; - - for constraint in constraints { - for setting in settings { - let closure = |$a| $b; - let actual = constraint.fitness_distance(setting.as_ref()); - closure(actual); - } - } - }}; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ] - ) => { - test_value_range_constraint!( - checks: [ - $({ - setting: $st => $se, - constraint: $ct => $ce, - expected: $ee, - }),+ - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - }; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ], - validate: |$ai:ident, $ei:ident| $b:block - ) => {{ - $({ - let closure = |$ai, $ei| $b; - let actual = $ce.fitness_distance($se.as_ref()); - closure(actual, $ee); - })+ - }}; -} - -mod empty; -mod f64; -mod u64; diff --git a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/empty.rs b/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/empty.rs deleted file mode 100644 index c2fdce3fc..000000000 --- a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/empty.rs +++ /dev/null @@ -1,75 +0,0 @@ -use super::*; - -macro_rules! generate_empty_value_range_constraint_tests { - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr $(,)? - }),+ $(,)? - ], - constraint: $ct:ty $(,)? - ) => { - generate_value_range_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - }),+ - ], - constraints: $ct => &[ - ResolvedValueRangeConstraint::<$ct> { - min: None, - max: None, - exact: None, - ideal: None, - } - ], - expected: Ok(0.0) - ); - }; -} - -mod u64_constraint { - use super::*; - - generate_empty_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(false)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraint: u64, - ); -} - -mod f64_constraint { - use super::*; - - generate_empty_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(false)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - ], - constraint: f64, - ); -} diff --git a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/f64.rs b/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/f64.rs deleted file mode 100644 index 79a4afc45..000000000 --- a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/f64.rs +++ /dev/null @@ -1,312 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42.0), - }], - expected: Ok(0.0) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: f64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42.0), - } - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_range_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_range_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: f64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4.0), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42.0), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: None, - }], - expected: Ok(0.0) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: f64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: None, - } - ], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: f64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == 42.0)".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: f64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42.0)".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42.0), - ideal: Some(42.0), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42.0)".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/u64.rs b/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/u64.rs deleted file mode 100644 index 72ec1845e..000000000 --- a/constraints/src/algorithms/fitness_distance/value_range_constraint/tests/u64.rs +++ /dev/null @@ -1,312 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42), - }], - expected: Ok(0.0) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: u64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42), - } - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_range_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_range_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: u64 => ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(4), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None, Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[None, Some(0.0)], - }, - ], - constraints: u64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: None, - ideal: Some(42), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: None, - }], - expected: Ok(0.0) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: u64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: None, - } - ], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: u64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == 42)".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_range_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: u64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42)".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_range_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: u64 => &[ - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: None, - }, - ResolvedValueRangeConstraint { - min: None, - max: None, - exact: Some(42), - ideal: Some(42), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == 42)".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint.rs deleted file mode 100644 index e8f68c28c..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint.rs +++ /dev/null @@ -1,173 +0,0 @@ -use super::setting::SettingFitnessDistanceError; -use super::{FitnessDistance, SettingFitnessDistanceErrorKind}; -use crate::ResolvedValueSequenceConstraint; - -macro_rules! impl_non_numeric_value_sequence_constraint { - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueSequenceConstraint<$c> - where - $s: PartialEq<$c>, - { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - if let Some(exact) = self.exact.as_ref() { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'exact' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(actual) if exact.contains(actual) => {} - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(ideal) = self.ideal.as_ref() { - // As specified in step 8 of the `fitness distance` algorithm: - // - // - // > For all string, enum and boolean constraints […], - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - // - // As well as step 5 of the `fitness distance` algorithm: - // - // - // > If the settings dictionary's `constraintName` member - // > does not exist, the fitness distance is 1. - match setting { - Some(actual) if ideal.contains(actual) => Ok(0.0), - Some(_) => Ok(1.0), - None => Ok(1.0), - } - } else { - // As specified in step 6 of the `fitness distance` algorithm: - // - // - // > If no ideal value is specified (constraintValue either - // > contains no member named 'ideal', or, if bare values are to be - // > treated as 'ideal', isn't a bare value), the fitness distance is 0. - Ok(0.0) - } - } - } - }; -} - -impl_non_numeric_value_sequence_constraint!(setting: bool, constraint: bool); -impl_non_numeric_value_sequence_constraint!(setting: String, constraint: String); - -macro_rules! impl_numeric_value_sequence_constraint { - (setting: $s:ty, constraint: $c:ty) => { - impl<'a> FitnessDistance> for ResolvedValueSequenceConstraint<$c> { - type Error = SettingFitnessDistanceError; - - fn fitness_distance(&self, setting: Option<&'a $s>) -> Result { - if let Some(exact) = &self.exact { - // As specified in step 2 of the `fitness distance` algorithm: - // - // - // > If the constraint is required (constraintValue either contains - // > one or more members named […] 'exact' […]), and the settings - // > dictionary's constraintName member's value does not satisfy the - // > constraint or doesn't exist, the fitness distance is positive infinity. - match setting { - Some(&actual) if exact.contains(&(actual as $c)) => {} - Some(setting) => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: format!("{}", self.to_required_only()), - setting: Some(format!("{:?}", setting)), - }) - } - None => { - return Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: format!("{}", self.to_required_only()), - setting: None, - }) - } - }; - } - - if let Some(ideal) = &self.ideal { - // As specified in step 8 of the `fitness distance` algorithm: - // - // - // > For all string, enum and boolean constraints […], - // > the fitness distance is the result of the formula: - // > - // > ``` - // > (actual == ideal) ? 0 : 1 - // > ``` - // - // As well as step 5 of the `fitness distance` algorithm: - // - // - // > If the settings dictionary's `constraintName` member - // > does not exist, the fitness distance is 1. - match setting { - Some(&actual) => { - let actual: f64 = actual as f64; - let mut min_fitness_distance = 1.0; - for ideal in ideal.into_iter() { - let ideal: f64 = (*ideal) as f64; - // As specified in step 7 of the `fitness distance` algorithm: - // - // - // > For all positive numeric constraints […], - // > the fitness distance is the result of the formula - // > - // > ``` - // > (actual == ideal) ? 0 : |actual - ideal| / max(|actual|, |ideal|) - // > ``` - let fitness_distance = - super::relative_fitness_distance(actual, ideal); - if fitness_distance < min_fitness_distance { - min_fitness_distance = fitness_distance; - } - } - Ok(min_fitness_distance) - } - None => Ok(1.0), - } - } else { - // As specified in step 6 of the `fitness distance` algorithm: - // - // - // > If no ideal value is specified (constraintValue either - // > contains no member named 'ideal', or, if bare values are to be - // > treated as 'ideal', isn't a bare value), the fitness distance is 0. - Ok(0.0) - } - } - } - }; -} - -impl_numeric_value_sequence_constraint!(setting: f64, constraint: f64); -impl_numeric_value_sequence_constraint!(setting: i64, constraint: u64); -impl_numeric_value_sequence_constraint!(setting: i64, constraint: f64); -impl_numeric_value_sequence_constraint!(setting: f64, constraint: u64); - -#[cfg(test)] -mod tests; diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests.rs deleted file mode 100644 index 5750ae0a8..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests.rs +++ /dev/null @@ -1,144 +0,0 @@ -use super::*; - -macro_rules! generate_value_constraint_tests { - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr $(,)? - }),+ $(,)? - ], - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - generate_value_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - expected: $e - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - expected: $e:expr $(,)? - ) => { - generate_value_constraint_tests!( - tests: [ - $({ - name: $ti, - settings: $st => $se, - constraints: $ct => $ce, - }),+ - ], - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - tests: [ - $({ - name: $ti:ident, - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr $(,)? - }),+ $(,)? - ], - validate: |$a:ident| $b:block - ) => { - $( - #[test] - fn $ti() { - test_value_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |$a| $b - ); - } - )+ - }; -} - -macro_rules! test_value_constraint { - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - expected: $e:expr $(,)? - ) => { - test_value_constraint!( - settings: $st => $se, - constraints: $ct => $ce, - validate: |result| { - assert_eq!(result, $e); - } - ); - }; - ( - settings: $st:ty => $se:expr, - constraints: $ct:ty => $ce:expr, - validate: |$a:ident| $b:block - ) => {{ - let settings: &[Option<$st>] = $se; - let constraints: &[ResolvedValueSequenceConstraint<$ct>] = $ce; - - for constraint in constraints { - for setting in settings { - let closure = |$a| $b; - let actual = constraint.fitness_distance(setting.as_ref()); - closure(actual); - } - } - }}; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ] - ) => { - test_value_constraint!( - checks: [ - $({ - setting: $st => $se, - constraint: $ct => $ce, - expected: $ee, - }),+ - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - }; - ( - checks: [ - $({ - setting: $st:ty => $se:expr, - constraint: $ct:ty => $ce:expr, - expected: $ee:expr $(,)? - }),+ $(,)? - ], - validate: |$ai:ident, $ei:ident| $b:block - ) => {{ - $({ - let closure = |$ai, $ei| $b; - let actual = $ce.fitness_distance($se.as_ref()); - closure(actual, $ee); - })+ - }}; -} - -mod bool; -mod f64; -mod string; -mod u64; diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/bool.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/bool.rs deleted file mode 100644 index b6d4bc245..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/bool.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - ], - constraints: bool => &[ - ResolvedValueSequenceConstraint { - exact: None, - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![true]), - }, - ], - expected: Ok(0.0) - ); - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[None, Some(false)], - }, - ], - constraints: bool => &[ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![true]), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - // A constraint that does apply for a type of setting, - // is expected to return a fitness distance of `0`, - // iff the setting matches the constraint: - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(true)], - }, - ], - constraints: bool => &[ResolvedValueSequenceConstraint { - exact: Some(vec![true]), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[None], - }, - ], - constraints: bool => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![true]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![true]), - ideal: Some(vec![true]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == [true])".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: bool_setting, - settings: bool => &[Some(false)], - }, - ], - constraints: bool => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![true]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![true]), - ideal: Some(vec![true]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [true])".to_owned(), - setting: Some("false".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/f64.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/f64.rs deleted file mode 100644 index aa3803323..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/f64.rs +++ /dev/null @@ -1,245 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ - ResolvedValueSequenceConstraint { - exact: None, - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![42.0]), - }, - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: f64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4.0]), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![42.0]), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: f64 => &[ResolvedValueSequenceConstraint { - exact: Some(vec![42.0]), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: f64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: Some(vec![1.5]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == [1.0, 1.5, 2.0])".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: f64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: Some(vec![1.5]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [1.0, 1.5, 2.0])".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: f64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![1.0, 1.5, 2.0]), - ideal: Some(vec![1.5]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [1.0, 1.5, 2.0])".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/string.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/string.rs deleted file mode 100644 index 5b2b11ece..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/string.rs +++ /dev/null @@ -1,133 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: String => &[ - ResolvedValueSequenceConstraint { - exact: None, - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec!["foo".to_owned()]), - }, - ], - expected: Ok(0.0) - ); - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None, Some("bar".to_owned())], - }, - ], - constraints: String => &[ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec!["foo".to_owned()]), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - // A constraint that does apply for a type of setting, - // is expected to return a fitness distance of `0`, - // iff the setting matches the constraint: - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("foo".to_owned())], - }, - ], - constraints: String => &[ResolvedValueSequenceConstraint { - exact: Some(vec!["foo".to_owned()]), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[None], - }, - ], - constraints: String => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()]), - ideal: Some(vec!["foo".to_owned()]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == [\"foo\", \"bar\", \"baz\"])".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: string_setting, - settings: String => &[Some("blee".to_owned())], - }, - ], - constraints: String => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()]), - ideal: Some(vec!["foo".to_owned()]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [\"foo\", \"bar\", \"baz\"])".to_owned(), - setting: Some("\"blee\"".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/u64.rs b/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/u64.rs deleted file mode 100644 index 4693fa143..000000000 --- a/constraints/src/algorithms/fitness_distance/value_sequence_constraint/tests/u64.rs +++ /dev/null @@ -1,244 +0,0 @@ -use super::*; -use crate::algorithms::SettingFitnessDistanceErrorKind; - -mod basic { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ - ResolvedValueSequenceConstraint { - exact: None, - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![42]), - }, - ], - expected: Ok(0.0) - ); - } - - mod fract_distance { - use super::*; - - #[test] - fn i64_setting() { - test_value_constraint!( - checks: [ - { - setting: i64 => Some(1), - constraint: i64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.75), - }, - { - setting: i64 => Some(2), - constraint: i64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.5), - }, - { - setting: i64 => Some(3), - constraint: i64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - - #[test] - fn f64_setting() { - test_value_constraint!( - checks: [ - { - setting: f64 => Some(1.0), - constraint: u64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.75), - }, - { - setting: f64 => Some(2.0), - constraint: u64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.5), - }, - { - setting: f64 => Some(3.0), - constraint: u64 => ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![4]), - }, - expected: Ok(0.25), - }, - ], - validate: |actual, expected| { - assert_eq!(actual, expected); - } - ); - } - } - - mod one_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None, Some(0)], - }, - { - name: f64_setting, - settings: f64 => &[None, Some(0.0)], - }, - ], - constraints: u64 => &[ResolvedValueSequenceConstraint { - exact: None, - ideal: Some(vec![42]), - }], - expected: Ok(1.0) - ); - } -} - -mod required { - use super::*; - - mod zero_distance { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(42)], - }, - { - name: f64_setting, - settings: f64 => &[Some(42.0)], - }, - ], - constraints: u64 => &[ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: None, - }], - expected: Ok(0.0) - ); - } - - mod inf_distance { - use super::*; - - mod missing { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[None], - }, - { - name: f64_setting, - settings: f64 => &[None], - }, - ], - constraints: u64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: Some(vec![42]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Missing, - constraint: "(x == [42])".to_owned(), - setting: None, - }) - ); - } - - mod mismatch { - use super::*; - - generate_value_constraint_tests!( - tests: [ - { - name: i64_setting, - settings: i64 => &[Some(0)], - }, - ], - constraints: u64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: Some(vec![42]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [42])".to_owned(), - setting: Some("0".to_owned()), - }) - ); - - generate_value_constraint_tests!( - tests: [ - { - name: f64_setting, - settings: f64 => &[Some(0.0)], - }, - ], - constraints: u64 => &[ - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: None, - }, - ResolvedValueSequenceConstraint { - exact: Some(vec![42]), - ideal: Some(vec![42]), - }, - ], - expected: Err(SettingFitnessDistanceError { - kind: SettingFitnessDistanceErrorKind::Mismatch, - constraint: "(x == [42])".to_owned(), - setting: Some("0.0".to_owned()), - }) - ); - } - } -} diff --git a/constraints/src/algorithms/select_settings.rs b/constraints/src/algorithms/select_settings.rs deleted file mode 100644 index bd0798b3e..000000000 --- a/constraints/src/algorithms/select_settings.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::HashSet; - -use thiserror::Error; - -use crate::algorithms::fitness_distance::SettingFitnessDistanceError; -use crate::errors::OverconstrainedError; -use crate::{MediaTrackSettings, SanitizedMediaTrackConstraints}; - -mod apply_advanced; -mod apply_mandatory; -mod select_optimal; -mod tie_breaking; - -use self::apply_advanced::*; -use self::apply_mandatory::*; -use self::select_optimal::*; -pub use self::tie_breaking::*; - -/// A mode indicating whether device information may be exposed. -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum DeviceInformationExposureMode { - /// Device information may be exposed. - Exposed, - /// Device information may NOT be exposed. - Protected, -} - -/// An error type indicating a failure of the `SelectSettings` algorithm. -#[derive(Error, Clone, Eq, PartialEq, Debug)] -pub enum SelectSettingsError { - /// An error caused by one or more over-constrained settings. - #[error(transparent)] - Overconstrained(#[from] OverconstrainedError), -} - -/// This function implements steps 1-5 of the `SelectSettings` algorithm -/// as defined by the W3C spec: -/// -/// -/// Step 6 (tie-breaking) is omitted by this implementation and expected to be performed -/// manually on the returned candidates. -/// For this several implementation of `TieBreakingPolicy` are provided by this crate. -pub fn select_settings_candidates<'a, I>( - possible_settings: I, - constraints: &SanitizedMediaTrackConstraints, - exposure_mode: DeviceInformationExposureMode, -) -> Result, SelectSettingsError> -where - I: IntoIterator, -{ - let possible_settings = possible_settings.into_iter(); - - // As specified in step 1 of the `SelectSettings` algorithm: - // - // - // > Each constraint specifies one or more values (or a range of values) for its property. - // > A property MAY appear more than once in the list of 'advanced' ConstraintSets. - // > If an empty list has been given as the value for a constraint, - // > it MUST be interpreted as if the constraint were not specified - // > (in other words, an empty constraint == no constraint). - // > - // > Note that unknown properties are discarded by WebIDL, - // > which means that unknown/unsupported required constraints will silently disappear. - // > To avoid this being a surprise, application authors are expected to first use - // > the `getSupportedConstraints()` method […]. - - // We expect "sanitized" constraints to not contain empty constraints: - debug_assert!(constraints - .mandatory - .iter() - .all(|(_, constraint)| !constraint.is_empty())); - - // Obtain candidates by filtering possible settings, dropping those with infinite fitness distances: - // - // This function call corresponds to steps 3 & 4 of the `SelectSettings` algorithm: - // - - let candidates_and_fitness_distances = - apply_mandatory_constraints(possible_settings, &constraints.mandatory, exposure_mode)?; - - // As specified in step 5 of the `SelectSettings` algorithm: - // - // - // > Iterate over the 'advanced' ConstraintSets in newConstraints in the order in which they were specified. - // > - // > For each ConstraintSet: - // > - // > 1. compute the fitness distance between it and each settings dictionary in candidates, - // > treating bare values of properties as exact. - // > - // > 2. If the fitness distance is finite for one or more settings dictionaries in candidates, - // > keep those settings dictionaries in candidates, discarding others. - // > - // > If the fitness distance is infinite for all settings dictionaries in candidates, - // > ignore this ConstraintSet. - let candidates = - apply_advanced_constraints(candidates_and_fitness_distances, &constraints.advanced); - - // As specified in step 6 of the `SelectSettings` algorithm: - // - // - // > Select one settings dictionary from candidates, and return it as the result of the `SelectSettings` algorithm. - // > The User Agent MUST use one with the smallest fitness distance, as calculated in step 3. - // > If more than one settings dictionary have the smallest fitness distance, - // > the User Agent chooses one of them based on system default property values and User Agent default property values. - // - // # Important - // Instead of return just ONE settings instance "with the smallest fitness distance, as calculated in step 3" - // we instead return ALL settings instances "with the smallest fitness distance, as calculated in step 3" - // and leave tie-breaking to the User Agent in a separate step: - Ok(select_optimal_candidates(candidates)) -} - -#[derive(Default)] -pub(crate) struct ConstraintFailureInfo { - pub(crate) failures: usize, - pub(crate) errors: HashSet, -} - -#[cfg(test)] -mod tests; diff --git a/constraints/src/algorithms/select_settings/apply_advanced.rs b/constraints/src/algorithms/select_settings/apply_advanced.rs deleted file mode 100644 index b7adfbe51..000000000 --- a/constraints/src/algorithms/select_settings/apply_advanced.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::algorithms::FitnessDistance; -use crate::constraints::SanitizedAdvancedMediaTrackConstraints; -use crate::MediaTrackSettings; - -/// Returns the set of settings for which all non-overconstraining advanced constraints' -/// fitness distance is finite. -/// -/// Implements step 5 of the `SelectSettings` algorithm: -/// -/// -/// # Note: -/// This may change the order of items in `feasible_candidates`. -/// In practice however this is not a problem as we have to sort -/// it by fitness-distance eventually anyway. -pub(super) fn apply_advanced_constraints<'a>( - mut candidates: Vec<(&'a MediaTrackSettings, f64)>, - advanced_constraints: &SanitizedAdvancedMediaTrackConstraints, -) -> Vec<(&'a MediaTrackSettings, f64)> { - // As specified in step 5 of the `SelectSettings` algorithm: - // - // - // > Iterate over the 'advanced' ConstraintSets in newConstraints in the order in which they were specified. - // > - // > For each ConstraintSet: - // > - // > 1. compute the fitness distance between it and each settings dictionary in candidates, - // > treating bare values of properties as exact. - // > - // > 2. If the fitness distance is finite for one or more settings dictionaries in candidates, - // > keep those settings dictionaries in candidates, discarding others. - // > - // > If the fitness distance is infinite for all settings dictionaries in candidates, - // > ignore this ConstraintSet. - - let mut selected_candidates = Vec::with_capacity(candidates.len()); - - // Double-buffered sieving to avoid excessive vec allocations: - for advanced_constraint_set in advanced_constraints.iter() { - for (candidate, fitness_distance) in candidates.iter() { - if advanced_constraint_set.fitness_distance(candidate).is_ok() { - selected_candidates.push((*candidate, *fitness_distance)); - } - } - - if !selected_candidates.is_empty() { - candidates.clear(); - std::mem::swap(&mut candidates, &mut selected_candidates); - } - } - - candidates -} - -#[cfg(test)] -mod tests { - use std::iter::FromIterator; - - use super::*; - use crate::property::all::name::*; - use crate::{ - MediaTrackSupportedConstraints, ResizeMode, ResolvedAdvancedMediaTrackConstraints, - ResolvedMediaTrackConstraintSet, ResolvedValueConstraint, ResolvedValueRangeConstraint, - }; - - // Advanced constraint sets that doe not match any - // candidates should just get ignored: - #[test] - fn overconstrained() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let settings = [ - MediaTrackSettings::from_iter([(&DEVICE_ID, "foo".into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "bar".into())]), - ]; - - let candidates: Vec<_> = settings - .iter() - // attach a dummy fitness function: - .map(|settings| (settings, 42.0)) - .collect(); - - let constraints = ResolvedAdvancedMediaTrackConstraints::from_iter([ - ResolvedMediaTrackConstraintSet::from_iter([( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("bazblee".to_owned()) - .into(), - )]), - ]); - - let sanitized_constraints = constraints.to_sanitized(&supported_constraints); - - let actual: Vec<_> = apply_advanced_constraints(candidates, &sanitized_constraints) - .into_iter() - // drop the dummy fitness distance: - .map(|(settings, _)| settings) - .collect(); - - let expected: Vec<_> = settings.iter().collect(); - - assert_eq!(actual, expected); - } - - #[test] - fn constrained() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&HEIGHT, 480.into()), - (&WIDTH, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&HEIGHT, 720.into()), - (&WIDTH, 1280.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&HEIGHT, 1080.into()), - (&WIDTH, 1920.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&HEIGHT, 1440.into()), - (&WIDTH, 2560.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&HEIGHT, 2160.into()), - (&WIDTH, 3840.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - ]; - - let candidates: Vec<_> = settings.iter().map(|settings| (settings, 42.0)).collect(); - - let constraints = ResolvedAdvancedMediaTrackConstraints::from_iter([ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - ResolvedMediaTrackConstraintSet::from_iter([( - &HEIGHT, - ResolvedValueRangeConstraint::default().exact(800).into(), - )]), - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - ResolvedMediaTrackConstraintSet::from_iter([( - &RESIZE_MODE, - ResolvedValueConstraint::default() - .exact(ResizeMode::none()) - .into(), - )]), - // The second advanced constraint set of "max 1440p" does match - // candidates and should thus be applied by the algorithm: - ResolvedMediaTrackConstraintSet::from_iter([( - &HEIGHT, - ResolvedValueRangeConstraint::default().max(1440).into(), - )]), - ]); - - let sanitized_constraints = constraints.to_sanitized(&supported_constraints); - - let actual: Vec<_> = apply_advanced_constraints(candidates, &sanitized_constraints) - .into_iter() - // drop the dummy fitness distance: - .map(|(settings, _)| settings) - .collect(); - - let expected = vec![&settings[2], &settings[3]]; - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/algorithms/select_settings/apply_mandatory.rs b/constraints/src/algorithms/select_settings/apply_mandatory.rs deleted file mode 100644 index a9bba70e8..000000000 --- a/constraints/src/algorithms/select_settings/apply_mandatory.rs +++ /dev/null @@ -1,213 +0,0 @@ -use std::collections::HashMap; - -use crate::algorithms::select_settings::{ConstraintFailureInfo, DeviceInformationExposureMode}; -use crate::algorithms::FitnessDistance; -use crate::errors::OverconstrainedError; -use crate::{MediaTrackProperty, MediaTrackSettings, SanitizedMediaTrackConstraintSet}; - -/// Returns the set of settings for which all mandatory constraints' -/// fitness distance is finite. -/// -/// Implements step 5 of the `SelectSettings` algorithm: -/// -pub(super) fn apply_mandatory_constraints<'a, I>( - candidates: I, - mandatory_constraints: &SanitizedMediaTrackConstraintSet, - exposure_mode: DeviceInformationExposureMode, -) -> Result, OverconstrainedError> -where - I: IntoIterator, -{ - // As specified in step 3 of the `SelectSettings` algorithm: - // - // - // > For every possible settings dictionary of copy compute its fitness distance, - // > treating bare values of properties as ideal values. Let candidates be the - // > set of settings dictionaries for which the fitness distance is finite. - - let mut feasible_candidates: Vec<(&'a MediaTrackSettings, f64)> = vec![]; - let mut failed_constraints: HashMap = - Default::default(); - - for candidate in candidates { - match mandatory_constraints.fitness_distance(candidate) { - Ok(fitness_distance) => { - debug_assert!(fitness_distance.is_finite()); - - feasible_candidates.push((candidate, fitness_distance)); - } - Err(error) => { - for (property, setting_error) in error.setting_errors { - let entry = failed_constraints.entry(property).or_default(); - entry.failures += 1; - entry.errors.insert(setting_error); - } - } - } - } - - if feasible_candidates.is_empty() { - return Err(match exposure_mode { - DeviceInformationExposureMode::Exposed => { - OverconstrainedError::exposing_device_information(failed_constraints) - } - DeviceInformationExposureMode::Protected => OverconstrainedError::default(), - }); - } - - Ok(feasible_candidates) -} - -#[cfg(test)] -mod tests { - use std::iter::FromIterator; - - use super::*; - use crate::property::all::name::*; - use crate::{ - MediaTrackSupportedConstraints, ResizeMode, ResolvedMandatoryMediaTrackConstraints, - ResolvedValueConstraint, ResolvedValueRangeConstraint, - }; - - // Advanced constraint sets that do not match any candidates should just get ignored: - #[test] - fn overconstrained() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let settings = [ - MediaTrackSettings::from_iter([(&DEVICE_ID, "foo".into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "bar".into())]), - ]; - - let candidates: Vec<_> = settings.iter().collect(); - - let constraints = ResolvedMandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("mismatched-device".to_owned()) - .into(), - )]); - - let sanitized_constraints = constraints.to_sanitized(&supported_constraints); - - // Exposed exposure mode: - - let error = apply_mandatory_constraints( - candidates.clone(), - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap_err(); - - let constraint = &error.constraint; - let err_message = error.message.as_ref().expect("Error message."); - - assert_eq!(constraint, &DEVICE_ID); - assert_eq!( - err_message, - "Setting was a mismatch ([\"bar\", \"foo\"] do not satisfy (x == \"mismatched-device\"))." - ); - - // Protected exposure mode: - - let error = apply_mandatory_constraints( - candidates, - &sanitized_constraints, - DeviceInformationExposureMode::Protected, - ) - .unwrap_err(); - - let constraint = &error.constraint; - let err_message = error.message; - - assert_eq!( - constraint, - &MediaTrackProperty::from(""), - "Constraint should not have been exposed" - ); - assert!( - err_message.is_none(), - "Error message should not have been exposed" - ); - } - - #[test] - fn constrained() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&HEIGHT, 480.into()), - (&WIDTH, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&HEIGHT, 720.into()), - (&WIDTH, 1280.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&HEIGHT, 1080.into()), - (&WIDTH, 1920.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&HEIGHT, 1440.into()), - (&WIDTH, 2560.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&HEIGHT, 2160.into()), - (&WIDTH, 3840.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - ]; - - let candidates: Vec<_> = settings.iter().collect(); - - let constraints = ResolvedMandatoryMediaTrackConstraints::from_iter([ - ( - &RESIZE_MODE, - ResolvedValueConstraint::default() - .exact(ResizeMode::none()) - .into(), - ), - ( - &HEIGHT, - ResolvedValueRangeConstraint::default().min(1000).into(), - ), - ( - &WIDTH, - ResolvedValueRangeConstraint::default().max(2000).into(), - ), - ]); - - let sanitized_constraints = constraints.to_sanitized(&supported_constraints); - - let actual = apply_mandatory_constraints( - candidates, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - let expected = vec![(&settings[2], 0.0)]; - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/algorithms/select_settings/select_optimal.rs b/constraints/src/algorithms/select_settings/select_optimal.rs deleted file mode 100644 index dbc631053..000000000 --- a/constraints/src/algorithms/select_settings/select_optimal.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::MediaTrackSettings; - -pub(super) fn select_optimal_candidates<'a, I>(candidates: I) -> Vec<&'a MediaTrackSettings> -where - I: IntoIterator, -{ - let mut optimal_candidates = vec![]; - let mut optimal_fitness_distance = f64::INFINITY; - - for (candidate, fitness_distance) in candidates { - use std::cmp::Ordering; - - #[cfg(feature = "total_cmp")] - let ordering = fitness_distance.total_cmp(&optimal_fitness_distance); - - // TODO: remove fallback once MSRV has been bumped to 1.62 or later: - #[cfg(not(feature = "total_cmp"))] - let ordering = { - // See http://doc.rust-lang.org/1.65.0/core/primitive.f64.html#method.total_cmp: - - let mut left = fitness_distance.to_bits() as i64; - let mut right = optimal_fitness_distance.to_bits() as i64; - - left ^= (((left >> 63) as u64) >> 1) as i64; - right ^= (((right >> 63) as u64) >> 1) as i64; - - left.cmp(&right) - }; - - if ordering == Ordering::Less { - // Candidate is new optimal, so drop current selection: - optimal_candidates.clear(); - optimal_fitness_distance = fitness_distance; - } - - if ordering != Ordering::Greater { - // Candidate is optimal, so add to selection: - optimal_candidates.push(candidate); - } - } - - optimal_candidates -} - -#[cfg(test)] -mod tests { - use super::select_optimal_candidates; - use crate::MediaTrackSettings; - - #[test] - fn monotonic_increasing() { - let settings = [ - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - ]; - - let candidates = vec![ - (&settings[0], 0.1), - (&settings[1], 0.1), - (&settings[2], 0.2), - (&settings[3], 0.3), - ]; - - let actual = select_optimal_candidates(candidates); - - let expected = vec![&settings[0], &settings[1]]; - - assert_eq!(actual, expected); - } - - #[test] - fn monotonic_decreasing() { - let settings = [ - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - ]; - - let candidates = vec![ - (&settings[0], 0.3), - (&settings[1], 0.2), - (&settings[2], 0.1), - (&settings[3], 0.1), - ]; - - let actual = select_optimal_candidates(candidates); - - let expected = vec![&settings[2], &settings[3]]; - - assert_eq!(actual, expected); - } - - #[test] - fn alternating() { - let settings = [ - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - MediaTrackSettings::default(), - ]; - - let candidates = vec![ - (&settings[0], 0.2), - (&settings[1], 0.1), - (&settings[2], 0.2), - (&settings[3], 0.1), - ]; - - let actual = select_optimal_candidates(candidates); - - let expected = vec![&settings[1], &settings[3]]; - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/algorithms/select_settings/tests.rs b/constraints/src/algorithms/select_settings/tests.rs deleted file mode 100644 index 5745662f4..000000000 --- a/constraints/src/algorithms/select_settings/tests.rs +++ /dev/null @@ -1,778 +0,0 @@ -use std::iter::FromIterator; - -use lazy_static::lazy_static; - -use super::DeviceInformationExposureMode; -use crate::algorithms::{select_settings_candidates, SelectSettingsError}; -use crate::errors::OverconstrainedError; -use crate::property::all::name::*; -use crate::property::all::names as all_properties; -use crate::{ - AdvancedMediaTrackConstraints, FacingMode, MandatoryMediaTrackConstraints, - MediaTrackConstraints, MediaTrackSettings, MediaTrackSupportedConstraints, ResizeMode, - ResolvedAdvancedMediaTrackConstraints, ResolvedMandatoryMediaTrackConstraints, - ResolvedMediaTrackConstraint, ResolvedMediaTrackConstraints, ResolvedValueConstraint, - ResolvedValueRangeConstraint, ResolvedValueSequenceConstraint, SanitizedMediaTrackConstraints, -}; - -lazy_static! { - static ref VIDEO_IDEAL: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 60.0.into()), - (&WIDTH, 1920.into()), - (&HEIGHT, 1080.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]); - static ref VIDEO_480P: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 240.into()), - (&WIDTH, 720.into()), - (&HEIGHT, 480.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]); - static ref VIDEO_720P: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 120.into()), - (&WIDTH, 1280.into()), - (&HEIGHT, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]); - static ref VIDEO_1080P: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 60.into()), - (&WIDTH, 1920.into()), - (&HEIGHT, 1080.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]); - static ref VIDEO_1440P: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 30.into()), - (&WIDTH, 2560.into()), - (&HEIGHT, 1440.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]); - static ref VIDEO_2160P: MediaTrackSettings = MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&ASPECT_RATIO, 0.5625.into()), - (&FACING_MODE, FacingMode::user().into()), - (&FRAME_RATE, 15.into()), - (&WIDTH, 3840.into()), - (&HEIGHT, 2160.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]); -} - -fn default_possible_settings() -> Vec { - vec![ - VIDEO_480P.clone(), - VIDEO_720P.clone(), - VIDEO_1080P.clone(), - VIDEO_1440P.clone(), - VIDEO_2160P.clone(), - ] -} - -fn default_supported_constraints() -> MediaTrackSupportedConstraints { - MediaTrackSupportedConstraints::from_iter(all_properties().into_iter().cloned()) -} - -fn test_overconstrained( - possible_settings: &[MediaTrackSettings], - mandatory_constraints: ResolvedMandatoryMediaTrackConstraints, - exposure_mode: DeviceInformationExposureMode, -) -> OverconstrainedError { - let constraints = ResolvedMediaTrackConstraints { - mandatory: mandatory_constraints, - advanced: ResolvedAdvancedMediaTrackConstraints::default(), - } - .to_sanitized(&default_supported_constraints()); - - let result = select_settings_candidates(possible_settings.iter(), &constraints, exposure_mode); - - let actual = result.err().unwrap(); - - let SelectSettingsError::Overconstrained(overconstrained_error) = actual; - - overconstrained_error -} - -fn test_constrained( - possible_settings: &[MediaTrackSettings], - mandatory_constraints: ResolvedMandatoryMediaTrackConstraints, - advanced_constraints: ResolvedAdvancedMediaTrackConstraints, -) -> Vec<&MediaTrackSettings> { - let constraints = ResolvedMediaTrackConstraints { - mandatory: mandatory_constraints, - advanced: advanced_constraints, - } - .to_sanitized(&default_supported_constraints()); - - let result = select_settings_candidates( - possible_settings.iter(), - &constraints, - DeviceInformationExposureMode::Exposed, - ); - - result.unwrap() -} - -mod unconstrained { - use super::*; - - fn default_constraints() -> MediaTrackConstraints { - MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::default(), - advanced: AdvancedMediaTrackConstraints::default(), - } - } - - fn default_resolved_constraints() -> ResolvedMediaTrackConstraints { - default_constraints().into_resolved() - } - - fn default_sanitized_constraints() -> SanitizedMediaTrackConstraints { - default_resolved_constraints().into_sanitized(&default_supported_constraints()) - } - - #[test] - fn pass_through() { - let possible_settings = default_possible_settings(); - let sanitized_constraints = default_sanitized_constraints(); - - let actual = select_settings_candidates( - &possible_settings[..], - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - let expected: Vec<_> = possible_settings.iter().collect(); - - assert_eq!(actual, expected); - } -} - -mod overconstrained { - use super::*; - use crate::MediaTrackProperty; - - #[test] - fn protected() { - let error = test_overconstrained( - &default_possible_settings(), - ResolvedMandatoryMediaTrackConstraints::from_iter([( - GROUP_ID.clone(), - ResolvedValueConstraint::default() - .exact("missing-group".to_owned()) - .into(), - )]), - DeviceInformationExposureMode::Protected, - ); - - assert_eq!(error.constraint, MediaTrackProperty::from("")); - assert_eq!(error.message, None); - } - - mod exposed { - use super::*; - - #[test] - fn missing() { - let error = test_overconstrained( - &default_possible_settings(), - ResolvedMandatoryMediaTrackConstraints::from_iter([( - GROUP_ID.clone(), - ResolvedValueConstraint::default() - .exact("missing-group".to_owned()) - .into(), - )]), - DeviceInformationExposureMode::Exposed, - ); - - let constraint = &error.constraint; - let err_message = error.message.as_ref().expect("Error message."); - - assert_eq!(constraint, &GROUP_ID); - assert_eq!( - err_message, - "Setting was missing (does not satisfy (x == \"missing-group\"))." - ); - } - - #[test] - fn mismatch() { - let error = test_overconstrained( - &default_possible_settings(), - ResolvedMandatoryMediaTrackConstraints::from_iter([( - DEVICE_ID.clone(), - ResolvedValueConstraint::default() - .exact("mismatched-device".to_owned()) - .into(), - )]), - DeviceInformationExposureMode::Exposed, - ); - - let constraint = &error.constraint; - let err_message = error.message.as_ref().expect("Error message."); - - assert_eq!(constraint, &DEVICE_ID); - assert_eq!( - err_message, - "Setting was a mismatch ([\"1080p\", \"1440p\", \"2160p\", \"480p\", \"720p\"] do not satisfy (x == \"mismatched-device\"))." - ); - } - - #[test] - fn too_small() { - let error = test_overconstrained( - &default_possible_settings(), - ResolvedMandatoryMediaTrackConstraints::from_iter([( - FRAME_RATE.clone(), - ResolvedValueRangeConstraint::default().min(1000).into(), - )]), - DeviceInformationExposureMode::Exposed, - ); - - let constraint = &error.constraint; - let err_message = error.message.as_ref().expect("Error message."); - - assert_eq!(constraint, &FRAME_RATE); - assert_eq!( - err_message, - "Setting was too small ([120, 15, 240, 30, 60] do not satisfy (1000 <= x))." - ); - } - - #[test] - fn too_large() { - let error = test_overconstrained( - &default_possible_settings(), - ResolvedMandatoryMediaTrackConstraints::from_iter([( - FRAME_RATE.clone(), - ResolvedValueRangeConstraint::default().max(10).into(), - )]), - DeviceInformationExposureMode::Exposed, - ); - - let constraint = &error.constraint; - let err_message = error.message.as_ref().expect("Error message."); - - assert_eq!(constraint, &FRAME_RATE); - assert_eq!( - err_message, - "Setting was too large ([120, 15, 240, 30, 60] do not satisfy (x <= 10))." - ); - } - } -} - -mod constrained { - use super::*; - - #[test] - fn specific_device_id() { - let possible_settings = default_possible_settings(); - - for target_settings in possible_settings.iter() { - let setting = match target_settings.get(&DEVICE_ID) { - Some(setting) => setting, - None => continue, - }; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - DEVICE_ID.clone(), - ResolvedMediaTrackConstraint::exact_from(setting.clone()), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![target_settings]; - - assert_eq!(actual, expected); - } - } - - mod exact { - use super::*; - - #[test] - fn value() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "a".into()), - (&GROUP_ID, "group-0".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "b".into()), - (&GROUP_ID, "group-1".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "c".into()), - (&GROUP_ID, "group-2".into()), - ]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &GROUP_ID, - ResolvedValueConstraint::default() - .exact("group-1".to_owned()) - .into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - - #[test] - fn value_range() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([(&DEVICE_ID, "a".into()), (&FRAME_RATE, 15.into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "b".into()), (&FRAME_RATE, 30.into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "c".into()), (&FRAME_RATE, 60.into())]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint::default().exact(30).into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - - #[test] - fn value_sequence() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "a".into()), - (&GROUP_ID, "group-0".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "b".into()), - (&GROUP_ID, "group-1".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "c".into()), - (&GROUP_ID, "group-2".into()), - ]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &GROUP_ID, - ResolvedValueSequenceConstraint::default() - .exact(vec!["group-1".to_owned(), "group-3".to_owned()]) - .into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - } - - mod ideal { - use super::*; - - #[test] - fn value() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "a".into()), - (&GROUP_ID, "group-0".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "b".into()), - (&GROUP_ID, "group-1".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "c".into()), - (&GROUP_ID, "group-2".into()), - ]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &GROUP_ID, - ResolvedValueConstraint::default() - .ideal("group-1".to_owned()) - .into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - - #[test] - fn value_range() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([(&DEVICE_ID, "a".into()), (&FRAME_RATE, 15.into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "b".into()), (&FRAME_RATE, 30.into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "c".into()), (&FRAME_RATE, 60.into())]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint::default().ideal(32).into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - - #[test] - fn value_sequence() { - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "a".into()), - (&GROUP_ID, "group-0".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "b".into()), - (&GROUP_ID, "group-1".into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "c".into()), - (&GROUP_ID, "group-2".into()), - ]), - ]; - - let actual = test_constrained( - &possible_settings, - ResolvedMandatoryMediaTrackConstraints::from_iter([( - &GROUP_ID, - ResolvedValueSequenceConstraint::default() - .ideal(vec!["group-1".to_owned(), "group-3".to_owned()]) - .into(), - )]), - ResolvedAdvancedMediaTrackConstraints::default(), - ); - - let expected = vec![&possible_settings[1]]; - - assert_eq!(actual, expected); - } - } -} - -// ``` -// ┌ -// mandatory constraints: ┤ ┄───────────────────────────────────────────┤ -// └ -// ┌ -// advanced constraints: ┤ ├─┤ ├────────────────────────────┄ -// └ -// ┌ -// possible settings: ┤ ●─────────────●──────────────●──────────────●─────────────● -// └ 480p 720p 1080p 1440p 2160p -// └───────┬──────┘ -// selected settings: ─────────────────────────────────────────┘ -// ``` -mod smoke { - use super::*; - use crate::{MediaTrackConstraintSet, ValueConstraint, ValueRangeConstraint}; - - #[test] - fn native() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let possible_settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&HEIGHT, 480.into()), - (&WIDTH, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&HEIGHT, 720.into()), - (&WIDTH, 1280.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&HEIGHT, 1080.into()), - (&WIDTH, 1920.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&HEIGHT, 1440.into()), - (&WIDTH, 2560.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&HEIGHT, 2160.into()), - (&WIDTH, 3840.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - ]; - - let constraints = MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - ( - &WIDTH, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().max(2560), - ) - .into(), - ), - ( - &HEIGHT, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().max(1440), - ) - .into(), - ), - // Unsupported constraint, which should thus get ignored: - ( - &FRAME_RATE, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().exact(30.0), - ) - .into(), - ), - // Ideal resize-mode: - ( - &RESIZE_MODE, - ValueConstraint::Bare(ResizeMode::none()).into(), - ), - ]), - advanced: AdvancedMediaTrackConstraints::from_iter([ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - MediaTrackConstraintSet::from_iter([( - &HEIGHT, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().exact(800), - ) - .into(), - )]), - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - MediaTrackConstraintSet::from_iter([( - &RESIZE_MODE, - ValueConstraint::Constraint( - ResolvedValueConstraint::default().exact(ResizeMode::none()), - ) - .into(), - )]), - ]), - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.to_sanitized(&supported_constraints); - - let actual = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - let expected = vec![&possible_settings[2], &possible_settings[3]]; - - assert_eq!(actual, expected); - } - - #[test] - fn macros() { - use crate::macros::*; - - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let possible_settings = vec![ - settings![ - &DEVICE_ID => "480p", - &HEIGHT => 480, - &WIDTH => 720, - &RESIZE_MODE => ResizeMode::crop_and_scale(), - ], - settings![ - &DEVICE_ID => "720p", - &HEIGHT => 720, - &WIDTH => 1280, - &RESIZE_MODE => ResizeMode::crop_and_scale(), - ], - settings![ - &DEVICE_ID => "1080p", - &HEIGHT => 1080, - &WIDTH => 1920, - &RESIZE_MODE => ResizeMode::none(), - ], - settings![ - &DEVICE_ID => "1440p", - &HEIGHT => 1440, - &WIDTH => 2560, - &RESIZE_MODE => ResizeMode::none(), - ], - settings![ - &DEVICE_ID => "2160p", - &HEIGHT => 2160, - &WIDTH => 3840, - &RESIZE_MODE => ResizeMode::none(), - ], - ]; - - let constraints = constraints! { - mandatory: { - &WIDTH => value_range_constraint!{ - max: 2560 - }, - &HEIGHT => value_range_constraint!{ - max: 1440 - }, - // Unsupported constraint, which should thus get ignored: - &FRAME_RATE => value_range_constraint!{ - exact: 30.0 - }, - }, - advanced: [ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - { - &HEIGHT => value_range_constraint!{ - exact: 800 - } - }, - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - { - &RESIZE_MODE => value_constraint!{ - exact: ResizeMode::none() - } - }, - ] - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.to_sanitized(&supported_constraints); - - let actual = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - let expected = vec![&possible_settings[2], &possible_settings[3]]; - - assert_eq!(actual, expected); - } - - #[cfg(feature = "serde")] - #[test] - fn json() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - // Deserialize possible settings from JSON: - let possible_settings: Vec = { - let json = serde_json::json!([ - { "deviceId": "480p", "width": 720, "height": 480, "resizeMode": "crop-and-scale" }, - { "deviceId": "720p", "width": 1280, "height": 720, "resizeMode": "crop-and-scale" }, - { "deviceId": "1080p", "width": 1920, "height": 1080, "resizeMode": "none" }, - { "deviceId": "1440p", "width": 2560, "height": 1440, "resizeMode": "none" }, - { "deviceId": "2160p", "width": 3840, "height": 2160, "resizeMode": "none" }, - ]); - serde_json::from_value(json).unwrap() - }; - - // Deserialize constraints from JSON: - let constraints: MediaTrackConstraints = { - let json = serde_json::json!({ - "width": { - "max": 2560, - }, - "height": { - "max": 1440, - }, - // Unsupported constraint, which should thus get ignored: - "frameRate": { - "exact": 30.0 - }, - // Ideal resize-mode: - "resizeMode": "none", - "advanced": [ - // The first advanced constraint set of "exact 800p" does not match - // any candidate and should thus get ignored by the algorithm: - { "height": 800 }, - // The second advanced constraint set of "no resizing" does match - // candidates and should thus be applied by the algorithm: - { "resizeMode": "none" }, - ] - }); - serde_json::from_value(json).unwrap() - }; - - // Resolve bare values to proper constraints: - let resolved_constraints = constraints.into_resolved(); - - // Sanitize constraints, removing empty and unsupported constraints: - let sanitized_constraints = resolved_constraints.into_sanitized(&supported_constraints); - - let actual = select_settings_candidates( - &possible_settings, - &sanitized_constraints, - DeviceInformationExposureMode::Exposed, - ) - .unwrap(); - - let expected = vec![&possible_settings[2], &possible_settings[3]]; - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/algorithms/select_settings/tie_breaking.rs b/constraints/src/algorithms/select_settings/tie_breaking.rs deleted file mode 100644 index 52e5c2ac6..000000000 --- a/constraints/src/algorithms/select_settings/tie_breaking.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::iter::FromIterator; - -use ordered_float::NotNan; - -use crate::algorithms::FitnessDistance; -use crate::{ - MandatoryMediaTrackConstraints, MediaTrackSettings, MediaTrackSupportedConstraints, - SanitizedMandatoryMediaTrackConstraints, -}; - -/// A tie-breaking policy used for selecting a single preferred candidate -/// from a set list of equally optimal setting candidates. -pub trait TieBreakingPolicy { - /// Selects a preferred candidate from a non-empty selection of optimal candidates. - /// - /// As specified in step 6 of the `SelectSettings` algorithm: - /// - /// - /// > Select one settings dictionary from candidates, and return it as the result - /// > of the SelectSettings algorithm. The User Agent MUST use one with the - /// > smallest fitness distance, as calculated in step 3. - /// > If more than one settings dictionary have the smallest fitness distance, - /// > the User Agent chooses one of them based on system default property values - /// > and User Agent default property values. - fn select_candidate<'a, I>(&self, candidates: I) -> &'a MediaTrackSettings - where - I: IntoIterator; -} - -/// A naïve tie-breaking policy that just picks the first settings item it encounters. -pub struct FirstPolicy; - -impl FirstPolicy { - /// Creates a new policy. - pub fn new() -> Self { - Self - } -} - -impl Default for FirstPolicy { - fn default() -> Self { - Self::new() - } -} - -impl TieBreakingPolicy for FirstPolicy { - fn select_candidate<'a, I>(&self, candidates: I) -> &'a MediaTrackSettings - where - I: IntoIterator, - { - // Safety: We know that `candidates is non-empty: - candidates - .into_iter() - .next() - .expect("The `candidates` iterator should have produced at least one item.") - } -} - -/// A tie-breaking policy that picks the settings item that's closest to the specified ideal settings. -pub struct ClosestToIdealPolicy { - sanitized_constraints: SanitizedMandatoryMediaTrackConstraints, -} - -impl ClosestToIdealPolicy { - /// Creates a new policy from the given ideal settings and supported constraints. - pub fn new( - ideal_settings: MediaTrackSettings, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> Self { - let sanitized_constraints = MandatoryMediaTrackConstraints::from_iter( - ideal_settings - .into_iter() - .map(|(property, setting)| (property, setting.into())), - ) - .into_resolved() - .into_sanitized(supported_constraints); - - Self { - sanitized_constraints, - } - } -} - -impl TieBreakingPolicy for ClosestToIdealPolicy { - fn select_candidate<'b, I>(&self, candidates: I) -> &'b MediaTrackSettings - where - I: IntoIterator, - { - candidates - .into_iter() - .min_by_key(|settings| { - let fitness_distance = self - .sanitized_constraints - .fitness_distance(settings) - .expect("Fitness distance should be positive."); - NotNan::new(fitness_distance).expect("Expected non-NaN fitness distance.") - }) - .expect("The `candidates` iterator should have produced at least one item.") - } -} - -#[cfg(test)] -mod tests { - use std::iter::FromIterator; - - use super::*; - use crate::property::all::name::*; - use crate::{MediaTrackSettings, MediaTrackSupportedConstraints, ResizeMode}; - - #[test] - fn first() { - let settings = vec![ - MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-0".into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-1".into())]), - MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-2".into())]), - ]; - - let policy = FirstPolicy; - - let actual = policy.select_candidate(&settings); - - let expected = &settings[0]; - - assert_eq!(actual, expected); - } - - #[test] - fn closest_to_ideal() { - let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![ - &DEVICE_ID, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ]); - - let settings = vec![ - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "480p".into()), - (&HEIGHT, 480.into()), - (&WIDTH, 720.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "720p".into()), - (&HEIGHT, 720.into()), - (&WIDTH, 1280.into()), - (&RESIZE_MODE, ResizeMode::crop_and_scale().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1080p".into()), - (&HEIGHT, 1080.into()), - (&WIDTH, 1920.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "1440p".into()), - (&HEIGHT, 1440.into()), - (&WIDTH, 2560.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - MediaTrackSettings::from_iter([ - (&DEVICE_ID, "2160p".into()), - (&HEIGHT, 2160.into()), - (&WIDTH, 3840.into()), - (&RESIZE_MODE, ResizeMode::none().into()), - ]), - ]; - - let ideal_settings = vec![ - MediaTrackSettings::from_iter([(&HEIGHT, 450.into()), (&WIDTH, 700.into())]), - MediaTrackSettings::from_iter([(&HEIGHT, 700.into()), (&WIDTH, 1250.into())]), - MediaTrackSettings::from_iter([(&HEIGHT, 1000.into()), (&WIDTH, 2000.into())]), - MediaTrackSettings::from_iter([(&HEIGHT, 1500.into()), (&WIDTH, 2500.into())]), - MediaTrackSettings::from_iter([(&HEIGHT, 2000.into()), (&WIDTH, 3750.into())]), - ]; - - for (index, ideal) in ideal_settings.iter().enumerate() { - let policy = ClosestToIdealPolicy::new(ideal.clone(), &supported_constraints); - - let actual = policy.select_candidate(&settings); - - let expected = &settings[index]; - - assert_eq!(actual, expected); - } - } -} diff --git a/constraints/src/capabilities.rs b/constraints/src/capabilities.rs deleted file mode 100644 index b0b0cdc62..000000000 --- a/constraints/src/capabilities.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod stream; -mod track; - -pub(crate) use self::stream::MediaStreamCapabilities; -pub use self::track::MediaTrackCapabilities; diff --git a/constraints/src/capabilities/stream.rs b/constraints/src/capabilities/stream.rs deleted file mode 100644 index cc7200099..000000000 --- a/constraints/src/capabilities/stream.rs +++ /dev/null @@ -1,58 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::MediaTrackCapabilities; - -/// The capabilities of a [`MediaStream`][media_stream] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastream -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub(crate) struct MediaStreamCapabilities { - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub audio: Option, - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub video: Option, -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaStreamCapabilities; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject { - audio: Some(MediaTrackCapabilities::default()), - video: None, - }; - let json = serde_json::json!({ - "audio": {} - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/capabilities/track.rs b/constraints/src/capabilities/track.rs deleted file mode 100644 index 6fb7f8ac8..000000000 --- a/constraints/src/capabilities/track.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::collections::HashMap; -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::{MediaTrackCapability, MediaTrackProperty}; - -/// The capabilities of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaTrackCapabilities`][media_track_capabilities] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// The W3C spec defines `MediaTrackSettings` in terma of a dictionary, -/// which per the [WebIDL spec][webidl_spec] is an ordered map (e.g. `IndexMap`). -/// Since the spec however does not make use of the order of items -/// in the map we use a simple `HashMap`. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_capabilities]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -/// [webidl_spec]: https://webidl.spec.whatwg.org/#idl-dictionaries -#[derive(Debug, Clone, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct MediaTrackCapabilities(HashMap); - -impl MediaTrackCapabilities { - /// Creates a capabilities value from its inner hashmap. - pub fn new(capabilities: HashMap) -> Self { - Self(capabilities) - } - - /// Consumes the value, returning its inner hashmap. - pub fn into_inner(self) -> HashMap { - self.0 - } -} - -impl Deref for MediaTrackCapabilities { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MediaTrackCapabilities { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator<(T, MediaTrackCapability)> for MediaTrackCapabilities -where - T: Into, -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - Self::new(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) - } -} - -impl IntoIterator for MediaTrackCapabilities { - type Item = (MediaTrackProperty, MediaTrackCapability); - type IntoIter = std::collections::hash_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::property::all::name::*; - - type Subject = MediaTrackCapabilities; - - #[test] - fn into_inner() { - let hash_map = HashMap::from_iter([ - (DEVICE_ID.clone(), "device-id".into()), - (AUTO_GAIN_CONTROL.clone(), true.into()), - (CHANNEL_COUNT.clone(), (12..=34).into()), - (LATENCY.clone(), (1.2..=3.4).into()), - ]); - - let subject = Subject::new(hash_map.clone()); - - let actual = subject.into_inner(); - - let expected = hash_map; - - assert_eq!(actual, expected); - } - - #[test] - fn into_iter() { - let hash_map = HashMap::from_iter([ - (DEVICE_ID.clone(), "device-id".into()), - (AUTO_GAIN_CONTROL.clone(), true.into()), - (CHANNEL_COUNT.clone(), (12..=34).into()), - (LATENCY.clone(), (1.2..=3.4).into()), - ]); - - let subject = Subject::new(hash_map.clone()); - - let actual: HashMap<_, _> = subject.into_iter().collect(); - - let expected = hash_map; - - assert_eq!(actual, expected); - } - - #[test] - fn deref_and_deref_mut() { - let mut subject = Subject::default(); - - // Deref mut: - subject.insert(DEVICE_ID.clone(), "device-id".into()); - - // Deref: - assert!(subject.contains_key(&DEVICE_ID)); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - use crate::property::all::name::*; - - type Subject = MediaTrackCapabilities; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, (12..=34).into()), - (&LATENCY, (1.2..=3.4).into()), - ]); - let json = serde_json::json!({ - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": { "min": 12, "max": 34 }, - "latency": { "min": 1.2, "max": 3.4 }, - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/capability.rs b/constraints/src/capability.rs deleted file mode 100644 index 28621b643..000000000 --- a/constraints/src/capability.rs +++ /dev/null @@ -1,219 +0,0 @@ -mod value; -mod value_range; -mod value_sequence; - -use std::ops::RangeInclusive; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -pub use self::value::MediaTrackValueCapability; -pub use self::value_range::MediaTrackValueRangeCapability; -pub use self::value_sequence::MediaTrackValueSequenceCapability; - -/// A single [capability][media_track_capabilities] value of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_capabilities]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum MediaTrackCapability { - // IMPORTANT: - // `BoolSequence` must be ordered before `Bool(…)` in order for - // `serde` to decode the correct variant. - /// A sequence of boolean-valued media track capabilities. - BoolSequence(MediaTrackValueSequenceCapability), - /// A single boolean-valued media track capability. - Bool(MediaTrackValueCapability), - // `IntegerRange` must be ordered before `FloatRange(…)` in order for - // `serde` to decode the correct variant. - /// A range of integer-valued media track capabilities. - IntegerRange(MediaTrackValueRangeCapability), - /// A range of floating-point-valued media track capabilities. - FloatRange(MediaTrackValueRangeCapability), - // IMPORTANT: - // `StringSequence` must be ordered before `String(…)` in order for - // `serde` to decode the correct variant. - /// A sequence of string-valued media track capabilities. - StringSequence(MediaTrackValueSequenceCapability), - /// A single string-valued media track capability. - String(MediaTrackValueCapability), -} - -impl From for MediaTrackCapability { - fn from(capability: bool) -> Self { - Self::Bool(capability.into()) - } -} - -impl From> for MediaTrackCapability { - fn from(capability: Vec) -> Self { - Self::BoolSequence(capability.into()) - } -} - -impl From> for MediaTrackCapability { - fn from(capability: RangeInclusive) -> Self { - Self::IntegerRange(capability.into()) - } -} - -impl From> for MediaTrackCapability { - fn from(capability: RangeInclusive) -> Self { - Self::FloatRange(capability.into()) - } -} - -impl From for MediaTrackCapability { - fn from(capability: String) -> Self { - Self::String(capability.into()) - } -} - -impl<'a> From<&'a str> for MediaTrackCapability { - fn from(capability: &'a str) -> Self { - let capability: String = capability.to_owned(); - Self::from(capability) - } -} - -impl From> for MediaTrackCapability { - fn from(capability: Vec) -> Self { - Self::StringSequence(capability.into()) - } -} - -impl From> for MediaTrackCapability { - fn from(capability: Vec<&str>) -> Self { - let capability: Vec = capability.into_iter().map(|c| c.to_owned()).collect(); - Self::from(capability) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackCapability; - - mod from { - use super::*; - - #[test] - fn bool_sequence() { - let actual = Subject::from(vec![false, true]); - let expected = Subject::BoolSequence(vec![false, true].into()); - - assert_eq!(actual, expected); - } - - #[test] - fn bool() { - let actual = Subject::from(true); - let expected = Subject::Bool(true.into()); - - assert_eq!(actual, expected); - } - - #[test] - fn integer_range() { - let actual = Subject::from(12..=34); - let expected = Subject::IntegerRange((12..=34).into()); - - assert_eq!(actual, expected); - } - - #[test] - fn float() { - let actual = Subject::from(1.2..=3.4); - let expected = Subject::FloatRange((1.2..=3.4).into()); - - assert_eq!(actual, expected); - } - - #[test] - fn string_sequence() { - let actual = Subject::from(vec!["foo".to_owned(), "bar".to_owned()]); - let expected = Subject::StringSequence(vec!["foo".to_owned(), "bar".to_owned()].into()); - - assert_eq!(actual, expected); - } - - #[test] - fn string() { - let actual = Subject::from("foo".to_owned()); - let expected = Subject::String("foo".to_owned().into()); - - assert_eq!(actual, expected); - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackCapability; - - #[test] - fn bool_sequence() { - let subject = Subject::BoolSequence(vec![false, true].into()); - let json = serde_json::json!([false, true]); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bool() { - let subject = Subject::Bool(true.into()); - let json = serde_json::json!(true); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn integer_range() { - let subject = Subject::IntegerRange((12..=34).into()); - let json = serde_json::json!({ - "min": 12, - "max": 34, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn float() { - let subject = Subject::FloatRange((1.2..=3.4).into()); - let json = serde_json::json!({ - "min": 1.2, - "max": 3.4, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string_sequence() { - let subject = Subject::StringSequence(vec!["foo".to_owned(), "bar".to_owned()].into()); - let json = serde_json::json!(["foo", "bar"]); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string() { - let subject = Subject::String("foo".to_owned().into()); - let json = serde_json::json!("foo"); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/capability/value.rs b/constraints/src/capability/value.rs deleted file mode 100644 index a55c0a32e..000000000 --- a/constraints/src/capability/value.rs +++ /dev/null @@ -1,74 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A capability specifying a single supported value. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `MediaTrackValueCapability` type aims to be a -/// generalization over multiple types in the W3C spec: -/// -/// | Rust | W3C | -/// | ----------------------------------- | ------------------------- | -/// | `MediaTrackValueCapability` | [`DOMString`][dom_string] | -/// -/// [dom_string]: https://webidl.spec.whatwg.org/#idl-DOMString -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct MediaTrackValueCapability { - pub value: T, -} - -impl From for MediaTrackValueCapability { - fn from(value: T) -> Self { - Self { value } - } -} - -impl From<&str> for MediaTrackValueCapability { - fn from(value: &str) -> Self { - Self { - value: value.to_owned(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackValueCapability; - - #[test] - fn from_str() { - let subject = Subject::from("string"); - - let actual = subject.value.as_str(); - let expected = "string"; - - assert_eq!(actual, expected); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackValueCapability; - - #[test] - fn customized() { - let subject = Subject { - value: "string".to_owned(), - }; - let json = serde_json::json!("string"); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/capability/value_range.rs b/constraints/src/capability/value_range.rs deleted file mode 100644 index 786b9b7c8..000000000 --- a/constraints/src/capability/value_range.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::ops::{RangeFrom, RangeInclusive, RangeToInclusive}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A capability specifying a range of supported values. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `MediaTrackValueRangeCapability` type aims to be a -/// generalization over multiple types in the W3C spec: -/// -/// | Rust | W3C | -/// | ------------------------------------- | ----------------------------- | -/// | `MediaTrackValueRangeCapability` | [`ULongRange`][ulong_range] | -/// | `MediaTrackValueRangeCapability` | [`DoubleRange`][double_range] | -/// -/// [double_range]: https://www.w3.org/TR/mediacapture-streams/#dom-doublerange -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -/// [ulong_range]: https://www.w3.org/TR/mediacapture-streams/#dom-ulongrange -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct MediaTrackValueRangeCapability { - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub min: Option, - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub max: Option, -} - -impl Default for MediaTrackValueRangeCapability { - fn default() -> Self { - Self { - min: Default::default(), - max: Default::default(), - } - } -} - -impl From> for MediaTrackValueRangeCapability { - fn from(range: RangeInclusive) -> Self { - let (min, max) = range.into_inner(); - Self { - min: Some(min), - max: Some(max), - } - } -} - -impl From> for MediaTrackValueRangeCapability { - fn from(range: RangeFrom) -> Self { - Self { - min: Some(range.start), - max: None, - } - } -} - -impl From> for MediaTrackValueRangeCapability { - fn from(range: RangeToInclusive) -> Self { - Self { - min: None, - max: Some(range.end), - } - } -} - -impl MediaTrackValueRangeCapability { - pub fn contains(&self, value: &T) -> bool - where - T: PartialOrd, - { - // FIXME(regexident): replace with if-let-chain, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/53667 - if let Some(ref min) = self.min { - if min > value { - return false; - } - } - // FIXME(regexident): replace with if-let-chain, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/53667 - if let Some(ref max) = self.max { - if max < value { - return false; - } - } - true - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackValueRangeCapability; - - #[test] - fn default() { - let subject = Subject::default(); - - assert_eq!(subject.min, None); - assert_eq!(subject.max, None); - } - - mod from { - use super::*; - - #[test] - fn range_inclusive() { - let subject = Subject::from(1..=5); - - assert_eq!(subject.min, Some(1)); - assert_eq!(subject.max, Some(5)); - } - - #[test] - fn range_from() { - let subject = Subject::from(1..); - - assert_eq!(subject.min, Some(1)); - assert_eq!(subject.max, None); - } - - #[test] - fn range_to_inclusive() { - let subject = Subject::from(..=5); - - assert_eq!(subject.min, None); - assert_eq!(subject.max, Some(5)); - } - } - - mod contains { - use super::*; - - #[test] - fn default() { - let subject = Subject::default(); - - assert!(subject.contains(&0)); - assert!(subject.contains(&1)); - assert!(subject.contains(&5)); - assert!(subject.contains(&6)); - } - - #[test] - fn from_range_inclusive() { - let subject = Subject::from(1..=5); - - assert!(!subject.contains(&0)); - assert!(subject.contains(&1)); - assert!(subject.contains(&5)); - assert!(!subject.contains(&6)); - } - - #[test] - fn from_range_from() { - let subject = Subject::from(1..); - - assert!(!subject.contains(&0)); - assert!(subject.contains(&1)); - assert!(subject.contains(&5)); - assert!(subject.contains(&6)); - } - - #[test] - fn from_range_to_inclusive() { - let subject = Subject::from(..=5); - - assert!(subject.contains(&0)); - assert!(subject.contains(&1)); - assert!(subject.contains(&5)); - assert!(!subject.contains(&6)); - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackValueRangeCapability; - - #[test] - fn customized() { - let subject = Subject { - min: Some(12), - max: Some(34), - }; - let json = serde_json::json!({ - "min": 12, - "max": 34, - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/capability/value_sequence.rs b/constraints/src/capability/value_sequence.rs deleted file mode 100644 index 0a090c499..000000000 --- a/constraints/src/capability/value_sequence.rs +++ /dev/null @@ -1,99 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A capability specifying a range of supported values. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`sequence`][sequence] from the W3C ["WebIDL"][webidl_spec] spec: -/// -/// | Rust | W3C | -/// | ----------------------------------------- | --------------------- | -/// | `MediaTrackValueSequenceCapability` | `sequence` | -/// | `MediaTrackValueSequenceCapability` | `sequence` | -/// -/// [sequence]: https://webidl.spec.whatwg.org/#idl-sequence -/// [webidl_spec]: https://webidl.spec.whatwg.org/ -#[derive(Default, Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct MediaTrackValueSequenceCapability { - pub values: Vec, -} - -impl From for MediaTrackValueSequenceCapability { - fn from(value: T) -> Self { - Self { - values: vec![value], - } - } -} - -impl From> for MediaTrackValueSequenceCapability { - fn from(values: Vec) -> Self { - Self { values } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackValueSequenceCapability; - - #[test] - fn default() { - let subject = Subject::default(); - - let actual = subject.values; - - let expected: Vec = vec![]; - - assert_eq!(actual, expected); - } - - mod from { - use super::*; - - #[test] - fn value() { - let subject = Subject::from("foo".to_owned()); - - let actual = subject.values; - - let expected: Vec = vec!["foo".to_owned()]; - - assert_eq!(actual, expected); - } - - #[test] - fn values() { - let subject = Subject::from(vec!["foo".to_owned(), "bar".to_owned()]); - - let actual = subject.values; - - let expected: Vec = vec!["foo".to_owned(), "bar".to_owned()]; - - assert_eq!(actual, expected); - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackValueSequenceCapability; - - #[test] - fn customized() { - let subject = Subject { - values: vec!["foo".to_owned(), "bar".to_owned()], - }; - let json = serde_json::json!(["foo", "bar"]); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/constraint.rs b/constraints/src/constraint.rs deleted file mode 100644 index 2ad4f10b4..000000000 --- a/constraints/src/constraint.rs +++ /dev/null @@ -1,789 +0,0 @@ -use std::ops::Deref; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -pub use self::value::{ResolvedValueConstraint, ValueConstraint}; -pub use self::value_range::{ResolvedValueRangeConstraint, ValueRangeConstraint}; -pub use self::value_sequence::{ResolvedValueSequenceConstraint, ValueSequenceConstraint}; -use crate::MediaTrackSetting; - -mod value; -mod value_range; -mod value_sequence; - -/// An empty [constraint][media_track_constraints] value for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// The purpose of this type is to reduce parsing ambiguity, since all constraint variant types -/// support serializing from an empty map, but an empty map isn't typed, really, -/// so parsing to a specifically typed constraint would be wrong, type-wise. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] -pub struct EmptyConstraint {} - -/// The strategy of a track [constraint][constraint]. -/// -/// [constraint]: https://www.w3.org/TR/mediacapture-streams/#dfn-constraint -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum MediaTrackConstraintResolutionStrategy { - /// Resolve bare values to `ideal` constraints. - BareToIdeal, - /// Resolve bare values to `exact` constraints. - BareToExact, -} - -/// A single [constraint][media_track_constraints] value for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum MediaTrackConstraint { - /// An empty constraint. - Empty(EmptyConstraint), - // `IntegerRange` must be ordered before `FloatRange(…)` in order for - // `serde` to decode the correct variant. - /// An integer-valued media track range constraint. - IntegerRange(ValueRangeConstraint), - /// An floating-point-valued media track range constraint. - FloatRange(ValueRangeConstraint), - // `Bool` must be ordered after `IntegerRange(…)`/`FloatRange(…)` in order for - // `serde` to decode the correct variant. - /// A single boolean-valued media track constraint. - Bool(ValueConstraint), - // `StringSequence` must be ordered before `String(…)` in order for - // `serde` to decode the correct variant. - /// A sequence of string-valued media track constraints. - StringSequence(ValueSequenceConstraint), - /// A single string-valued media track constraint. - String(ValueConstraint), -} - -impl Default for MediaTrackConstraint { - fn default() -> Self { - Self::Empty(EmptyConstraint {}) - } -} - -// Bool constraint: - -impl From for MediaTrackConstraint { - fn from(bare: bool) -> Self { - Self::Bool(bare.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ResolvedValueConstraint) -> Self { - Self::Bool(constraint.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ValueConstraint) -> Self { - Self::Bool(constraint) - } -} - -// Unsigned integer range constraint: - -impl From for MediaTrackConstraint { - fn from(bare: u64) -> Self { - Self::IntegerRange(bare.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ResolvedValueRangeConstraint) -> Self { - Self::IntegerRange(constraint.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ValueRangeConstraint) -> Self { - Self::IntegerRange(constraint) - } -} - -// Floating-point range constraint: - -impl From for MediaTrackConstraint { - fn from(bare: f64) -> Self { - Self::FloatRange(bare.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ResolvedValueRangeConstraint) -> Self { - Self::FloatRange(constraint.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ValueRangeConstraint) -> Self { - Self::FloatRange(constraint) - } -} - -// String sequence constraint: - -impl From> for MediaTrackConstraint { - fn from(bare: Vec) -> Self { - Self::StringSequence(bare.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(bare: Vec<&str>) -> Self { - let bare: Vec = bare.into_iter().map(|c| c.to_owned()).collect(); - Self::from(bare) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ResolvedValueSequenceConstraint) -> Self { - Self::StringSequence(constraint.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ValueSequenceConstraint) -> Self { - Self::StringSequence(constraint) - } -} - -// String constraint: - -impl From for MediaTrackConstraint { - fn from(bare: String) -> Self { - Self::String(bare.into()) - } -} - -impl<'a> From<&'a str> for MediaTrackConstraint { - fn from(bare: &'a str) -> Self { - let bare: String = bare.to_owned(); - Self::from(bare) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ResolvedValueConstraint) -> Self { - Self::String(constraint.into()) - } -} - -impl From> for MediaTrackConstraint { - fn from(constraint: ValueConstraint) -> Self { - Self::String(constraint) - } -} - -// Conversion from settings: - -impl From for MediaTrackConstraint { - fn from(settings: MediaTrackSetting) -> Self { - match settings { - MediaTrackSetting::Bool(value) => Self::Bool(value.into()), - MediaTrackSetting::Integer(value) => { - Self::IntegerRange((value.clamp(0, i64::MAX) as u64).into()) - } - MediaTrackSetting::Float(value) => Self::FloatRange(value.into()), - MediaTrackSetting::String(value) => Self::String(value.into()), - } - } -} - -impl MediaTrackConstraint { - /// Returns `true` if `self` is empty, otherwise `false`. - pub fn is_empty(&self) -> bool { - match self { - Self::Empty(_) => true, - Self::IntegerRange(constraint) => constraint.is_empty(), - Self::FloatRange(constraint) => constraint.is_empty(), - Self::Bool(constraint) => constraint.is_empty(), - Self::StringSequence(constraint) => constraint.is_empty(), - Self::String(constraint) => constraint.is_empty(), - } - } - - /// Returns a resolved representation of the constraint - /// with bare values resolved to fully-qualified constraints. - pub fn to_resolved( - &self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedMediaTrackConstraint { - self.clone().into_resolved(strategy) - } - - /// Consumes the constraint, returning a resolved representation of the - /// constraint with bare values resolved to fully-qualified constraints. - pub fn into_resolved( - self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedMediaTrackConstraint { - match self { - Self::Empty(constraint) => ResolvedMediaTrackConstraint::Empty(constraint), - Self::IntegerRange(constraint) => { - ResolvedMediaTrackConstraint::IntegerRange(constraint.into_resolved(strategy)) - } - Self::FloatRange(constraint) => { - ResolvedMediaTrackConstraint::FloatRange(constraint.into_resolved(strategy)) - } - Self::Bool(constraint) => { - ResolvedMediaTrackConstraint::Bool(constraint.into_resolved(strategy)) - } - Self::StringSequence(constraint) => { - ResolvedMediaTrackConstraint::StringSequence(constraint.into_resolved(strategy)) - } - Self::String(constraint) => { - ResolvedMediaTrackConstraint::String(constraint.into_resolved(strategy)) - } - } - } -} - -/// A single [constraint][media_track_constraints] value for a [`MediaStreamTrack`][media_stream_track] object -/// with its potential bare value either resolved to an `exact` or `ideal` constraint. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum ResolvedMediaTrackConstraint { - /// An empty constraint. - Empty(EmptyConstraint), - /// An integer-valued media track range constraint. - IntegerRange(ResolvedValueRangeConstraint), - /// An floating-point-valued media track range constraint. - FloatRange(ResolvedValueRangeConstraint), - /// A single boolean-valued media track constraint. - Bool(ResolvedValueConstraint), - /// A sequence of string-valued media track constraints. - StringSequence(ResolvedValueSequenceConstraint), - /// A single string-valued media track constraint. - String(ResolvedValueConstraint), -} - -impl Default for ResolvedMediaTrackConstraint { - fn default() -> Self { - Self::Empty(EmptyConstraint {}) - } -} - -impl std::fmt::Display for ResolvedMediaTrackConstraint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Empty(_constraint) => "".fmt(f), - Self::IntegerRange(constraint) => constraint.fmt(f), - Self::FloatRange(constraint) => constraint.fmt(f), - Self::Bool(constraint) => constraint.fmt(f), - Self::StringSequence(constraint) => constraint.fmt(f), - Self::String(constraint) => constraint.fmt(f), - } - } -} - -// Bool constraint: - -impl From> for ResolvedMediaTrackConstraint { - fn from(constraint: ResolvedValueConstraint) -> Self { - Self::Bool(constraint) - } -} - -// Unsigned integer range constraint: - -impl From> for ResolvedMediaTrackConstraint { - fn from(constraint: ResolvedValueRangeConstraint) -> Self { - Self::IntegerRange(constraint) - } -} - -// Floating-point range constraint: - -impl From> for ResolvedMediaTrackConstraint { - fn from(constraint: ResolvedValueRangeConstraint) -> Self { - Self::FloatRange(constraint) - } -} - -// String sequence constraint: - -impl From> for ResolvedMediaTrackConstraint { - fn from(constraint: ResolvedValueSequenceConstraint) -> Self { - Self::StringSequence(constraint) - } -} - -// String constraint: - -impl From> for ResolvedMediaTrackConstraint { - fn from(constraint: ResolvedValueConstraint) -> Self { - Self::String(constraint) - } -} - -impl ResolvedMediaTrackConstraint { - /// Creates a resolved media track constraint by resolving - /// bare values to exact constraints: `{ exact: bare }`. - pub fn exact_from(setting: MediaTrackSetting) -> Self { - MediaTrackConstraint::from(setting) - .into_resolved(MediaTrackConstraintResolutionStrategy::BareToExact) - } - - /// Creates a resolved media track constraint by resolving - /// bare values to ideal constraints: `{ ideal: bare }`. - pub fn ideal_from(setting: MediaTrackSetting) -> Self { - MediaTrackConstraint::from(setting) - .into_resolved(MediaTrackConstraintResolutionStrategy::BareToIdeal) - } - - /// Returns `true` if `self` is required, otherwise `false`. - pub fn is_required(&self) -> bool { - match self { - Self::Empty(_constraint) => false, - Self::IntegerRange(constraint) => constraint.is_required(), - Self::FloatRange(constraint) => constraint.is_required(), - Self::Bool(constraint) => constraint.is_required(), - Self::StringSequence(constraint) => constraint.is_required(), - Self::String(constraint) => constraint.is_required(), - } - } - - /// Returns `true` if `self` is empty, otherwise `false`. - pub fn is_empty(&self) -> bool { - match self { - Self::Empty(_constraint) => true, - Self::IntegerRange(constraint) => constraint.is_empty(), - Self::FloatRange(constraint) => constraint.is_empty(), - Self::Bool(constraint) => constraint.is_empty(), - Self::StringSequence(constraint) => constraint.is_empty(), - Self::String(constraint) => constraint.is_empty(), - } - } - - /// Returns a corresponding constraint containing only required values. - pub fn to_required_only(&self) -> Self { - self.clone().into_required_only() - } - - /// Consumes `self, returning a corresponding constraint - /// containing only required values. - pub fn into_required_only(self) -> Self { - match self { - Self::Empty(constraint) => Self::Empty(constraint), - Self::IntegerRange(constraint) => Self::IntegerRange(constraint.into_required_only()), - Self::FloatRange(constraint) => Self::FloatRange(constraint.into_required_only()), - Self::Bool(constraint) => Self::Bool(constraint.into_required_only()), - Self::StringSequence(constraint) => { - Self::StringSequence(constraint.into_required_only()) - } - Self::String(constraint) => Self::String(constraint.into_required_only()), - } - } - - /// Returns a corresponding sanitized constraint - /// if `self` is non-empty, otherwise `None`. - pub fn to_sanitized(&self) -> Option { - self.clone().into_sanitized() - } - - /// Consumes `self`, returning a corresponding sanitized constraint - /// if `self` is non-empty, otherwise `None`. - pub fn into_sanitized(self) -> Option { - if self.is_empty() { - return None; - } - - Some(SanitizedMediaTrackConstraint(self)) - } -} - -/// A single non-empty [constraint][media_track_constraints] value for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # Invariant -/// -/// The wrapped `ResolvedMediaTrackConstraint` MUST not be empty. -/// -/// To enforce this invariant the only way to create an instance of this type -/// is by calling `constraint.to_sanitized()`/`constraint.into_sanitized()` on -/// an instance of `ResolvedMediaTrackConstraint`, which returns `None` if `self` is empty. -/// -/// Further more `self.0` MUST NOT be exposed mutably, -/// as otherwise it could become empty via mutation. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -#[derive(Debug, Clone, PartialEq)] -pub struct SanitizedMediaTrackConstraint(ResolvedMediaTrackConstraint); - -impl Deref for SanitizedMediaTrackConstraint { - type Target = ResolvedMediaTrackConstraint; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl SanitizedMediaTrackConstraint { - /// Consumes `self` returning its inner resolved constraint. - pub fn into_inner(self) -> ResolvedMediaTrackConstraint { - self.0 - } -} - -#[cfg(test)] -mod tests { - use MediaTrackConstraintResolutionStrategy::*; - - use super::*; - - type Subject = MediaTrackConstraint; - - #[test] - fn default() { - let subject = Subject::default(); - - let actual = subject.is_empty(); - let expected = true; - - assert_eq!(actual, expected); - } - - mod from { - - use super::*; - - #[test] - fn setting() { - use crate::MediaTrackSetting; - - assert!(matches!( - Subject::from(MediaTrackSetting::Bool(true)), - Subject::Bool(ValueConstraint::Bare(_)) - )); - assert!(matches!( - Subject::from(MediaTrackSetting::Integer(42)), - Subject::IntegerRange(ValueRangeConstraint::Bare(_)) - )); - assert!(matches!( - Subject::from(MediaTrackSetting::Float(4.2)), - Subject::FloatRange(ValueRangeConstraint::Bare(_)) - )); - assert!(matches!( - Subject::from(MediaTrackSetting::String("string".to_owned())), - Subject::String(ValueConstraint::Bare(_)) - )); - } - - #[test] - fn bool() { - let subjects = [ - Subject::from(false), - Subject::from(ValueConstraint::::default()), - Subject::from(ResolvedValueConstraint::::default()), - ]; - - for subject in subjects { - // TODO: replace with `assert_matches!(…)`, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/82775 - assert!(matches!(subject, Subject::Bool(_))); - } - } - - #[test] - fn integer_range() { - let subjects = [ - Subject::from(42_u64), - Subject::from(ValueRangeConstraint::::default()), - Subject::from(ResolvedValueRangeConstraint::::default()), - ]; - - for subject in subjects { - // TODO: replace with `assert_matches!(…)`, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/82775 - assert!(matches!(subject, Subject::IntegerRange(_))); - } - } - - #[test] - fn float_range() { - let subjects = [ - Subject::from(42.0_f64), - Subject::from(ValueRangeConstraint::::default()), - Subject::from(ResolvedValueRangeConstraint::::default()), - ]; - - for subject in subjects { - // TODO: replace with `assert_matches!(…)`, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/82775 - assert!(matches!(subject, Subject::FloatRange(_))); - } - } - - #[test] - fn string() { - let subjects = [ - Subject::from(""), - Subject::from(String::new()), - Subject::from(ValueConstraint::::default()), - Subject::from(ResolvedValueConstraint::::default()), - ]; - - for subject in subjects { - // TODO: replace with `assert_matches!(…)`, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/82775 - assert!(matches!(subject, Subject::String(_))); - } - } - - #[test] - fn string_sequence() { - let subjects = [ - Subject::from(vec![""]), - Subject::from(vec![String::new()]), - Subject::from(ValueSequenceConstraint::::default()), - Subject::from(ResolvedValueSequenceConstraint::::default()), - ]; - - for subject in subjects { - // TODO: replace with `assert_matches!(…)`, once stabilized: - // Tracking issue: https://github.com/rust-lang/rust/issues/82775 - assert!(matches!(subject, Subject::StringSequence(_))); - } - } - } - - #[test] - fn is_empty() { - let empty_subject = Subject::Empty(EmptyConstraint {}); - - assert!(empty_subject.is_empty()); - - let non_empty_subjects = [ - Subject::Bool(ValueConstraint::Bare(true)), - Subject::FloatRange(ValueRangeConstraint::Bare(42.0)), - Subject::IntegerRange(ValueRangeConstraint::Bare(42)), - Subject::String(ValueConstraint::Bare("string".to_owned())), - Subject::StringSequence(ValueSequenceConstraint::Bare(vec!["string".to_owned()])), - ]; - - for non_empty_subject in non_empty_subjects { - assert!(!non_empty_subject.is_empty()); - } - } - - #[test] - fn to_resolved() { - let subjects = [ - ( - Subject::Empty(EmptyConstraint {}), - ResolvedMediaTrackConstraint::Empty(EmptyConstraint {}), - ), - ( - Subject::Bool(ValueConstraint::Bare(true)), - ResolvedMediaTrackConstraint::Bool(ResolvedValueConstraint::default().exact(true)), - ), - ( - Subject::FloatRange(ValueRangeConstraint::Bare(42.0)), - ResolvedMediaTrackConstraint::FloatRange( - ResolvedValueRangeConstraint::default().exact(42.0), - ), - ), - ( - Subject::IntegerRange(ValueRangeConstraint::Bare(42)), - ResolvedMediaTrackConstraint::IntegerRange( - ResolvedValueRangeConstraint::default().exact(42), - ), - ), - ( - Subject::String(ValueConstraint::Bare("string".to_owned())), - ResolvedMediaTrackConstraint::String( - ResolvedValueConstraint::default().exact("string".to_owned()), - ), - ), - ( - Subject::StringSequence(ValueSequenceConstraint::Bare(vec!["string".to_owned()])), - ResolvedMediaTrackConstraint::StringSequence( - ResolvedValueSequenceConstraint::default().exact(vec!["string".to_owned()]), - ), - ), - ]; - - for (subject, expected) in subjects { - let actual = subject.to_resolved(BareToExact); - - assert_eq!(actual, expected); - } - } - - mod resolved { - use super::*; - - type Subject = ResolvedMediaTrackConstraint; - - #[test] - fn to_string() { - let scenarios = [ - (Subject::Empty(EmptyConstraint {}), ""), - ( - Subject::Bool(ResolvedValueConstraint::default().exact(true)), - "(x == true)", - ), - ( - Subject::FloatRange(ResolvedValueRangeConstraint::default().exact(42.0)), - "(x == 42.0)", - ), - ( - Subject::IntegerRange(ResolvedValueRangeConstraint::default().exact(42)), - "(x == 42)", - ), - ( - Subject::String(ResolvedValueConstraint::default().exact("string".to_owned())), - "(x == \"string\")", - ), - ( - Subject::StringSequence( - ResolvedValueSequenceConstraint::default().exact(vec!["string".to_owned()]), - ), - "(x == [\"string\"])", - ), - ]; - - for (subject, expected) in scenarios { - let actual = subject.to_string(); - - assert_eq!(actual, expected); - } - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackConstraint; - - #[test] - fn empty() { - let subject = Subject::Empty(EmptyConstraint {}); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bool_bare() { - let subject = Subject::Bool(true.into()); - let json = serde_json::json!(true); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bool_constraint() { - let subject = Subject::Bool(ResolvedValueConstraint::default().exact(true).into()); - let json = serde_json::json!({ "exact": true }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn integer_range_bare() { - let subject = Subject::IntegerRange(42.into()); - let json = serde_json::json!(42); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn integer_range_constraint() { - let subject = - Subject::IntegerRange(ResolvedValueRangeConstraint::default().exact(42).into()); - let json = serde_json::json!({ "exact": 42 }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn float_range_bare() { - let subject = Subject::FloatRange(4.2.into()); - let json = serde_json::json!(4.2); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn float_range_constraint() { - let subject = - Subject::FloatRange(ResolvedValueRangeConstraint::default().exact(42.0).into()); - let json = serde_json::json!({ "exact": 42.0 }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string_sequence_bare() { - let subject = Subject::StringSequence(vec!["foo".to_owned(), "bar".to_owned()].into()); - let json = serde_json::json!(["foo", "bar"]); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string_sequence_constraint() { - let subject = Subject::StringSequence( - ResolvedValueSequenceConstraint::default() - .exact(vec!["foo".to_owned(), "bar".to_owned()]) - .into(), - ); - let json = serde_json::json!({ "exact": ["foo", "bar"] }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string_bare() { - let subject = Subject::String("foo".to_owned().into()); - let json = serde_json::json!("foo"); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string_constraint() { - let subject = Subject::String( - ResolvedValueConstraint::default() - .exact("foo".to_owned()) - .into(), - ); - let json = serde_json::json!({ "exact": "foo" }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/constraint/value.rs b/constraints/src/constraint/value.rs deleted file mode 100644 index 600074b13..000000000 --- a/constraints/src/constraint/value.rs +++ /dev/null @@ -1,419 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::MediaTrackConstraintResolutionStrategy; - -/// A bare value or constraint specifying a single accepted value. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `ValueConstraint` type aims to be a generalization over -/// multiple types in the spec. -/// -/// | Rust | W3C | -/// | ------------------------------ | --------------------------------------- | -/// | `ValueConstraint` | [`ConstrainBoolean`][constrain_boolean] | -/// -/// [constrain_boolean]: https://www.w3.org/TR/mediacapture-streams/#dom-constrainboolean -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum ValueConstraint { - /// A bare-valued media track constraint. - Bare(T), - /// A fully-qualified media track constraint. - Constraint(ResolvedValueConstraint), -} - -impl Default for ValueConstraint { - fn default() -> Self { - Self::Constraint(Default::default()) - } -} - -impl From for ValueConstraint { - fn from(bare: T) -> Self { - Self::Bare(bare) - } -} - -impl From> for ValueConstraint { - fn from(constraint: ResolvedValueConstraint) -> Self { - Self::Constraint(constraint) - } -} - -impl ValueConstraint -where - T: Clone, -{ - /// Returns a resolved representation of the constraint - /// with bare values resolved to fully-qualified constraints. - pub fn to_resolved( - &self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueConstraint { - self.clone().into_resolved(strategy) - } - - /// Consumes the constraint, returning a resolved representation of the - /// constraint with bare values resolved to fully-qualified constraints. - pub fn into_resolved( - self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueConstraint { - match self { - Self::Bare(bare) => match strategy { - MediaTrackConstraintResolutionStrategy::BareToIdeal => { - ResolvedValueConstraint::default().ideal(bare) - } - MediaTrackConstraintResolutionStrategy::BareToExact => { - ResolvedValueConstraint::default().exact(bare) - } - }, - Self::Constraint(constraint) => constraint, - } - } -} - -impl ValueConstraint { - /// Returns `true` if `self` is empty, otherwise `false`. - pub fn is_empty(&self) -> bool { - match self { - Self::Bare(_) => false, - Self::Constraint(constraint) => constraint.is_empty(), - } - } -} - -/// A constraint specifying a single accepted value. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `ValueConstraint` type aims to be a -/// generalization over multiple types in the W3C spec: -/// -/// | Rust | W3C | -/// | ------------------------------ | --------------------------------------- | -/// | `ResolvedValueConstraint` | [`ConstrainBooleanParameters`][constrain_boolean_parameters] | -/// -/// [constrain_boolean_parameters]: https://www.w3.org/TR/mediacapture-streams/#dom-constrainbooleanparameters -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct ResolvedValueConstraint { - /// The exact required value for this property. - /// - /// This is a required value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub exact: Option, - /// The ideal (target) value for this property. - /// - /// This is an optional value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub ideal: Option, -} - -impl ResolvedValueConstraint { - /// Consumes `self`, returning a corresponding constraint - /// with the exact required value set to `exact`. - #[inline] - pub fn exact(mut self, exact: U) -> Self - where - Option: From, - { - self.exact = exact.into(); - self - } - - /// Consumes `self`, returning a corresponding constraint - /// with the ideal required value set to `ideal`. - #[inline] - pub fn ideal(mut self, ideal: U) -> Self - where - Option: From, - { - self.ideal = ideal.into(); - self - } - - /// Returns `true` if `value.is_some()` is `true` for any of its required values, - /// otherwise `false`. - pub fn is_required(&self) -> bool { - self.exact.is_some() - } - - /// Returns `true` if `value.is_none()` is `true` for all of its values, - /// otherwise `false`. - pub fn is_empty(&self) -> bool { - self.exact.is_none() && self.ideal.is_none() - } - - /// Returns a corresponding constraint containing only required values. - pub fn to_required_only(&self) -> Self - where - T: Clone, - { - self.clone().into_required_only() - } - - /// Consumes `self, returning a corresponding constraint - /// containing only required values. - pub fn into_required_only(self) -> Self { - Self { - exact: self.exact, - ideal: None, - } - } -} - -impl Default for ResolvedValueConstraint { - #[inline] - fn default() -> Self { - Self { - exact: None, - ideal: None, - } - } -} - -impl std::fmt::Display for ResolvedValueConstraint -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut is_first = true; - f.write_str("(")?; - if let Some(ref exact) = &self.exact { - f.write_fmt(format_args!("x == {exact:?}"))?; - is_first = false; - } - if let Some(ref ideal) = &self.ideal { - if !is_first { - f.write_str(" && ")?; - } - f.write_fmt(format_args!("x ~= {ideal:?}"))?; - is_first = false; - } - if is_first { - f.write_str("")?; - } - f.write_str(")")?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_string() { - let scenarios = [ - (ResolvedValueConstraint::default(), "()"), - ( - ResolvedValueConstraint::default().exact(true), - "(x == true)", - ), - ( - ResolvedValueConstraint::default().ideal(true), - "(x ~= true)", - ), - ( - ResolvedValueConstraint::default().exact(true).ideal(true), - "(x == true && x ~= true)", - ), - ]; - - for (constraint, expected) in scenarios { - let actual = constraint.to_string(); - - assert_eq!(actual, expected); - } - } - - #[test] - fn is_required() { - let scenarios = [ - (ResolvedValueConstraint::default(), false), - (ResolvedValueConstraint::default().exact(true), true), - (ResolvedValueConstraint::default().ideal(true), false), - ( - ResolvedValueConstraint::default().exact(true).ideal(true), - true, - ), - ]; - - for (constraint, expected) in scenarios { - let actual = constraint.is_required(); - - assert_eq!(actual, expected); - } - } - - mod is_empty { - use super::*; - - #[test] - fn bare() { - let constraint = ValueConstraint::Bare(true); - - assert!(!constraint.is_empty()); - } - - #[test] - fn constraint() { - let scenarios = [ - (ResolvedValueConstraint::default(), true), - (ResolvedValueConstraint::default().exact(true), false), - (ResolvedValueConstraint::default().ideal(true), false), - ( - ResolvedValueConstraint::default().exact(true).ideal(true), - false, - ), - ]; - - for (constraint, expected) in scenarios { - let constraint = ValueConstraint::::Constraint(constraint); - - let actual = constraint.is_empty(); - - assert_eq!(actual, expected); - } - } - } - - #[test] - fn resolve_to_advanced() { - let constraints = [ - ValueConstraint::Bare(true), - ValueConstraint::Constraint(ResolvedValueConstraint::default().exact(true)), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToExact; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueConstraint::default().exact(true); - - for actual in actuals { - assert_eq!(actual, expected); - } - } - } - - #[test] - fn resolve_to_basic() { - let constraints = [ - ValueConstraint::Bare(true), - ValueConstraint::Constraint(ResolvedValueConstraint::default().ideal(true)), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToIdeal; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueConstraint::default().ideal(true); - - for actual in actuals { - assert_eq!(actual, expected); - } - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - macro_rules! test_serde { - ($t:ty => { - value: $value:expr - }) => { - type Subject = ValueConstraint<$t>; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bare() { - let subject = Subject::Bare($value.to_owned()); - let json = serde_json::json!($value); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn exact_constraint() { - let subject = Subject::Constraint(ResolvedValueConstraint::default().exact($value.to_owned())); - let json = serde_json::json!({ - "exact": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn ideal_constraint() { - let subject = Subject::Constraint(ResolvedValueConstraint::default().ideal($value.to_owned())); - let json = serde_json::json!({ - "ideal": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn full_constraint() { - let subject = Subject::Constraint(ResolvedValueConstraint::default().exact($value.to_owned()).ideal($value.to_owned())); - let json = serde_json::json!({ - "exact": $value, - "ideal": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - }; - } - - mod bool { - use super::*; - - test_serde!(bool => { - value: true - }); - } - - mod string { - use super::*; - - test_serde!(String => { - value: "VALUE" - }); - } -} diff --git a/constraints/src/constraint/value_range.rs b/constraints/src/constraint/value_range.rs deleted file mode 100644 index 4fed9808a..000000000 --- a/constraints/src/constraint/value_range.rs +++ /dev/null @@ -1,513 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::MediaTrackConstraintResolutionStrategy; - -/// A bare value or constraint specifying a range of accepted values. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `ValueConstraint` type aims to be a generalization over -/// multiple types in the spec. -/// -/// | Rust | W3C | -/// | ---------------------------------- | ------------------------------------- | -/// | `ValueRangeConstraint` | [`ConstrainULong`][constrain_ulong] | -/// | `ValueRangeConstraint` | [`ConstrainDouble`][constrain_double] | -/// -/// [constrain_double]: https://www.w3.org/TR/mediacapture-streams/#dom-constraindouble -/// [constrain_ulong]: https://www.w3.org/TR/mediacapture-streams/#dom-constrainulong -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum ValueRangeConstraint { - /// A bare-valued media track constraint. - Bare(T), - /// A fully-qualified media track constraint. - Constraint(ResolvedValueRangeConstraint), -} - -impl Default for ValueRangeConstraint { - fn default() -> Self { - Self::Constraint(Default::default()) - } -} - -impl From for ValueRangeConstraint { - fn from(bare: T) -> Self { - Self::Bare(bare) - } -} - -impl From> for ValueRangeConstraint { - fn from(constraint: ResolvedValueRangeConstraint) -> Self { - Self::Constraint(constraint) - } -} - -impl ValueRangeConstraint -where - T: Clone, -{ - /// Returns a resolved representation of the constraint - /// with bare values resolved to fully-qualified constraints. - pub fn to_resolved( - &self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueRangeConstraint { - self.clone().into_resolved(strategy) - } - - /// Consumes the constraint, returning a resolved representation of the - /// constraint with bare values resolved to fully-qualified constraints. - pub fn into_resolved( - self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueRangeConstraint { - match self { - Self::Bare(bare) => match strategy { - MediaTrackConstraintResolutionStrategy::BareToIdeal => { - ResolvedValueRangeConstraint::default().ideal(bare) - } - MediaTrackConstraintResolutionStrategy::BareToExact => { - ResolvedValueRangeConstraint::default().exact(bare) - } - }, - Self::Constraint(constraint) => constraint, - } - } -} - -impl ValueRangeConstraint { - /// Returns `true` if `self` is empty, otherwise `false`. - pub fn is_empty(&self) -> bool { - match self { - Self::Bare(_) => false, - Self::Constraint(constraint) => constraint.is_empty(), - } - } -} - -/// A constraint specifying a range of accepted values. -/// -/// Corresponding W3C spec types as per ["Media Capture and Streams"][spec]: -/// - `ConstrainDouble` => `ResolvedValueRangeConstraint` -/// - `ConstrainULong` => `ResolvedValueRangeConstraint` -/// -/// [spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct ResolvedValueRangeConstraint { - /// The minimum legal value of this property. - /// - /// This is a required value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub min: Option, - /// The maximum legal value of this property. - /// - /// This is a required value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub max: Option, - /// The exact required value for this property. - /// - /// This is a required value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub exact: Option, - /// The ideal (target) value for this property. - /// - /// This is an optional value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub ideal: Option, -} - -impl ResolvedValueRangeConstraint { - /// Consumes `self`, returning a corresponding constraint - /// with the exact required value set to `exact`. - #[inline] - pub fn exact(mut self, exact: U) -> Self - where - Option: From, - { - self.exact = exact.into(); - self - } - - /// Consumes `self`, returning a corresponding constraint - /// with the ideal required value set to `ideal`. - #[inline] - pub fn ideal(mut self, ideal: U) -> Self - where - Option: From, - { - self.ideal = ideal.into(); - self - } - - /// Consumes `self`, returning a corresponding constraint - /// with the minimum required value set to `min`. - #[inline] - pub fn min(mut self, min: U) -> Self - where - Option: From, - { - self.min = min.into(); - self - } - - /// Consumes `self`, returning a corresponding constraint - /// with the maximum required value set to `max`. - #[inline] - pub fn max(mut self, max: U) -> Self - where - Option: From, - { - self.max = max.into(); - self - } - - /// Returns `true` if `value.is_some()` is `true` for any of its required values, - /// otherwise `false`. - pub fn is_required(&self) -> bool { - self.min.is_some() || self.max.is_some() || self.exact.is_some() - } - - /// Returns `true` if `value.is_none()` is `true` for all of its values, - /// otherwise `false`. - pub fn is_empty(&self) -> bool { - self.min.is_none() && self.max.is_none() && self.exact.is_none() && self.ideal.is_none() - } - - /// Returns a corresponding constraint containing only required values. - pub fn to_required_only(&self) -> Self - where - T: Clone, - { - self.clone().into_required_only() - } - - /// Consumes `self, returning a corresponding constraint - /// containing only required values. - pub fn into_required_only(self) -> Self { - Self { - min: self.min, - max: self.max, - exact: self.exact, - ideal: None, - } - } -} - -impl Default for ResolvedValueRangeConstraint { - #[inline] - fn default() -> Self { - Self { - min: None, - max: None, - exact: None, - ideal: None, - } - } -} - -impl std::fmt::Display for ResolvedValueRangeConstraint -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut is_first = true; - f.write_str("(")?; - if let Some(exact) = &self.exact { - f.write_fmt(format_args!("x == {exact:?}"))?; - is_first = false; - } else if let (Some(min), Some(max)) = (&self.min, &self.max) { - f.write_fmt(format_args!("{min:?} <= x <= {max:?}"))?; - is_first = false; - } else if let Some(min) = &self.min { - f.write_fmt(format_args!("{min:?} <= x"))?; - is_first = false; - } else if let Some(max) = &self.max { - f.write_fmt(format_args!("x <= {max:?}"))?; - is_first = false; - } - if let Some(ideal) = &self.ideal { - if !is_first { - f.write_str(" && ")?; - } - f.write_fmt(format_args!("x ~= {ideal:?}"))?; - is_first = false; - } - if is_first { - f.write_str("")?; - } - f.write_str(")")?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_string() { - let scenarios = [ - (ResolvedValueRangeConstraint::default(), "()"), - (ResolvedValueRangeConstraint::default().exact(1), "(x == 1)"), - (ResolvedValueRangeConstraint::default().ideal(2), "(x ~= 2)"), - ( - ResolvedValueRangeConstraint::default().exact(1).ideal(2), - "(x == 1 && x ~= 2)", - ), - ]; - - for (constraint, expected) in scenarios { - let actual = constraint.to_string(); - - assert_eq!(actual, expected); - } - } - - #[test] - fn is_required() { - for min_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let min = if min_is_some { Some(1) } else { None }; - for max_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let max = if max_is_some { Some(2) } else { None }; - for exact_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let exact = if exact_is_some { Some(3) } else { None }; - for ideal_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let ideal = if ideal_is_some { Some(4) } else { None }; - - let constraint = ResolvedValueRangeConstraint:: { - min, - max, - exact, - ideal, - }; - - let actual = constraint.is_required(); - let expected = min_is_some || max_is_some || exact_is_some; - - assert_eq!(actual, expected); - } - } - } - } - } - - mod is_empty { - use super::*; - - #[test] - fn bare() { - let constraint = ValueRangeConstraint::Bare(42); - - assert!(!constraint.is_empty()); - } - - #[test] - fn constraint() { - for min_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let min = if min_is_some { Some(1) } else { None }; - for max_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let max = if max_is_some { Some(2) } else { None }; - for exact_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let exact = if exact_is_some { Some(3) } else { None }; - for ideal_is_some in [false, true] { - // TODO: Replace `if { Some(_) } else { None }` with `.then_some(_)` - // once MSRV has passed 1.62.0: - let ideal = if ideal_is_some { Some(4) } else { None }; - - let constraint = ResolvedValueRangeConstraint:: { - min, - max, - exact, - ideal, - }; - - let actual = constraint.is_empty(); - let expected = - !(min_is_some || max_is_some || exact_is_some || ideal_is_some); - - assert_eq!(actual, expected); - } - } - } - } - } - } -} - -#[test] -fn resolve_to_advanced() { - let constraints = [ - ValueRangeConstraint::Bare(42), - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint::default().exact(42)), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToExact; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueRangeConstraint::default().exact(42); - - for actual in actuals { - assert_eq!(actual, expected); - } - } -} - -#[test] -fn resolve_to_basic() { - let constraints = [ - ValueRangeConstraint::Bare(42), - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint::default().ideal(42)), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToIdeal; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueRangeConstraint::default().ideal(42); - - for actual in actuals { - assert_eq!(actual, expected); - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - macro_rules! test_serde { - ($t:ty => { - value: $value:expr - }) => { - type Subject = ValueRangeConstraint<$t>; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bare() { - let subject = Subject::Bare($value.to_owned()); - let json = serde_json::json!($value); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn min_constraint() { - let subject = Subject::Constraint(ResolvedValueRangeConstraint::default().min($value.to_owned())); - let json = serde_json::json!({ - "min": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn max_constraint() { - let subject = Subject::Constraint(ResolvedValueRangeConstraint::default().max($value.to_owned())); - let json = serde_json::json!({ - "max": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn exact_constraint() { - let subject = Subject::Constraint(ResolvedValueRangeConstraint::default().exact($value.to_owned())); - let json = serde_json::json!({ - "exact": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn ideal_constraint() { - let subject = Subject::Constraint(ResolvedValueRangeConstraint::default().ideal($value.to_owned())); - let json = serde_json::json!({ - "ideal": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn full_constraint() { - let subject = Subject::Constraint(ResolvedValueRangeConstraint::default().min($value.to_owned()).max($value.to_owned()).exact($value.to_owned()).ideal($value.to_owned())); - let json = serde_json::json!({ - "min": $value, - "max": $value, - "exact": $value, - "ideal": $value, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - }; - } - - mod f64 { - use super::*; - - test_serde!(f64 => { - value: 42.0 - }); - } - - mod u64 { - use super::*; - - test_serde!(u64 => { - value: 42 - }); - } -} diff --git a/constraints/src/constraint/value_sequence.rs b/constraints/src/constraint/value_sequence.rs deleted file mode 100644 index b3d95517c..000000000 --- a/constraints/src/constraint/value_sequence.rs +++ /dev/null @@ -1,440 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::MediaTrackConstraintResolutionStrategy; - -/// A bare value or constraint specifying a sequence of accepted values. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `ValueConstraint` type aims to be a generalization over -/// multiple types in the spec. -/// -/// | Rust | W3C | -/// | ---------------------------------------- | -------------------------------------------- | -/// | `ValueSequenceConstraint` | [`ConstrainDOMString`][constrain_dom_string] | -/// -/// [constrain_dom_string]: https://www.w3.org/TR/mediacapture-streams/#dom-constraindomstring -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum ValueSequenceConstraint { - /// A bare-valued media track constraint. - Bare(Vec), - /// A fully-qualified media track constraint. - Constraint(ResolvedValueSequenceConstraint), -} - -impl Default for ValueSequenceConstraint { - fn default() -> Self { - Self::Constraint(Default::default()) - } -} - -impl From for ValueSequenceConstraint { - fn from(bare: T) -> Self { - Self::Bare(vec![bare]) - } -} - -impl From> for ValueSequenceConstraint { - fn from(bare: Vec) -> Self { - Self::Bare(bare) - } -} - -impl From> for ValueSequenceConstraint { - fn from(constraint: ResolvedValueSequenceConstraint) -> Self { - Self::Constraint(constraint) - } -} - -impl ValueSequenceConstraint -where - T: Clone, -{ - /// Returns a resolved representation of the constraint - /// with bare values resolved to fully-qualified constraints. - pub fn to_resolved( - &self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueSequenceConstraint { - self.clone().into_resolved(strategy) - } - - /// Consumes the constraint, returning a resolved representation of the - /// constraint with bare values resolved to fully-qualified constraints. - pub fn into_resolved( - self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedValueSequenceConstraint { - match self { - Self::Bare(bare) => match strategy { - MediaTrackConstraintResolutionStrategy::BareToIdeal => { - ResolvedValueSequenceConstraint::default().ideal(bare) - } - MediaTrackConstraintResolutionStrategy::BareToExact => { - ResolvedValueSequenceConstraint::default().exact(bare) - } - }, - Self::Constraint(constraint) => constraint, - } - } -} - -impl ValueSequenceConstraint { - /// Returns `true` if `self` is empty, otherwise `false`. - pub fn is_empty(&self) -> bool { - match self { - Self::Bare(bare) => bare.is_empty(), - Self::Constraint(constraint) => constraint.is_empty(), - } - } -} - -/// A constraint specifying a sequence of accepted values. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `ValueSequenceConstraint` type aims to be a -/// generalization over multiple types in the W3C spec: -/// -/// | Rust | W3C | -/// | --------------------------------- | ----------------------------------------------------------------- | -/// | `ResolvedValueSequenceConstraint` | [`ConstrainDOMStringParameters`][constrain_dom_string_parameters] | -/// -/// [constrain_dom_string_parameters]: https://www.w3.org/TR/mediacapture-streams/#dom-constraindomstringparameters -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct ResolvedValueSequenceConstraint { - /// The exact required value for this property. - /// - /// This is a required value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub exact: Option>, - /// The ideal (target) value for this property. - /// - /// This is an optional value. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub ideal: Option>, -} - -impl ResolvedValueSequenceConstraint { - /// Consumes `self`, returning a corresponding constraint - /// with the exact required value set to `exact`. - #[inline] - pub fn exact(mut self, exact: U) -> Self - where - Option>: From, - { - self.exact = exact.into(); - self - } - - /// Consumes `self`, returning a corresponding constraint - /// with the ideal required value set to `ideal`. - #[inline] - pub fn ideal(mut self, ideal: U) -> Self - where - Option>: From, - { - self.ideal = ideal.into(); - self - } - - /// Returns `true` if `value.is_some()` is `true` for any of its required values, - /// otherwise `false`. - pub fn is_required(&self) -> bool { - self.exact.is_some() - } - - /// Returns `true` if `value.is_none()` is `true` for all of its values, - /// otherwise `false`. - pub fn is_empty(&self) -> bool { - let exact_is_empty = self.exact.as_ref().map_or(true, Vec::is_empty); - let ideal_is_empty = self.ideal.as_ref().map_or(true, Vec::is_empty); - exact_is_empty && ideal_is_empty - } - - /// Returns a corresponding constraint containing only required values. - pub fn to_required_only(&self) -> Self - where - T: Clone, - { - self.clone().into_required_only() - } - - /// Consumes `self, returning a corresponding constraint - /// containing only required values. - pub fn into_required_only(self) -> Self { - Self { - exact: self.exact, - ideal: None, - } - } -} - -impl Default for ResolvedValueSequenceConstraint { - fn default() -> Self { - Self { - exact: None, - ideal: None, - } - } -} - -impl std::fmt::Display for ResolvedValueSequenceConstraint -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut is_first = true; - f.write_str("(")?; - if let Some(ref exact) = &self.exact { - f.write_fmt(format_args!("x == {exact:?}"))?; - is_first = false; - } - if let Some(ref ideal) = &self.ideal { - if !is_first { - f.write_str(" && ")?; - } - f.write_fmt(format_args!("x ~= {ideal:?}"))?; - is_first = false; - } - if is_first { - f.write_str("")?; - } - f.write_str(")")?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_string() { - let scenarios = [ - (ResolvedValueSequenceConstraint::default(), "()"), - ( - ResolvedValueSequenceConstraint::default().exact(vec![1, 2]), - "(x == [1, 2])", - ), - ( - ResolvedValueSequenceConstraint::default().ideal(vec![2, 3]), - "(x ~= [2, 3])", - ), - ( - ResolvedValueSequenceConstraint::default() - .exact(vec![1, 2]) - .ideal(vec![2, 3]), - "(x == [1, 2] && x ~= [2, 3])", - ), - ]; - - for (constraint, expected) in scenarios { - let actual = constraint.to_string(); - - assert_eq!(actual, expected); - } - } - - #[test] - fn is_required() { - let scenarios = [ - (ResolvedValueSequenceConstraint::default(), false), - ( - ResolvedValueSequenceConstraint::default().exact(vec![true]), - true, - ), - ( - ResolvedValueSequenceConstraint::default().ideal(vec![true]), - false, - ), - ( - ResolvedValueSequenceConstraint::default() - .exact(vec![true]) - .ideal(vec![true]), - true, - ), - ]; - - for (constraint, expected) in scenarios { - let actual = constraint.is_required(); - - assert_eq!(actual, expected); - } - } - - mod is_empty { - use super::*; - - #[test] - fn bare() { - let constraint = ValueSequenceConstraint::Bare(vec![true]); - - assert!(!constraint.is_empty()); - } - - #[test] - fn constraint() { - let scenarios = [ - (ResolvedValueSequenceConstraint::default(), true), - ( - ResolvedValueSequenceConstraint::default().exact(vec![true]), - false, - ), - ( - ResolvedValueSequenceConstraint::default().ideal(vec![true]), - false, - ), - ( - ResolvedValueSequenceConstraint::default() - .exact(vec![true]) - .ideal(vec![true]), - false, - ), - ]; - - for (constraint, expected) in scenarios { - let constraint = ValueSequenceConstraint::::Constraint(constraint); - - let actual = constraint.is_empty(); - - assert_eq!(actual, expected); - } - } - } - - #[test] - fn resolve_to_advanced() { - let constraints = [ - ValueSequenceConstraint::Bare(vec![true]), - ValueSequenceConstraint::Constraint( - ResolvedValueSequenceConstraint::default().exact(vec![true]), - ), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToExact; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueSequenceConstraint::default().exact(vec![true]); - - for actual in actuals { - assert_eq!(actual, expected); - } - } - } - - #[test] - fn resolve_to_basic() { - let constraints = [ - ValueSequenceConstraint::Bare(vec![true]), - ValueSequenceConstraint::Constraint( - ResolvedValueSequenceConstraint::default().ideal(vec![true]), - ), - ]; - let strategy = MediaTrackConstraintResolutionStrategy::BareToIdeal; - - for constraint in constraints { - let actuals = [ - constraint.to_resolved(strategy), - constraint.into_resolved(strategy), - ]; - - let expected = ResolvedValueSequenceConstraint::default().ideal(vec![true]); - - for actual in actuals { - assert_eq!(actual, expected); - } - } - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - macro_rules! test_serde { - ($t:ty => { - values: [$($values:expr),*] - }) => { - type Subject = ValueSequenceConstraint<$t>; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn bare() { - let subject = Subject::Bare(vec![$($values.to_owned()),*].into()); - let json = serde_json::json!([$($values),*]); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn exact_constraint() { - let subject = Subject::Constraint(ResolvedValueSequenceConstraint::default().exact(vec![$($values.to_owned()),*])); - let json = serde_json::json!({ - "exact": [$($values),*], - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn ideal_constraint() { - let subject = Subject::Constraint(ResolvedValueSequenceConstraint::default().ideal(vec![$($values.to_owned()),*])); - let json = serde_json::json!({ - "ideal": [$($values),*], - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn full_constraint() { - let subject = Subject::Constraint(ResolvedValueSequenceConstraint::default().exact(vec![$($values.to_owned()),*]).ideal(vec![$($values.to_owned()),*])); - let json = serde_json::json!({ - "exact": [$($values),*], - "ideal": [$($values),*], - }); - - test_serde_symmetry!(subject: subject, json: json); - } - }; - } - - mod string { - use super::*; - - test_serde!(String => { - values: ["VALUE_0", "VALUE_1"] - }); - } -} diff --git a/constraints/src/constraints.rs b/constraints/src/constraints.rs deleted file mode 100644 index 8b89b909e..000000000 --- a/constraints/src/constraints.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod advanced; -mod constraint_set; -mod mandatory; -mod stream; -mod track; - -pub use self::advanced::{ - AdvancedMediaTrackConstraints, ResolvedAdvancedMediaTrackConstraints, - SanitizedAdvancedMediaTrackConstraints, -}; -pub use self::constraint_set::{ - MediaTrackConstraintSet, ResolvedMediaTrackConstraintSet, SanitizedMediaTrackConstraintSet, -}; -pub use self::mandatory::{ - MandatoryMediaTrackConstraints, ResolvedMandatoryMediaTrackConstraints, - SanitizedMandatoryMediaTrackConstraints, -}; -pub use self::stream::MediaStreamConstraints; -pub use self::track::{ - BoolOrMediaTrackConstraints, MediaTrackConstraints, ResolvedMediaTrackConstraints, - SanitizedMediaTrackConstraints, -}; diff --git a/constraints/src/constraints/advanced.rs b/constraints/src/constraints/advanced.rs deleted file mode 100644 index d6e893199..000000000 --- a/constraints/src/constraints/advanced.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use super::constraint_set::GenericMediaTrackConstraintSet; -use crate::{ - MediaTrackConstraint, MediaTrackConstraintResolutionStrategy, MediaTrackSupportedConstraints, - ResolvedMediaTrackConstraint, SanitizedMediaTrackConstraint, -}; - -/// Advanced media track constraints that contain sets of either bare values or constraints. -pub type AdvancedMediaTrackConstraints = GenericAdvancedMediaTrackConstraints; - -/// Advanced media track constraints that contain sets of constraints (both, empty and non-empty). -pub type ResolvedAdvancedMediaTrackConstraints = - GenericAdvancedMediaTrackConstraints; - -/// Advanced media track constraints that contain sets of only non-empty constraints. -pub type SanitizedAdvancedMediaTrackConstraints = - GenericAdvancedMediaTrackConstraints; - -/// The list of advanced constraint sets for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`ResolvedMediaTrackConstraints.advanced`][media_track_constraints_advanced] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints_advanced]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints-advanced -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct GenericAdvancedMediaTrackConstraints(Vec>); - -impl GenericAdvancedMediaTrackConstraints { - pub fn new(constraints: Vec>) -> Self { - Self(constraints) - } - - pub fn into_inner(self) -> Vec> { - self.0 - } -} - -impl Deref for GenericAdvancedMediaTrackConstraints { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GenericAdvancedMediaTrackConstraints { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for GenericAdvancedMediaTrackConstraints { - fn default() -> Self { - Self(Default::default()) - } -} - -impl FromIterator> - for GenericAdvancedMediaTrackConstraints -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator>, - { - Self::new(iter.into_iter().collect()) - } -} - -impl IntoIterator for GenericAdvancedMediaTrackConstraints { - type Item = GenericMediaTrackConstraintSet; - type IntoIter = std::vec::IntoIter>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl AdvancedMediaTrackConstraints { - pub fn to_resolved(&self) -> ResolvedAdvancedMediaTrackConstraints { - self.clone().into_resolved() - } - - pub fn into_resolved(self) -> ResolvedAdvancedMediaTrackConstraints { - let strategy = MediaTrackConstraintResolutionStrategy::BareToExact; - ResolvedAdvancedMediaTrackConstraints::from_iter( - self.into_iter() - .map(|constraint_set| constraint_set.into_resolved(strategy)), - ) - } -} - -impl ResolvedAdvancedMediaTrackConstraints { - pub fn to_sanitized( - &self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedAdvancedMediaTrackConstraints { - self.clone().into_sanitized(supported_constraints) - } - - pub fn into_sanitized( - self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedAdvancedMediaTrackConstraints { - SanitizedAdvancedMediaTrackConstraints::from_iter( - self.into_iter() - .map(|constraint_set| constraint_set.into_sanitized(supported_constraints)) - .filter(|constraint_set| !constraint_set.is_empty()), - ) - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::property::all::name::*; - use crate::MediaTrackConstraintSet; - - #[test] - fn serialize_default() { - let advanced = AdvancedMediaTrackConstraints::default(); - let actual = serde_json::to_value(advanced).unwrap(); - let expected = serde_json::json!([]); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize_default() { - let json = serde_json::json!([]); - let actual: AdvancedMediaTrackConstraints = serde_json::from_value(json).unwrap(); - let expected = AdvancedMediaTrackConstraints::default(); - - assert_eq!(actual, expected); - } - - #[test] - fn serialize() { - let advanced = - AdvancedMediaTrackConstraints::new(vec![MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ])]); - let actual = serde_json::to_value(advanced).unwrap(); - let expected = serde_json::json!([ - { - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - } - ]); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize() { - let json = serde_json::json!([ - { - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - } - ]); - let actual: AdvancedMediaTrackConstraints = serde_json::from_value(json).unwrap(); - let expected = - AdvancedMediaTrackConstraints::new(vec![MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ])]); - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/constraints/constraint_set.rs b/constraints/src/constraints/constraint_set.rs deleted file mode 100644 index 89680963b..000000000 --- a/constraints/src/constraints/constraint_set.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -use indexmap::IndexMap; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::constraint::SanitizedMediaTrackConstraint; -use crate::{ - MediaTrackConstraint, MediaTrackConstraintResolutionStrategy, MediaTrackProperty, - MediaTrackSupportedConstraints, ResolvedMediaTrackConstraint, -}; - -/// Media track constraint set that contains either bare values or constraints. -pub type MediaTrackConstraintSet = GenericMediaTrackConstraintSet; - -/// Media track constraint set that contains only constraints (both, empty and non-empty). -pub type ResolvedMediaTrackConstraintSet = - GenericMediaTrackConstraintSet; - -/// Media track constraint set that contains only non-empty constraints. -pub type SanitizedMediaTrackConstraintSet = - GenericMediaTrackConstraintSet; - -/// The set of constraints for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`ResolvedMediaTrackConstraintSet`][media_track_constraint_set] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraint_set]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraintset -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct GenericMediaTrackConstraintSet(IndexMap); - -impl GenericMediaTrackConstraintSet { - pub fn new(constraint_set: IndexMap) -> Self { - Self(constraint_set) - } - - pub fn into_inner(self) -> IndexMap { - self.0 - } -} - -impl Deref for GenericMediaTrackConstraintSet { - type Target = IndexMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GenericMediaTrackConstraintSet { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for GenericMediaTrackConstraintSet { - fn default() -> Self { - Self(IndexMap::new()) - } -} - -impl FromIterator<(U, T)> for GenericMediaTrackConstraintSet -where - U: Into, -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - Self::new(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) - } -} - -impl IntoIterator for GenericMediaTrackConstraintSet { - type Item = (MediaTrackProperty, T); - type IntoIter = indexmap::map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl MediaTrackConstraintSet { - pub fn to_resolved( - &self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedMediaTrackConstraintSet { - self.clone().into_resolved(strategy) - } - - pub fn into_resolved( - self, - strategy: MediaTrackConstraintResolutionStrategy, - ) -> ResolvedMediaTrackConstraintSet { - ResolvedMediaTrackConstraintSet::new( - self.into_iter() - .map(|(property, constraint)| (property, constraint.into_resolved(strategy))) - .collect(), - ) - } -} - -impl ResolvedMediaTrackConstraintSet { - pub fn to_sanitized( - &self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMediaTrackConstraintSet { - self.clone().into_sanitized(supported_constraints) - } - - pub fn into_sanitized( - self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMediaTrackConstraintSet { - let index_map: IndexMap = self - .into_iter() - .filter_map(|(property, constraint)| { - if supported_constraints.contains(&property) { - constraint - .into_sanitized() - .map(|constraint| (property, constraint)) - } else { - None - } - }) - .collect(); - SanitizedMediaTrackConstraintSet::new(index_map) - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::property::all::name::*; - - #[test] - fn serialize_default() { - let constraint_set = MediaTrackConstraintSet::default(); - let actual = serde_json::to_value(constraint_set).unwrap(); - let expected = serde_json::json!({}); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize_default() { - let json = serde_json::json!({}); - let actual: MediaTrackConstraintSet = serde_json::from_value(json).unwrap(); - let expected = MediaTrackConstraintSet::default(); - - assert_eq!(actual, expected); - } - - #[test] - fn serialize() { - let constraint_set = MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ]); - let actual = serde_json::to_value(constraint_set).unwrap(); - let expected = serde_json::json!({ - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - }); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize() { - let json = serde_json::json!({ - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - }); - let actual: MediaTrackConstraintSet = serde_json::from_value(json).unwrap(); - let expected = MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ]); - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/constraints/mandatory.rs b/constraints/src/constraints/mandatory.rs deleted file mode 100644 index 95dd8fc24..000000000 --- a/constraints/src/constraints/mandatory.rs +++ /dev/null @@ -1,321 +0,0 @@ -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use super::constraint_set::GenericMediaTrackConstraintSet; -use crate::{ - MediaTrackConstraint, MediaTrackConstraintResolutionStrategy, MediaTrackProperty, - MediaTrackSupportedConstraints, ResolvedMediaTrackConstraint, SanitizedMediaTrackConstraint, -}; - -/// The list of mandatory constraint sets for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`ResolvedMediaTrackConstraints.mandatory`][media_track_constraints_mandatory] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// Unlike `ResolvedMandatoryMediaTrackConstraints` this type may contain constraints with bare values. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints_mandatory]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints-mandatory -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -pub type MandatoryMediaTrackConstraints = - GenericMandatoryMediaTrackConstraints; - -/// The list of mandatory constraint sets for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`ResolvedMediaTrackConstraintSet`][media_track_constraints_mandatory] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// Unlike `MandatoryMediaTrackConstraints` this type does not contain constraints -/// with bare values, but has them resolved to full constraints instead. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints_mandatory]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints-mandatory -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -pub type ResolvedMandatoryMediaTrackConstraints = - GenericMandatoryMediaTrackConstraints; - -/// Set of mandatory media track constraints that contains only non-empty constraints. -pub type SanitizedMandatoryMediaTrackConstraints = - GenericMandatoryMediaTrackConstraints; - -/// The set of constraints for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`ResolvedMediaTrackConstraintSet`][media_track_constraint_set] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraint_set]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraintset -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct GenericMandatoryMediaTrackConstraints(GenericMediaTrackConstraintSet); - -impl GenericMandatoryMediaTrackConstraints { - pub fn new(constraints: GenericMediaTrackConstraintSet) -> Self { - Self(constraints) - } - - pub fn into_inner(self) -> GenericMediaTrackConstraintSet { - self.0 - } -} - -impl GenericMandatoryMediaTrackConstraints { - pub fn basic(&self) -> GenericMediaTrackConstraintSet { - self.basic_or_required(false) - } - - pub fn required(&self) -> GenericMediaTrackConstraintSet { - self.basic_or_required(true) - } - - fn basic_or_required( - &self, - required: bool, - ) -> GenericMediaTrackConstraintSet { - GenericMediaTrackConstraintSet::new( - self.0 - .iter() - .filter_map(|(property, constraint)| { - if constraint.is_required() == required { - Some((property.clone(), constraint.clone())) - } else { - None - } - }) - .collect(), - ) - } -} - -impl Deref for GenericMandatoryMediaTrackConstraints { - type Target = GenericMediaTrackConstraintSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GenericMandatoryMediaTrackConstraints { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for GenericMandatoryMediaTrackConstraints { - fn default() -> Self { - Self(Default::default()) - } -} - -impl FromIterator<(U, T)> for GenericMandatoryMediaTrackConstraints -where - U: Into, -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - Self::new(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) - } -} - -impl IntoIterator for GenericMandatoryMediaTrackConstraints { - type Item = (MediaTrackProperty, T); - type IntoIter = indexmap::map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl MandatoryMediaTrackConstraints { - pub fn to_resolved(&self) -> ResolvedMandatoryMediaTrackConstraints { - self.clone().into_resolved() - } - - pub fn into_resolved(self) -> ResolvedMandatoryMediaTrackConstraints { - let strategy = MediaTrackConstraintResolutionStrategy::BareToIdeal; - ResolvedMandatoryMediaTrackConstraints::new(self.0.into_resolved(strategy)) - } -} - -impl ResolvedMandatoryMediaTrackConstraints { - pub fn to_sanitized( - &self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMandatoryMediaTrackConstraints { - self.clone().into_sanitized(supported_constraints) - } - - pub fn into_sanitized( - self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMandatoryMediaTrackConstraints { - SanitizedMandatoryMediaTrackConstraints::new(self.0.into_sanitized(supported_constraints)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::property::all::name::*; - use crate::{ - ResolvedMediaTrackConstraintSet, ResolvedValueConstraint, ResolvedValueRangeConstraint, - }; - - #[test] - fn basic() { - let mandatory = ResolvedMandatoryMediaTrackConstraints::new( - ResolvedMediaTrackConstraintSet::from_iter([ - ( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("device-id".to_owned()) - .into(), - ), - ( - &AUTO_GAIN_CONTROL, - ResolvedValueConstraint::default().ideal(true).into(), - ), - ( - &CHANNEL_COUNT, - ResolvedValueRangeConstraint::default() - .exact(2) - .ideal(3) - .into(), - ), - ]), - ); - - let actual = mandatory.basic(); - let expected = ResolvedMediaTrackConstraintSet::from_iter([( - &AUTO_GAIN_CONTROL, - ResolvedValueConstraint::default().ideal(true).into(), - )]); - - assert_eq!(actual, expected); - } - - #[test] - fn required() { - let mandatory = ResolvedMandatoryMediaTrackConstraints::new( - ResolvedMediaTrackConstraintSet::from_iter([ - ( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("device-id".to_owned()) - .into(), - ), - ( - &AUTO_GAIN_CONTROL, - ResolvedValueConstraint::default().ideal(true).into(), - ), - ( - &CHANNEL_COUNT, - ResolvedValueRangeConstraint::default() - .exact(2) - .ideal(3) - .into(), - ), - ]), - ); - - let actual = mandatory.required(); - let expected = ResolvedMediaTrackConstraintSet::from_iter([ - ( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("device-id".to_owned()) - .into(), - ), - ( - &CHANNEL_COUNT, - ResolvedValueRangeConstraint::default() - .exact(2) - .ideal(3) - .into(), - ), - ]); - - assert_eq!(actual, expected); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::property::all::name::*; - use crate::MediaTrackConstraintSet; - - #[test] - fn serialize_default() { - let mandatory = MandatoryMediaTrackConstraints::default(); - let actual = serde_json::to_value(mandatory).unwrap(); - let expected = serde_json::json!({}); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize_default() { - let json = serde_json::json!({}); - let actual: MandatoryMediaTrackConstraints = serde_json::from_value(json).unwrap(); - let expected = MandatoryMediaTrackConstraints::default(); - - assert_eq!(actual, expected); - } - - #[test] - fn serialize() { - let mandatory = MandatoryMediaTrackConstraints::new(MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ])); - let actual = serde_json::to_value(mandatory).unwrap(); - let expected = serde_json::json!( - { - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - } - ); - - assert_eq!(actual, expected); - } - - #[test] - fn deserialize() { - let json = serde_json::json!( - { - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - } - ); - let actual: MandatoryMediaTrackConstraints = serde_json::from_value(json).unwrap(); - let expected = MandatoryMediaTrackConstraints::new(MediaTrackConstraintSet::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ])); - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/constraints/stream.rs b/constraints/src/constraints/stream.rs deleted file mode 100644 index c3268160f..000000000 --- a/constraints/src/constraints/stream.rs +++ /dev/null @@ -1,90 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use super::track::GenericBoolOrMediaTrackConstraints; -use crate::MediaTrackConstraint; - -/// The constraints for a [`MediaStream`][media_stream] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaStreamConstraints`][media_stream_constraints] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastream -/// [media_stream_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -pub type MediaStreamConstraints = GenericMediaStreamConstraints; - -/// The constraints for a [`MediaStream`][media_stream] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaStreamConstraints`][media_stream_constraints] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastream -/// [media_stream_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Default, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct GenericMediaStreamConstraints { - #[cfg_attr(feature = "serde", serde(default))] - pub audio: GenericBoolOrMediaTrackConstraints, - #[cfg_attr(feature = "serde", serde(default))] - pub video: GenericBoolOrMediaTrackConstraints, -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod tests { - use std::iter::FromIterator; - - use super::*; - use crate::constraints::advanced::AdvancedMediaTrackConstraints; - use crate::constraints::mandatory::MandatoryMediaTrackConstraints; - use crate::constraints::track::{BoolOrMediaTrackConstraints, MediaTrackConstraints}; - use crate::macros::test_serde_symmetry; - use crate::property::all::name::*; - use crate::MediaTrackConstraintSet; - - type Subject = MediaStreamConstraints; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({ - "audio": false, - "video": false, - }); - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject { - audio: BoolOrMediaTrackConstraints::Constraints(MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - (&DEVICE_ID, "microphone".into()), - (&CHANNEL_COUNT, 2.into()), - ]), - advanced: AdvancedMediaTrackConstraints::new(vec![ - MediaTrackConstraintSet::from_iter([(&LATENCY, 0.123.into())]), - ]), - }), - video: BoolOrMediaTrackConstraints::Bool(true), - }; - let json = serde_json::json!({ - "audio": { - "deviceId": "microphone", - "channelCount": 2_i64, - "advanced": [ - { "latency": 0.123_f64, } - ] - }, - "video": true, - }); - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/constraints/track.rs b/constraints/src/constraints/track.rs deleted file mode 100644 index 425fd6c54..000000000 --- a/constraints/src/constraints/track.rs +++ /dev/null @@ -1,339 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use super::advanced::GenericAdvancedMediaTrackConstraints; -use super::mandatory::GenericMandatoryMediaTrackConstraints; -use crate::constraint::SanitizedMediaTrackConstraint; -use crate::{MediaTrackConstraint, MediaTrackSupportedConstraints, ResolvedMediaTrackConstraint}; - -/// A boolean on/off flag or bare value or constraints for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -pub type BoolOrMediaTrackConstraints = GenericBoolOrMediaTrackConstraints; - -/// A boolean on/off flag or constraints for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no direct corresponding type in the -/// W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec, -/// since the `BoolOrMediaTrackConstraints` type aims to be a -/// generalization over multiple types in the W3C spec: -/// -/// | Rust | W3C | -/// | ----------------------------- | -------------------------------------------------------------------------------------------------- | -/// | `BoolOrMediaTrackConstraints` | [`MediaStreamConstraints`][media_stream_constraints]'s [`video`][video] / [`audio`][audio] members | -/// -/// [media_stream_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints-video -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [video]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints-video -/// [audio]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints-audio -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum GenericBoolOrMediaTrackConstraints { - /// Boolean track selector. - Bool(bool), - /// Constraints-based track selector. - Constraints(GenericMediaTrackConstraints), -} - -impl GenericBoolOrMediaTrackConstraints -where - T: Clone, -{ - pub fn to_constraints(&self) -> Option> { - self.clone().into_constraints() - } - - pub fn into_constraints(self) -> Option> { - match self { - Self::Bool(false) => None, - Self::Bool(true) => Some(GenericMediaTrackConstraints::default()), - Self::Constraints(constraints) => Some(constraints), - } - } -} - -impl Default for GenericBoolOrMediaTrackConstraints { - fn default() -> Self { - Self::Bool(false) - } -} - -impl From for GenericBoolOrMediaTrackConstraints { - fn from(flag: bool) -> Self { - Self::Bool(flag) - } -} - -impl From> for GenericBoolOrMediaTrackConstraints { - fn from(constraints: GenericMediaTrackConstraints) -> Self { - Self::Constraints(constraints) - } -} - -/// Media track constraints that contains either bare values or constraints. -pub type MediaTrackConstraints = GenericMediaTrackConstraints; - -/// Media track constraints that contains only constraints (both, empty and non-empty). -pub type ResolvedMediaTrackConstraints = GenericMediaTrackConstraints; - -/// Media track constraints that contains only non-empty constraints. -pub type SanitizedMediaTrackConstraints = - GenericMediaTrackConstraints; - -/// The constraints for a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaTrackConstraints`][media_track_constraints] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams/ -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct GenericMediaTrackConstraints { - /// Mandatory (i.e required or optional basic) constraints, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-constraint - #[cfg_attr(feature = "serde", serde(flatten))] - pub mandatory: GenericMandatoryMediaTrackConstraints, - - /// Advanced constraints, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-constraint - #[cfg_attr( - feature = "serde", - serde(default = "Default::default"), - serde(skip_serializing_if = "should_skip_advanced") - )] - pub advanced: GenericAdvancedMediaTrackConstraints, -} - -#[cfg(feature = "serde")] -fn should_skip_advanced(advanced: &GenericAdvancedMediaTrackConstraints) -> bool { - advanced.is_empty() -} - -impl Default for GenericMediaTrackConstraints { - fn default() -> Self { - Self { - mandatory: Default::default(), - advanced: Default::default(), - } - } -} - -impl MediaTrackConstraints { - pub fn to_resolved(&self) -> ResolvedMediaTrackConstraints { - self.clone().into_resolved() - } - - pub fn into_resolved(self) -> ResolvedMediaTrackConstraints { - let Self { - mandatory, - advanced, - } = self; - ResolvedMediaTrackConstraints { - mandatory: mandatory.into_resolved(), - advanced: advanced.into_resolved(), - } - } -} - -impl ResolvedMediaTrackConstraints { - pub fn to_sanitized( - &self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMediaTrackConstraints { - self.clone().into_sanitized(supported_constraints) - } - - pub fn into_sanitized( - self, - supported_constraints: &MediaTrackSupportedConstraints, - ) -> SanitizedMediaTrackConstraints { - let mandatory = self.mandatory.into_sanitized(supported_constraints); - let advanced = self.advanced.into_sanitized(supported_constraints); - SanitizedMediaTrackConstraints { - mandatory, - advanced, - } - } -} - -#[cfg(test)] -mod tests { - use std::iter::FromIterator; - - use super::*; - use crate::constraints::mandatory::MandatoryMediaTrackConstraints; - use crate::property::all::name::*; - use crate::{ - AdvancedMediaTrackConstraints, ResolvedAdvancedMediaTrackConstraints, - ResolvedMandatoryMediaTrackConstraints, ResolvedValueConstraint, - }; - - type Subject = BoolOrMediaTrackConstraints; - - #[test] - fn default() { - let actual = Subject::default(); - let expected = Subject::Bool(false); - - assert_eq!(actual, expected); - } - - mod from { - use super::*; - - #[test] - fn bool() { - for value in [false, true] { - let actual = Subject::from(value); - let expected = Subject::Bool(value); - - assert_eq!(actual, expected); - } - } - - #[test] - fn constraints() { - let constraints = GenericMediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - "microphone".into(), - )]), - advanced: AdvancedMediaTrackConstraints::new(vec![]), - }; - - let actual = Subject::from(constraints.clone()); - let expected = Subject::Constraints(constraints); - - assert_eq!(actual, expected); - } - } - - mod to_constraints { - use super::*; - - #[test] - fn bool_false() { - let subject = Subject::Bool(false); - - let actual = subject.to_constraints(); - let expected = None; - - assert_eq!(actual, expected); - } - - #[test] - fn bool_true() { - let subject = Subject::Bool(true); - - let actual = subject.to_constraints(); - let expected = Some(GenericMediaTrackConstraints::default()); - - assert_eq!(actual, expected); - } - - #[test] - fn constraints() { - let constraints = GenericMediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - "microphone".into(), - )]), - advanced: AdvancedMediaTrackConstraints::new(vec![]), - }; - - let subject = Subject::Constraints(constraints.clone()); - - let actual = subject.to_constraints(); - let expected = Some(constraints); - - assert_eq!(actual, expected); - } - } - - #[test] - fn to_resolved() { - let subject = MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - "microphone".into(), - )]), - advanced: AdvancedMediaTrackConstraints::new(vec![]), - }; - - let actual = subject.to_resolved(); - let expected = ResolvedMediaTrackConstraints { - mandatory: ResolvedMandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - ResolvedValueConstraint::default() - .ideal("microphone".to_owned()) - .into(), - )]), - advanced: ResolvedAdvancedMediaTrackConstraints::new(vec![]), - }; - - assert_eq!(actual, expected); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use std::iter::FromIterator; - - use super::*; - use crate::constraints::mandatory::MandatoryMediaTrackConstraints; - use crate::macros::test_serde_symmetry; - use crate::property::all::name::*; - use crate::{AdvancedMediaTrackConstraints, MediaTrackConstraintSet}; - - type Subject = MediaTrackConstraints; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject { - mandatory: MandatoryMediaTrackConstraints::from_iter([( - &DEVICE_ID, - "microphone".into(), - )]), - advanced: AdvancedMediaTrackConstraints::new(vec![ - MediaTrackConstraintSet::from_iter([ - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - ]), - MediaTrackConstraintSet::from_iter([(&LATENCY, 0.123.into())]), - ]), - }; - let json = serde_json::json!({ - "deviceId": "microphone", - "advanced": [ - { - "autoGainControl": true, - "channelCount": 2, - }, - { - "latency": 0.123, - }, - ] - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/enumerations.rs b/constraints/src/enumerations.rs deleted file mode 100644 index 2c890a866..000000000 --- a/constraints/src/enumerations.rs +++ /dev/null @@ -1,131 +0,0 @@ -/// The directions that the camera can face, as seen from the user's perspective. -/// -/// # Note -/// The enumeration is not exhaustive and merely provides a list of known values. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum FacingMode { - /// The source is facing toward the user (a self-view camera). - User, - - /// The source is facing away from the user (viewing the environment). - Environment, - - /// The source is facing to the left of the user. - Left, - - /// The source is facing to the right of the user. - Right, -} - -impl FacingMode { - /// Returns `"user"`, the string-value of the `User` facing mode. - pub fn user() -> String { - Self::User.to_string() - } - - /// Returns `"environment"`, the string-value of the `Environment` facing mode. - pub fn environment() -> String { - Self::Environment.to_string() - } - - /// Returns `"left"`, the string-value of the `Left` facing mode. - pub fn left() -> String { - Self::Left.to_string() - } - - /// Returns `"right"`, the string-value of the `Right` facing mode. - pub fn right() -> String { - Self::Right.to_string() - } -} - -impl std::fmt::Display for FacingMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::User => f.write_str("user"), - Self::Environment => f.write_str("environment"), - Self::Left => f.write_str("left"), - Self::Right => f.write_str("right"), - } - } -} - -/// The means by which the resolution can be derived by the client. -/// -/// # Note -/// The enumeration is not exhaustive and merely provides a list of known values. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ResizeMode { - /// This resolution and frame rate is offered by the camera, its driver, or the OS. - /// - /// # Note - /// The user agent MAY report this value to disguise concurrent use, - /// but only when the camera is in use in another browsing context. - /// - /// # Important - /// This value is a possible finger-printing surface. - None, - - /// This resolution is downscaled and/or cropped from a higher camera resolution by the user agent, - /// or its frame rate is decimated by the User Agent. - /// - /// # Important - /// The media MUST NOT be upscaled, stretched or have fake data created that did not occur in the input source. - CropAndScale, -} - -impl ResizeMode { - /// Returns `"none"`, the string-value of the `None` resize mode. - pub fn none() -> String { - Self::None.to_string() - } - - /// Returns `"crop-and-scale"`, the string-value of the `CropAndScale` resize mode. - pub fn crop_and_scale() -> String { - Self::CropAndScale.to_string() - } -} - -impl std::fmt::Display for ResizeMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::None => f.write_str("none"), - Self::CropAndScale => f.write_str("crop-and-scale"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod facing_mode { - use super::*; - - #[test] - fn to_string() { - assert_eq!(FacingMode::User.to_string(), "user"); - assert_eq!(FacingMode::Environment.to_string(), "environment"); - assert_eq!(FacingMode::Left.to_string(), "left"); - assert_eq!(FacingMode::Right.to_string(), "right"); - - assert_eq!(FacingMode::user(), "user"); - assert_eq!(FacingMode::environment(), "environment"); - assert_eq!(FacingMode::left(), "left"); - assert_eq!(FacingMode::right(), "right"); - } - } - - mod resize_mode { - use super::*; - - #[test] - fn to_string() { - assert_eq!(ResizeMode::None.to_string(), "none"); - assert_eq!(ResizeMode::CropAndScale.to_string(), "crop-and-scale"); - - assert_eq!(ResizeMode::none(), "none"); - assert_eq!(ResizeMode::crop_and_scale(), "crop-and-scale"); - } - } -} diff --git a/constraints/src/errors.rs b/constraints/src/errors.rs deleted file mode 100644 index cdd1a2a1c..000000000 --- a/constraints/src/errors.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Errors, as defined in the ["Media Capture and Streams"][mediacapture_streams] spec. -//! -//! [mediacapture_streams]: https://www.w3.org/TR/mediacapture-streams/ - -use std::collections::HashMap; - -use thiserror::Error; - -use crate::algorithms::{ConstraintFailureInfo, SettingFitnessDistanceErrorKind}; -use crate::MediaTrackProperty; - -/// An error indicating one or more over-constrained settings. -#[derive(Error, Clone, Eq, PartialEq, Debug)] -pub struct OverconstrainedError { - /// The offending constraint's name. - pub constraint: MediaTrackProperty, - /// An error message, or `None` if exposure-mode was `Protected`. - pub message: Option, -} - -impl Default for OverconstrainedError { - fn default() -> Self { - Self { - constraint: MediaTrackProperty::from(""), - message: Default::default(), - } - } -} - -impl std::fmt::Display for OverconstrainedError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Over-constrained property {:?}", self.constraint)?; - if let Some(message) = self.message.as_ref() { - write!(f, ": {message}")?; - } - Ok(()) - } -} - -impl OverconstrainedError { - pub(super) fn exposing_device_information( - failed_constraints: HashMap, - ) -> Self { - let failed_constraint = failed_constraints - .into_iter() - .max_by_key(|(_, failure_info)| failure_info.failures); - - let (constraint, failure_info) = - failed_constraint.expect("Empty candidates implies non-empty failed constraints"); - - struct Violation { - constraint: String, - settings: Vec, - } - let mut violators_by_kind: HashMap = - HashMap::default(); - - for error in failure_info.errors { - let violation = violators_by_kind.entry(error.kind).or_insert(Violation { - constraint: error.constraint.clone(), - settings: vec![], - }); - assert_eq!(violation.constraint, error.constraint); - if let Some(setting) = error.setting { - violation.settings.push(setting.clone()); - } - } - - let formatted_reasons: Vec<_> = violators_by_kind - .into_iter() - .map(|(kind, violation)| { - let kind_str = match kind { - SettingFitnessDistanceErrorKind::Missing => "missing", - SettingFitnessDistanceErrorKind::Mismatch => "a mismatch", - SettingFitnessDistanceErrorKind::TooSmall => "too small", - SettingFitnessDistanceErrorKind::TooLarge => "too large", - }; - - let mut settings = violation.settings; - - if settings.is_empty() { - return format!("{} (does not satisfy {})", kind_str, violation.constraint); - } - - settings.sort(); - - format!( - "{} ([{}] do not satisfy {})", - kind_str, - settings.join(", "), - violation.constraint - ) - }) - .collect(); - - let formatted_reason = match &formatted_reasons[..] { - [] => unreachable!(), - [reason] => reason.clone(), - [reasons @ .., reason] => { - let reasons = reasons.join(", "); - format!("either {reasons}, or {reason}") - } - }; - let message = Some(format!("Setting was {formatted_reason}.")); - - Self { - constraint, - message, - } - } -} diff --git a/constraints/src/lib.rs b/constraints/src/lib.rs deleted file mode 100644 index dca56e368..000000000 --- a/constraints/src/lib.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Pure Rust implementation of the constraint logic defined in the ["Media Capture and Streams"][mediacapture_streams] spec. -//! -//! [mediacapture_streams]: https://www.w3.org/TR/mediacapture-streams/ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod algorithms; -pub mod errors; -pub mod macros; -pub mod property; - -mod capabilities; -mod capability; -mod constraint; -mod constraints; -mod enumerations; -mod setting; -mod settings; -mod supported_constraints; - -#[allow(unused_imports)] -pub(crate) use self::{capabilities::MediaStreamCapabilities, settings::MediaStreamSettings}; -#[allow(unused_imports)] -pub use self::{ - capabilities::MediaTrackCapabilities, - capability::MediaTrackCapability, - constraint::{ - MediaTrackConstraint, MediaTrackConstraintResolutionStrategy, ResolvedMediaTrackConstraint, - ResolvedValueConstraint, ResolvedValueRangeConstraint, ResolvedValueSequenceConstraint, - SanitizedMediaTrackConstraint, ValueConstraint, ValueRangeConstraint, - ValueSequenceConstraint, - }, - constraints::{ - AdvancedMediaTrackConstraints, BoolOrMediaTrackConstraints, MandatoryMediaTrackConstraints, - MediaStreamConstraints, MediaTrackConstraintSet, MediaTrackConstraints, - ResolvedAdvancedMediaTrackConstraints, ResolvedMandatoryMediaTrackConstraints, - ResolvedMediaTrackConstraintSet, ResolvedMediaTrackConstraints, - SanitizedMandatoryMediaTrackConstraints, SanitizedMediaTrackConstraintSet, - SanitizedMediaTrackConstraints, - }, - enumerations::{FacingMode, ResizeMode}, - property::MediaTrackProperty, - setting::MediaTrackSetting, - settings::MediaTrackSettings, - supported_constraints::MediaTrackSupportedConstraints, -}; diff --git a/constraints/src/macros.rs b/constraints/src/macros.rs deleted file mode 100644 index 73f518204..000000000 --- a/constraints/src/macros.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! Convenience macros. - -/// A convenience macro for defining settings. -#[macro_export] -macro_rules! settings { - [ - $($p:expr => $c:expr),* $(,)? - ] => { - <$crate::MediaTrackSettings as std::iter::FromIterator<_>>::from_iter([ - $(($p, $c.into())),* - ]) - }; -} - -pub use settings; - -/// A convenience macro for defining individual "value" constraints. -#[macro_export] -macro_rules! value_constraint { - ($($p:ident: $c:expr),+ $(,)?) => { - $crate::ValueConstraint::Constraint( - #[allow(clippy::needless_update)] - $crate::ResolvedValueConstraint { - $($p: Some($c)),+, - ..Default::default() - } - ) - }; - ($c:expr) => { - $crate::ValueConstraint::Bare($c) - }; -} - -pub use value_constraint; - -/// A convenience macro for defining individual "value range" constraints. -#[macro_export] -macro_rules! value_range_constraint { - {$($p:ident: $c:expr),+ $(,)?} => { - $crate::ValueRangeConstraint::Constraint( - $crate::ResolvedValueRangeConstraint { - $($p: Some($c)),+, - ..Default::default() - } - ) - }; - ($c:expr) => { - $crate::ValueRangeConstraint::Bare($c) - }; -} - -pub use value_range_constraint; - -/// A convenience macro for defining individual "value sequence" constraints. -#[macro_export] -macro_rules! value_sequence_constraint { - {$($p:ident: $c:expr),+ $(,)?} => { - $crate::ValueSequenceConstraint::Constraint( - $crate::ResolvedValueSequenceConstraint { - $($p: Some($c)),*, - ..Default::default() - } - ) - }; - ($c:expr) => { - $crate::ValueSequenceConstraint::Bare($c) - }; -} - -pub use value_sequence_constraint; - -/// A convenience macro for defining constraint sets. -#[macro_export] -macro_rules! constraint_set { - { - $($p:expr => $c:expr),* $(,)? - } => { - <$crate::MediaTrackConstraintSet as std::iter::FromIterator<_>>::from_iter([ - $(($p, $c.into())),* - ]) - }; -} - -pub use constraint_set; - -/// A convenience macro for defining "mandatory" constraints. -#[macro_export] -macro_rules! mandatory_constraints { - { - $($p:expr => $c:expr),* $(,)? - } => { - $crate::MandatoryMediaTrackConstraints::new( - constraint_set!{ - $($p => $c),* - } - ) - }; -} - -pub use mandatory_constraints; - -/// A convenience macro for defining "advanced" constraints. -#[macro_export] -macro_rules! advanced_constraints { - [ - $({ - $($p:expr => $c:expr),* $(,)? - }),* $(,)? - ] => { - <$crate::AdvancedMediaTrackConstraints as std::iter::FromIterator<_>>::from_iter([ - $(constraint_set!{ - $($p => $c),* - }),* - ]) - }; -} - -pub use advanced_constraints; - -/// A convenience macro for defining constraints. -#[macro_export] -macro_rules! constraints { - [ - mandatory: {$($mp:expr => $mc:expr),* $(,)?}, - advanced: [$( - {$($ap:expr => $ac:expr),* $(,)?} - ),* $(,)?] - ] => { - $crate::MediaTrackConstraints { - mandatory: mandatory_constraints!($($mp => $mc),*), - advanced: advanced_constraints!($({ $($ap => $ac),* }),*) - } - }; -} - -pub use constraints; - -#[allow(unused_macros)] -#[cfg(test)] -macro_rules! test_serde_symmetry { - (subject: $s:expr, json: $j:expr) => { - // Serialize: - { - let actual = serde_json::to_value($s.clone()).unwrap(); - let expected = $j.clone(); - - assert_eq!(actual, expected); - } - - // Deserialize: - { - let actual: Subject = serde_json::from_value($j).unwrap(); - let expected = $s; - - assert_eq!(actual, expected); - } - }; -} - -#[allow(unused_imports)] -#[cfg(test)] -pub(crate) use test_serde_symmetry; - -#[cfg(test)] -mod tests { - use crate::property::all::name::*; - use crate::{ - AdvancedMediaTrackConstraints, FacingMode, MandatoryMediaTrackConstraints, - MediaTrackConstraintSet, MediaTrackConstraints, MediaTrackSettings, - ResolvedValueConstraint, ResolvedValueRangeConstraint, ResolvedValueSequenceConstraint, - ValueConstraint, ValueRangeConstraint, ValueSequenceConstraint, - }; - - #[test] - fn settings() { - let actual: MediaTrackSettings = settings![ - &DEVICE_ID => "foobar".to_owned(), - &FRAME_RATE => 30.0, - &HEIGHT => 1080, - &FACING_MODE => FacingMode::user(), - ]; - - let expected = >::from_iter([ - (&DEVICE_ID, "foobar".to_owned().into()), - (&FRAME_RATE, 30.0.into()), - (&HEIGHT, 1080.into()), - (&FACING_MODE, FacingMode::user().into()), - ]); - - assert_eq!(actual, expected); - } - - mod constraint { - use super::*; - - #[test] - fn value() { - // Bare: - - let actual = value_constraint!("foobar".to_owned()); - - let expected = ValueConstraint::Bare("foobar".to_owned()); - - assert_eq!(actual, expected); - - // Constraint: - - let actual = value_constraint! { - exact: "foobar".to_owned(), - ideal: "bazblee".to_owned(), - }; - - let expected = ValueConstraint::Constraint( - ResolvedValueConstraint::default() - .exact("foobar".to_owned()) - .ideal("bazblee".to_owned()), - ); - - assert_eq!(actual, expected); - } - - #[test] - fn range() { - // Bare: - - let actual = value_range_constraint!(42); - - let expected = ValueRangeConstraint::Bare(42); - - assert_eq!(actual, expected); - - // Constraint: - - let actual = value_range_constraint! { - min: 30.0, - max: 60.0, - }; - - let expected = ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().min(30.0).max(60.0), - ); - - assert_eq!(actual, expected); - } - - #[test] - fn sequence() { - // Bare: - - let actual = value_sequence_constraint![vec![FacingMode::user()]]; - - let expected = ValueSequenceConstraint::Bare(vec![FacingMode::user()]); - - assert_eq!(actual, expected); - - // Constraint: - - let actual = value_sequence_constraint! { - ideal: vec![FacingMode::user()], - }; - - let expected = ValueSequenceConstraint::Constraint( - ResolvedValueSequenceConstraint::default().ideal(vec![FacingMode::user()]), - ); - - assert_eq!(actual, expected); - } - } - - #[test] - fn mandatory_constraints() { - let actual = mandatory_constraints! { - &DEVICE_ID => value_constraint! { - exact: "foobar".to_owned(), - ideal: "bazblee".to_owned(), - }, - &FRAME_RATE => value_range_constraint! { - min: 30.0, - max: 60.0, - }, - &FACING_MODE => value_sequence_constraint! { - exact: vec![FacingMode::user(), FacingMode::environment()] - }, - }; - - let expected = >::from_iter([ - ( - &DEVICE_ID, - ValueConstraint::Constraint( - ResolvedValueConstraint::default() - .exact("foobar".to_owned()) - .ideal("bazblee".to_owned()), - ) - .into(), - ), - ( - &FRAME_RATE, - ValueRangeConstraint::Constraint( - ResolvedValueRangeConstraint::default().min(30.0).max(60.0), - ) - .into(), - ), - ( - &FACING_MODE, - ValueSequenceConstraint::Constraint( - ResolvedValueSequenceConstraint::default() - .exact(vec![FacingMode::user(), FacingMode::environment()]), - ) - .into(), - ), - ]); - - assert_eq!(actual, expected); - } - - #[test] - fn advanced_constraints() { - let actual = advanced_constraints! [ - { - &DEVICE_ID => value_constraint! { - exact: "foobar".to_owned(), - ideal: "bazblee".to_owned(), - }, - }, - { - &FRAME_RATE => value_range_constraint! { - min: 30.0, - max: 60.0, - }, - }, - { - &FACING_MODE => value_sequence_constraint! { - exact: vec![FacingMode::user(), FacingMode::environment()] - }, - }, - ]; - - let expected = >::from_iter([ - >::from_iter([( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("foobar".to_owned()) - .ideal("bazblee".to_owned()) - .into(), - )]), - >::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint::default() - .min(30.0) - .max(60.0) - .into(), - )]), - >::from_iter([( - &FACING_MODE, - ResolvedValueSequenceConstraint::default() - .exact(vec![FacingMode::user(), FacingMode::environment()]) - .into(), - )]), - ]); - - assert_eq!(actual, expected); - } - - #[test] - fn constraints() { - let actual: MediaTrackConstraints = constraints!( - mandatory: { - &DEVICE_ID => value_constraint! { - exact: "foobar".to_owned(), - ideal: "bazblee".to_owned(), - }, - &FRAME_RATE => value_range_constraint! { - min: 30.0, - max: 60.0, - }, - &FACING_MODE => value_sequence_constraint! { - exact: vec![FacingMode::user(), FacingMode::environment()] - }, - }, - advanced: [ - { - &DEVICE_ID => value_constraint! { - exact: "foobar".to_owned(), - ideal: "bazblee".to_owned(), - }, - }, - { - &FRAME_RATE => value_range_constraint! { - min: 30.0, - max: 60.0, - }, - }, - { - &FACING_MODE => value_sequence_constraint! { - exact: vec![FacingMode::user(), FacingMode::environment()] - }, - }, - ] - ); - - let expected = MediaTrackConstraints { - mandatory: >::from_iter([ - ( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("foobar".to_owned()) - .ideal("bazblee".to_owned()) - .into(), - ), - ( - &FRAME_RATE, - ResolvedValueRangeConstraint::default() - .min(30.0) - .max(60.0) - .into(), - ), - ( - &FACING_MODE, - ResolvedValueSequenceConstraint::default() - .exact(vec![FacingMode::user(), FacingMode::environment()]) - .into(), - ), - ]), - advanced: >::from_iter([ - >::from_iter([( - &DEVICE_ID, - ResolvedValueConstraint::default() - .exact("foobar".to_owned()) - .ideal("bazblee".to_owned()) - .into(), - )]), - >::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint::default() - .min(30.0) - .max(60.0) - .into(), - )]), - >::from_iter([( - &FACING_MODE, - ResolvedValueSequenceConstraint::default() - .exact(vec![FacingMode::user(), FacingMode::environment()]) - .into(), - )]), - ]), - }; - - assert_eq!(actual, expected); - } -} diff --git a/constraints/src/property.rs b/constraints/src/property.rs deleted file mode 100644 index 473095773..000000000 --- a/constraints/src/property.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Constants identifying the properties of a [`MediaStreamTrack`][media_stream_track] object, -//! as defined in the ["Media Capture and Streams"][media_track_supported_constraints] spec. -//! -//! [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#mediastreamtrack -//! [media_track_supported_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksupportedconstraints - -use std::borrow::Cow; -use std::fmt::Display; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// An identifier for a media track property. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub struct MediaTrackProperty(Cow<'static, str>); - -impl From<&MediaTrackProperty> for MediaTrackProperty { - fn from(borrowed: &MediaTrackProperty) -> Self { - borrowed.clone() - } -} - -impl From for MediaTrackProperty { - /// Creates a property from an owned representation of its name. - fn from(owned: String) -> Self { - Self(Cow::Owned(owned)) - } -} - -impl From<&str> for MediaTrackProperty { - /// Creates a property from an owned representation of its name. - /// - /// Use `MediaTrackProperty::named(str)` if your property name - /// is statically borrowed (i.e. `&'static str`). - fn from(borrowed: &str) -> Self { - Self(Cow::Owned(borrowed.to_owned())) - } -} - -impl Display for MediaTrackProperty { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} - -impl MediaTrackProperty { - /// Creates a property from a statically borrowed representation of its name. - pub const fn named(name: &'static str) -> Self { - Self(Cow::Borrowed(name)) - } - - /// The property's name. - pub fn name(&self) -> &str { - &self.0 - } -} - -/// Standard properties that apply to both, audio and video device types. -pub mod common { - use super::*; - - /// Names of common properties. - pub mod name { - use super::*; - - /// The identifier of the device generating the content of the track, - /// as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-deviceid - pub static DEVICE_ID: MediaTrackProperty = MediaTrackProperty::named("deviceId"); - - /// The document-unique group identifier for the device generating the content - /// of the track, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-groupid - pub static GROUP_ID: MediaTrackProperty = MediaTrackProperty::named("groupId"); - } - - /// Names of common properties. - pub fn names() -> Vec<&'static MediaTrackProperty> { - use self::name::*; - - vec![&DEVICE_ID, &GROUP_ID] - } -} - -/// Standard properties that apply only to audio device types. -pub mod audio_only { - use super::*; - - /// Names of audio-only properties. - pub mod name { - use super::*; - - /// Automatic gain control is often desirable on the input signal recorded - /// by the microphone, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-autogaincontrol - pub static AUTO_GAIN_CONTROL: MediaTrackProperty = - MediaTrackProperty::named("autoGainControl"); - - /// The number of independent channels of sound that the audio data contains, - /// i.e. the number of audio samples per sample frame, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-channelcount - pub static CHANNEL_COUNT: MediaTrackProperty = MediaTrackProperty::named("channelCount"); - - /// When one or more audio streams is being played in the processes of - /// various microphones, it is often desirable to attempt to remove - /// all the sound being played from the input signals recorded by the microphones. - /// This is referred to as echo cancellation, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-echocancellation - pub static ECHO_CANCELLATION: MediaTrackProperty = - MediaTrackProperty::named("echoCancellation"); - - /// The latency or latency range, in seconds, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-latency - pub static LATENCY: MediaTrackProperty = MediaTrackProperty::named("latency"); - - /// Noise suppression is often desirable on the input signal recorded by the microphone, - /// as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-noisesuppression - pub static NOISE_SUPPRESSION: MediaTrackProperty = - MediaTrackProperty::named("noiseSuppression"); - - /// The sample rate in samples per second for the audio data, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-samplerate - pub static SAMPLE_RATE: MediaTrackProperty = MediaTrackProperty::named("sampleRate"); - - /// The linear sample size in bits. This constraint can only - /// be satisfied for audio devices that produce linear samples, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-samplesize - pub static SAMPLE_SIZE: MediaTrackProperty = MediaTrackProperty::named("sampleSize"); - } - - /// Names of all audio-only properties. - pub fn names() -> Vec<&'static MediaTrackProperty> { - use self::name::*; - - vec![ - &AUTO_GAIN_CONTROL, - &CHANNEL_COUNT, - &ECHO_CANCELLATION, - &LATENCY, - &NOISE_SUPPRESSION, - &SAMPLE_RATE, - &SAMPLE_SIZE, - ] - } -} - -/// Standard properties that apply only to video device types. -pub mod video_only { - use super::*; - - /// Names of audio-only properties. - pub mod name { - use super::*; - - /// The exact aspect ratio (width in pixels divided by height in pixels, - /// represented as a double rounded to the tenth decimal place), - /// as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-aspectratio - pub static ASPECT_RATIO: MediaTrackProperty = MediaTrackProperty::named("aspectRatio"); - - /// The directions that the camera can face, as seen from the user's perspective, - /// as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-facingmode - pub static FACING_MODE: MediaTrackProperty = MediaTrackProperty::named("facingMode"); - - /// The exact frame rate (frames per second) or frame rate range, - /// as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-framerate - pub static FRAME_RATE: MediaTrackProperty = MediaTrackProperty::named("frameRate"); - - /// The height or height range, in pixels, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-height - pub static HEIGHT: MediaTrackProperty = MediaTrackProperty::named("height"); - - /// The width or width range, in pixels, as defined in the [spec][spec]. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-width - pub static WIDTH: MediaTrackProperty = MediaTrackProperty::named("width"); - - /// The means by which the resolution can be derived by the client, as defined in the [spec][spec]. - /// - /// In other words, whether the client is allowed to use cropping and downscaling on the camera output. - /// - /// [spec]: https://www.w3.org/TR/mediacapture-streams/#dfn-resizemode - pub static RESIZE_MODE: MediaTrackProperty = MediaTrackProperty::named("resizeMode"); - } - - /// Names of all video-only properties. - pub fn names() -> Vec<&'static MediaTrackProperty> { - use self::name::*; - vec![ - &ASPECT_RATIO, - &FACING_MODE, - &FRAME_RATE, - &HEIGHT, - &WIDTH, - &RESIZE_MODE, - ] - } -} - -/// The union of all standard properties (i.e. common + audio + video). -pub mod all { - use super::*; - - /// Names of all properties. - pub mod name { - pub use super::audio_only::name::*; - pub use super::common::name::*; - pub use super::video_only::name::*; - } - - /// Names of all properties. - pub fn names() -> Vec<&'static MediaTrackProperty> { - let mut all = vec![]; - all.append(&mut self::common::names()); - all.append(&mut self::audio_only::names()); - all.append(&mut self::video_only::names()); - all - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackProperty; - - mod from { - use super::*; - - #[test] - fn owned() { - let actuals = [Subject::from("string"), Subject::from("string".to_owned())]; - let expected = MediaTrackProperty(Cow::Owned("string".to_owned())); - - for actual in actuals { - assert_eq!(actual, expected); - - // TODO: remove feature-gate, once stabilized: - #[cfg(feature = "cow_is_borrowed")] - assert!(actual.0.is_owned()); - } - } - - #[test] - fn borrowed() { - let actual = Subject::named("string"); - let expected = MediaTrackProperty(Cow::Borrowed("string")); - - assert_eq!(actual, expected); - - // TODO: remove feature-gate, once stabilized: - #[cfg(feature = "cow_is_borrowed")] - assert!(actual.0.is_borrowed()); - } - } - - #[test] - fn name() { - assert_eq!(Subject::named("string").name(), "string"); - } - - #[test] - fn to_string() { - assert_eq!(Subject::named("string").to_string(), "string"); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackProperty; - - #[test] - fn is_symmetric() { - let subject = Subject::named("string"); - let json = serde_json::json!("string"); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/setting.rs b/constraints/src/setting.rs deleted file mode 100644 index 15dc24c64..000000000 --- a/constraints/src/setting.rs +++ /dev/null @@ -1,159 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A single [setting][media_track_settings] value of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_settings]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksettings -/// [media_track_supported_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksupportedconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum MediaTrackSetting { - /// A boolean-valued track setting. - Bool(bool), - /// An integer-valued track setting. - Integer(i64), - /// A floating-point-valued track setting. - Float(f64), - /// A string-valued track setting. - String(String), -} - -impl std::fmt::Display for MediaTrackSetting { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Bool(setting) => f.write_fmt(format_args!("{setting:?}")), - Self::Integer(setting) => f.write_fmt(format_args!("{setting:?}")), - Self::Float(setting) => f.write_fmt(format_args!("{setting:?}")), - Self::String(setting) => f.write_fmt(format_args!("{setting:?}")), - } - } -} - -impl From for MediaTrackSetting { - fn from(setting: bool) -> Self { - Self::Bool(setting) - } -} - -impl From for MediaTrackSetting { - fn from(setting: i64) -> Self { - Self::Integer(setting) - } -} - -impl From for MediaTrackSetting { - fn from(setting: f64) -> Self { - Self::Float(setting) - } -} - -impl From for MediaTrackSetting { - fn from(setting: String) -> Self { - Self::String(setting) - } -} - -impl<'a> From<&'a str> for MediaTrackSetting { - fn from(setting: &'a str) -> Self { - Self::String(setting.to_owned()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Subject = MediaTrackSetting; - - mod from { - use super::*; - - #[test] - fn bool() { - let actual = Subject::from(true); - let expected = Subject::Bool(true); - - assert_eq!(actual, expected); - } - - #[test] - fn integer() { - let actual = Subject::from(42); - let expected = Subject::Integer(42); - - assert_eq!(actual, expected); - } - - #[test] - fn float() { - let actual = Subject::from(4.2); - let expected = Subject::Float(4.2); - - assert_eq!(actual, expected); - } - - #[test] - fn string() { - let actual = Subject::from("string".to_owned()); - let expected = Subject::String("string".to_owned()); - - assert_eq!(actual, expected); - } - } - - #[test] - fn to_string() { - assert_eq!(Subject::from(true).to_string(), "true"); - assert_eq!(Subject::from(42).to_string(), "42"); - assert_eq!(Subject::from(4.2).to_string(), "4.2"); - assert_eq!(Subject::from("string".to_owned()).to_string(), "\"string\""); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaTrackSetting; - - #[test] - fn bool() { - let subject = Subject::Bool(true); - let json = serde_json::json!(true); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn integer() { - let subject = Subject::Integer(42); - let json = serde_json::json!(42); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn float() { - let subject = Subject::Float(4.2); - let json = serde_json::json!(4.2); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn string() { - let subject = Subject::String("string".to_owned()); - let json = serde_json::json!("string"); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/settings.rs b/constraints/src/settings.rs deleted file mode 100644 index b69a2c76a..000000000 --- a/constraints/src/settings.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod stream; -mod track; - -pub(crate) use self::stream::MediaStreamSettings; -pub use self::track::MediaTrackSettings; diff --git a/constraints/src/settings/stream.rs b/constraints/src/settings/stream.rs deleted file mode 100644 index 79f99cc99..000000000 --- a/constraints/src/settings/stream.rs +++ /dev/null @@ -1,58 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::MediaTrackSettings; - -/// The settings of a [`MediaStream`][media_stream] object. -/// -/// # W3C Spec Compliance -/// -/// There exists no corresponding type in the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// [media_stream]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastream -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -#[derive(Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub(crate) struct MediaStreamSettings { - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub audio: Option, - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "core::option::Option::is_none") - )] - pub video: Option, -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - - type Subject = MediaStreamSettings; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = MediaStreamSettings { - audio: Some(MediaTrackSettings::default()), - video: None, - }; - let json = serde_json::json!({ - "audio": {} - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/settings/track.rs b/constraints/src/settings/track.rs deleted file mode 100644 index 625e7bc4d..000000000 --- a/constraints/src/settings/track.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::collections::HashMap; -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::{MediaTrackProperty, MediaTrackSetting}; - -/// The settings of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaTrackSettings`][media_track_settings] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// The W3C spec defines `MediaTrackSettings` in terma of a dictionary, -/// which per the [WebIDL spec][webidl_spec] is an ordered map (e.g. [`IndexMap`][index_map]). -/// Since the spec however does not make use of the order of items -/// in the map we use a simple [`HashMap`][hash_map]. -/// -/// [hash_map]: https://doc.rust-lang.org/std/collections/struct.HashMap.html -/// [index_map]: https://docs.rs/indexmap/latest/indexmap/set/struct.IndexMap.html -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_settings]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksettings -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -/// [webidl_spec]: https://webidl.spec.whatwg.org/#idl-dictionaries -#[derive(Debug, Clone, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct MediaTrackSettings(HashMap); - -impl MediaTrackSettings { - /// Creates a settings value from its inner hashmap. - pub fn new(settings: HashMap) -> Self { - Self(settings) - } - - /// Consumes the value, returning its inner hashmap. - pub fn into_inner(self) -> HashMap { - self.0 - } -} - -impl Deref for MediaTrackSettings { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MediaTrackSettings { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator<(T, MediaTrackSetting)> for MediaTrackSettings -where - T: Into, -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - Self::new(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) - } -} - -impl IntoIterator for MediaTrackSettings { - type Item = (MediaTrackProperty, MediaTrackSetting); - type IntoIter = std::collections::hash_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::property::all::name::*; - - type Subject = MediaTrackSettings; - - #[test] - fn into_inner() { - let hash_map = HashMap::from_iter([ - (DEVICE_ID.clone(), "device-id".into()), - (AUTO_GAIN_CONTROL.clone(), true.into()), - (CHANNEL_COUNT.clone(), 20.into()), - (LATENCY.clone(), 2.0.into()), - ]); - - let subject = Subject::new(hash_map.clone()); - - let actual = subject.into_inner(); - - let expected = hash_map; - - assert_eq!(actual, expected); - } - - #[test] - fn into_iter() { - let hash_map = HashMap::from_iter([ - (DEVICE_ID.clone(), "device-id".into()), - (AUTO_GAIN_CONTROL.clone(), true.into()), - (CHANNEL_COUNT.clone(), 20.into()), - (LATENCY.clone(), 2.0.into()), - ]); - - let subject = Subject::new(hash_map.clone()); - - let actual: HashMap<_, _> = subject.into_iter().collect(); - - let expected = hash_map; - - assert_eq!(actual, expected); - } - - #[test] - fn deref_and_deref_mut() { - let mut subject = Subject::default(); - - // Deref mut: - subject.insert(DEVICE_ID.clone(), "device-id".into()); - - // Deref: - assert!(subject.contains_key(&DEVICE_ID)); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - use crate::property::all::name::*; - - type Subject = MediaTrackSettings; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({}); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject::from_iter([ - (&DEVICE_ID, "device-id".into()), - (&AUTO_GAIN_CONTROL, true.into()), - (&CHANNEL_COUNT, 2.into()), - (&LATENCY, 0.123.into()), - ]); - let json = serde_json::json!({ - "deviceId": "device-id".to_owned(), - "autoGainControl": true, - "channelCount": 2, - "latency": 0.123, - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/src/supported_constraints.rs b/constraints/src/supported_constraints.rs deleted file mode 100644 index 21ca96b08..000000000 --- a/constraints/src/supported_constraints.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::collections::HashSet; -use std::iter::FromIterator; -use std::ops::{Deref, DerefMut}; - -#[cfg(feature = "serde")] -use serde::{ - de::{MapAccess, Visitor}, - ser::SerializeMap, - Deserialize, Deserializer, Serialize, Serializer, -}; - -use crate::MediaTrackProperty; - -/// The list of constraints recognized by a User Agent for controlling the -/// capabilities of a [`MediaStreamTrack`][media_stream_track] object. -/// -/// # W3C Spec Compliance -/// -/// Corresponds to [`MediaTrackSupportedConstraints`][media_track_supported_constraints] -/// from the W3C ["Media Capture and Streams"][media_capture_and_streams_spec] spec. -/// -/// The W3C spec defines `MediaTrackSupportedConstraints` in terma of a dictionary, -/// which per the [WebIDL spec][webidl_spec] is an ordered map (e.g. [`IndexSet`][index_set]). -/// Since the spec however does not make use of the order of items -/// in the map we use a simple `HashSet`. -/// -/// [hash_set]: https://doc.rust-lang.org/std/collections/struct.HashSet.html -/// [index_set]: https://docs.rs/indexmap/latest/indexmap/set/struct.IndexSet.html -/// [media_stream_track]: https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -/// [media_track_supported_constraints]: https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksupportedconstraints -/// [media_capture_and_streams_spec]: https://www.w3.org/TR/mediacapture-streams -/// [webidl_spec]: https://webidl.spec.whatwg.org/#idl-dictionaries -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MediaTrackSupportedConstraints(HashSet); - -impl MediaTrackSupportedConstraints { - /// Creates a supported constraints value from its inner hashmap. - pub fn new(properties: HashSet) -> Self { - Self(properties) - } - - /// Consumes the value, returning its inner hashmap. - pub fn into_inner(self) -> HashSet { - self.0 - } -} - -impl Deref for MediaTrackSupportedConstraints { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MediaTrackSupportedConstraints { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for MediaTrackSupportedConstraints { - /// [Default values][default_values] as defined by the W3C specification. - /// - /// [default_values]: https://www.w3.org/TR/mediacapture-streams/#dictionary-mediatracksupportedconstraints-members - fn default() -> Self { - use crate::property::all::names as property_names; - - Self::from_iter(property_names().into_iter().cloned()) - } -} - -impl FromIterator for MediaTrackSupportedConstraints -where - T: Into, -{ - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - Self(iter.into_iter().map(|property| property.into()).collect()) - } -} - -impl IntoIterator for MediaTrackSupportedConstraints { - type Item = MediaTrackProperty; - type IntoIter = std::collections::hash_set::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for MediaTrackSupportedConstraints { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(SerdeVisitor) - } -} - -#[cfg(feature = "serde")] -impl Serialize for MediaTrackSupportedConstraints { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(self.0.len()))?; - for property in &self.0 { - map.serialize_entry(property, &true)?; - } - map.end() - } -} - -#[cfg(feature = "serde")] -struct SerdeVisitor; - -#[cfg(feature = "serde")] -impl<'de> Visitor<'de> for SerdeVisitor { - type Value = MediaTrackSupportedConstraints; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("an object with strings as keys and `true` as values") - } - - fn visit_map(self, mut access: M) -> Result - where - M: MapAccess<'de>, - { - let mut set = HashSet::with_capacity(access.size_hint().unwrap_or(0)); - while let Some((key, value)) = access.next_entry()? { - if value { - set.insert(key); - } - } - Ok(MediaTrackSupportedConstraints(set)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::property::all::name::*; - - type Subject = MediaTrackSupportedConstraints; - - #[test] - fn into_inner() { - let hash_set = HashSet::from_iter([ - DEVICE_ID.clone(), - AUTO_GAIN_CONTROL.clone(), - CHANNEL_COUNT.clone(), - LATENCY.clone(), - ]); - - let subject = Subject::new(hash_set.clone()); - - let actual = subject.into_inner(); - - let expected = hash_set; - - assert_eq!(actual, expected); - } - - #[test] - fn into_iter() { - let hash_set = HashSet::from_iter([ - DEVICE_ID.clone(), - AUTO_GAIN_CONTROL.clone(), - CHANNEL_COUNT.clone(), - LATENCY.clone(), - ]); - - let subject = Subject::new(hash_set.clone()); - - let actual: HashSet<_, _> = subject.into_iter().collect(); - - let expected = hash_set; - - assert_eq!(actual, expected); - } - - #[test] - fn deref_and_deref_mut() { - let mut subject = Subject::default(); - - // Deref mut: - subject.insert(DEVICE_ID.clone()); - - // Deref: - assert!(subject.contains(&DEVICE_ID)); - } -} - -#[cfg(feature = "serde")] -#[cfg(test)] -mod serde_tests { - use super::*; - use crate::macros::test_serde_symmetry; - use crate::property::all::name::*; - - type Subject = MediaTrackSupportedConstraints; - - #[test] - fn default() { - let subject = Subject::default(); - let json = serde_json::json!({ - "deviceId": true, - "groupId": true, - "autoGainControl": true, - "channelCount": true, - "echoCancellation": true, - "latency": true, - "noiseSuppression": true, - "sampleRate": true, - "sampleSize": true, - "aspectRatio": true, - "facingMode": true, - "frameRate": true, - "height": true, - "width": true, - "resizeMode": true, - }); - - test_serde_symmetry!(subject: subject, json: json); - } - - #[test] - fn customized() { - let subject = Subject::from_iter([ - &DEVICE_ID, - &GROUP_ID, - &AUTO_GAIN_CONTROL, - &CHANNEL_COUNT, - &ASPECT_RATIO, - &FACING_MODE, - ]); - let json = serde_json::json!({ - "deviceId": true, - "groupId": true, - "autoGainControl": true, - "channelCount": true, - "aspectRatio": true, - "facingMode": true - }); - - test_serde_symmetry!(subject: subject, json: json); - } -} diff --git a/constraints/tests/w3c_spec_examples.rs b/constraints/tests/w3c_spec_examples.rs deleted file mode 100644 index f46144f00..000000000 --- a/constraints/tests/w3c_spec_examples.rs +++ /dev/null @@ -1,216 +0,0 @@ -#[cfg(feature = "serde")] -use webrtc_constraints::{ - property::all::name::*, AdvancedMediaTrackConstraints, BoolOrMediaTrackConstraints, - MediaTrackConstraintSet, MediaTrackConstraints, ResolvedValueRangeConstraint, - ValueRangeConstraint, -}; - -// -#[cfg(feature = "serde")] -#[test] -fn w3c_spec_example_1() { - use std::iter::FromIterator; - - use webrtc_constraints::{MandatoryMediaTrackConstraints, MediaStreamConstraints}; - - let actual: MediaStreamConstraints = { - let json = serde_json::json!({ - "video": { - "width": 1280, - "height": 720, - "aspectRatio": 1.5, - } - }); - serde_json::from_value(json).unwrap() - }; - let expected = MediaStreamConstraints { - audio: BoolOrMediaTrackConstraints::Bool(false), - video: BoolOrMediaTrackConstraints::Constraints(MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - (&WIDTH, 1280.into()), - (&HEIGHT, 720.into()), - (&ASPECT_RATIO, 1.5.into()), - ]), - advanced: AdvancedMediaTrackConstraints::default(), - }), - }; - - assert_eq!(actual, expected); -} - -// -#[cfg(feature = "serde")] -#[test] -fn w3c_spec_example_2() { - use std::iter::FromIterator; - - use webrtc_constraints::{MandatoryMediaTrackConstraints, MediaStreamConstraints}; - - let actual: MediaStreamConstraints = { - let json = serde_json::json!({ - "video": { - "width": { "min": 640, "ideal": 1280 }, - "height": { "min": 480, "ideal": 720 }, - "aspectRatio": 1.5, - "frameRate": { "min": 20.0 }, - } - }); - serde_json::from_value(json).unwrap() - }; - - let expected = MediaStreamConstraints { - audio: BoolOrMediaTrackConstraints::Bool(false), - video: BoolOrMediaTrackConstraints::Constraints(MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - ( - &WIDTH, - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint { - min: Some(640), - max: None, - exact: None, - ideal: Some(1280), - }) - .into(), - ), - ( - &HEIGHT, - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint { - min: Some(480), - max: None, - exact: None, - ideal: Some(720), - }) - .into(), - ), - (&ASPECT_RATIO, ValueRangeConstraint::Bare(1.5).into()), - ( - &FRAME_RATE, - ValueRangeConstraint::Constraint(ResolvedValueRangeConstraint { - min: Some(20.0), - max: None, - exact: None, - ideal: None, - }) - .into(), - ), - ]), - advanced: AdvancedMediaTrackConstraints::default(), - }), - }; - - assert_eq!(actual, expected); -} - -// -#[cfg(feature = "serde")] -#[test] -fn w3c_spec_example_3() { - use std::iter::FromIterator; - - use webrtc_constraints::{MandatoryMediaTrackConstraints, MediaStreamConstraints}; - - let actual: MediaStreamConstraints = { - let json = serde_json::json!({ - "video": { - "height": { "min": 480, "ideal": 720 }, - "width": { "min": 640, "ideal": 1280 }, - "frameRate": { "min": 30.0 }, - "advanced": [ - {"width": 1920, "height": 1280 }, - {"aspectRatio": 1.333}, - {"frameRate": {"min": 50.0 } }, - {"frameRate": {"min": 40.0 } } - ] - } - }); - serde_json::from_value(json).unwrap() - }; - - let expected = MediaStreamConstraints { - audio: BoolOrMediaTrackConstraints::Bool(false), - video: BoolOrMediaTrackConstraints::Constraints(MediaTrackConstraints { - mandatory: MandatoryMediaTrackConstraints::from_iter([ - ( - &HEIGHT, - ResolvedValueRangeConstraint { - min: Some(480), - max: None, - exact: None, - ideal: Some(720), - } - .into(), - ), - ( - &WIDTH, - ResolvedValueRangeConstraint { - min: Some(640), - max: None, - exact: None, - ideal: Some(1280), - } - .into(), - ), - ( - &FRAME_RATE, - ResolvedValueRangeConstraint { - min: Some(30.0), - max: None, - exact: None, - ideal: None, - } - .into(), - ), - ]), - advanced: AdvancedMediaTrackConstraints::new(vec![ - MediaTrackConstraintSet::from_iter([(&WIDTH, 1920.into()), (&HEIGHT, 1280.into())]), - MediaTrackConstraintSet::from_iter([(&ASPECT_RATIO, 1.333.into())]), - MediaTrackConstraintSet::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint { - min: Some(50.0), - max: None, - exact: None, - ideal: None, - } - .into(), - )]), - MediaTrackConstraintSet::from_iter([( - &FRAME_RATE, - ResolvedValueRangeConstraint { - min: Some(40.0), - max: None, - exact: None, - ideal: None, - } - .into(), - )]), - ]), - }), - }; - - assert_eq!(actual, expected); -} - -// -#[cfg(feature = "serde")] -#[test] -fn w3c_spec_example_4() { - use std::iter::FromIterator; - - let actual: MediaTrackConstraintSet = { - let json = serde_json::json!({ - "width": 1920, - "height": 1080, - "frameRate": 30, - }); - serde_json::from_value(json).unwrap() - }; - - let expected = MediaTrackConstraintSet::from_iter([ - (&WIDTH, ValueRangeConstraint::Bare(1920).into()), - (&HEIGHT, ValueRangeConstraint::Bare(1080).into()), - (&FRAME_RATE, ValueRangeConstraint::Bare(30).into()), - ]); - - assert_eq!(actual, expected); -} diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/data/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/data/CHANGELOG.md b/data/CHANGELOG.md deleted file mode 100644 index f632b1312..000000000 --- a/data/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# webrtc-data changelog - -## Unreleased - -* Remove builder pattern from `data_channel::Config` [#411](https://github.com/webrtc-rs/webrtc/pull/411). - -## v0.7.0 - -* Increased required `webrtc-sctp` version to `0.8.0`. - -## v0.6.0 - -* Increased minimum support rust version to `1.60.0`. -* Do not loose data in `PollDataChannel::poll_write` [#341](https://github.com/webrtc-rs/webrtc/pull/341). -* `PollDataChannel::poll_shutdown`: make sure to flush any writes before shutting down [#340](https://github.com/webrtc-rs/webrtc/pull/340) -* Increased required `webrtc-util` version to `0.7.0`. -* Increased required `webrtc-sctp` version to `0.7.0`. - -### Breaking changes - -* Make `DataChannel::on_buffered_amount_low` function non-async [#338](https://github.com/webrtc-rs/webrtc/pull/338). - -## v0.5.0 - -* [#16 [PollDataChannel] reset shutdown_fut future after done](https://github.com/webrtc-rs/data/pull/16) by [@melekes](https://github.com/melekes). -* Increase min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). - -## Prior to 0.4.0 - -Before 0.4.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/data/releases). diff --git a/data/Cargo.toml b/data/Cargo.toml deleted file mode 100644 index f03fb72c7..000000000 --- a/data/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "webrtc-data" -version = "0.9.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of WebRTC DataChannel API" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-data" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/data" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn", "marshal"] } -sctp = { version = "0.10.0", path = "../sctp", package = "webrtc-sctp" } - -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -bytes = "1" -log = "0.4" -thiserror = "1" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" # must match the min version of the `tokio` crate above -env_logger = "0.10" -chrono = "0.4.28" diff --git a/data/LICENSE-APACHE b/data/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/data/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/data/LICENSE-MIT b/data/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/data/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/data/README.md b/data/README.md deleted file mode 100644 index 8230caac5..000000000 --- a/data/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of WebRTC DataChannel. Rewrite Pion DataChannel in Rust -

diff --git a/data/codecov.yml b/data/codecov.yml deleted file mode 100644 index 616770e51..000000000 --- a/data/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 00d131c6-1478-4018-b481-be1b44f5f094 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/data/doc/webrtc.rs.png b/data/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/data/src/data_channel/data_channel_test.rs b/data/src/data_channel/data_channel_test.rs deleted file mode 100644 index 382ae85b9..000000000 --- a/data/src/data_channel/data_channel_test.rs +++ /dev/null @@ -1,670 +0,0 @@ -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::sync::{broadcast, mpsc}; -use tokio::time::Duration; -use util::conn::conn_bridge::*; -use util::conn::*; - -use super::*; -use crate::error::Result; - -async fn bridge_process_at_least_one(br: &Arc) { - let mut n_sum = 0; - loop { - tokio::time::sleep(Duration::from_millis(10)).await; - n_sum += br.tick().await; - if br.len(0).await == 0 && br.len(1).await == 0 && n_sum > 0 { - break; - } - } -} - -async fn create_new_association_pair( - br: &Arc, - ca: Arc, - cb: Arc, -) -> Result<(Arc, Arc)> { - let (handshake0ch_tx, mut handshake0ch_rx) = mpsc::channel(1); - let (handshake1ch_tx, mut handshake1ch_rx) = mpsc::channel(1); - let (closed_tx, mut closed_rx0) = broadcast::channel::<()>(1); - let mut closed_rx1 = closed_tx.subscribe(); - - // Setup client - tokio::spawn(async move { - let client = Association::client(sctp::association::Config { - net_conn: ca, - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }) - .await; - - let _ = handshake0ch_tx.send(client).await; - let _ = closed_rx0.recv().await; - - Result::<()>::Ok(()) - }); - - // Setup server - tokio::spawn(async move { - let server = Association::server(sctp::association::Config { - net_conn: cb, - max_receive_buffer_size: 0, - max_message_size: 0, - name: "server".to_owned(), - }) - .await; - - let _ = handshake1ch_tx.send(server).await; - let _ = closed_rx1.recv().await; - - Result::<()>::Ok(()) - }); - - let mut client = None; - let mut server = None; - let mut a0handshake_done = false; - let mut a1handshake_done = false; - let mut i = 0; - while (!a0handshake_done || !a1handshake_done) && i < 100 { - br.tick().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - r0 = handshake0ch_rx.recv() => { - if let Ok(c) = r0.unwrap() { - client = Some(c); - } - a0handshake_done = true; - }, - r1 = handshake1ch_rx.recv() => { - if let Ok(s) = r1.unwrap() { - server = Some(s); - } - a1handshake_done = true; - }, - }; - i += 1; - } - - if !a0handshake_done || !a1handshake_done { - return Err(Error::new("handshake failed".to_owned())); - } - - drop(closed_tx); - - Ok((Arc::new(client.unwrap()), Arc::new(server.unwrap()))) -} - -async fn close_association_pair( - br: &Arc, - client: Arc, - server: Arc, -) { - let (handshake0ch_tx, mut handshake0ch_rx) = mpsc::channel(1); - let (handshake1ch_tx, mut handshake1ch_rx) = mpsc::channel(1); - let (closed_tx, mut closed_rx0) = broadcast::channel::<()>(1); - let mut closed_rx1 = closed_tx.subscribe(); - - // Close client - tokio::spawn(async move { - client.close().await?; - let _ = handshake0ch_tx.send(()).await; - let _ = closed_rx0.recv().await; - - Result::<()>::Ok(()) - }); - - // Close server - tokio::spawn(async move { - server.close().await?; - let _ = handshake1ch_tx.send(()).await; - let _ = closed_rx1.recv().await; - - Result::<()>::Ok(()) - }); - - let mut a0handshake_done = false; - let mut a1handshake_done = false; - let mut i = 0; - while (!a0handshake_done || !a1handshake_done) && i < 100 { - br.tick().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - _ = handshake0ch_rx.recv() => { - a0handshake_done = true; - }, - _ = handshake1ch_rx.recv() => { - a1handshake_done = true; - }, - }; - i += 1; - } - - drop(closed_tx); -} - -//use std::io::Write; - -async fn pr_ordered_unordered_test(channel_type: ChannelType, is_ordered: bool) -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let mut sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 2000]; - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let cfg = Config { - channel_type, - reliability_parameter: 0, - label: "data".to_string(), - ..Default::default() - }; - - let dc0 = DataChannel::dial(&a0, 100, cfg.clone()).await?; - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?; - bridge_process_at_least_one(&br).await; - - assert_eq!(dc0.config, cfg, "local config should match"); - assert_eq!(dc1.config, cfg, "remote config should match"); - - dc0.commit_reliability_params(); - dc1.commit_reliability_params(); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = dc0 - .write_data_channel(&Bytes::from(sbuf.clone()), true) - .await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - sbuf[0..4].copy_from_slice(&2u32.to_be_bytes()); - let n = dc0 - .write_data_channel(&Bytes::from(sbuf.clone()), true) - .await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - if !is_ordered { - sbuf[0..4].copy_from_slice(&3u32.to_be_bytes()); - let n = dc0 - .write_data_channel(&Bytes::from(sbuf.clone()), true) - .await?; - assert_eq!(sbuf.len(), n, "data length should match"); - } - - tokio::time::sleep(Duration::from_millis(100)).await; - br.drop_offset(0, 0, 1).await; // drop the first packet on the wire - if !is_ordered { - br.reorder(0).await; - } else { - tokio::time::sleep(Duration::from_millis(100)).await; - } - bridge_process_at_least_one(&br).await; - - if !is_ordered { - let (n, is_string) = dc1.read_data_channel(&mut rbuf[..]).await?; - assert!(is_string, "should return isString being true"); - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 3, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - } - - let (n, is_string) = dc1.read_data_channel(&mut rbuf[..]).await?; - assert!(is_string, "should return isString being true"); - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 2, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - dc0.close().await?; - dc1.close().await?; - bridge_process_at_least_one(&br).await; - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_channel_type_reliable_ordered() -> Result<()> { - let mut sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1500]; - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let cfg = Config { - channel_type: ChannelType::Reliable, - reliability_parameter: 123, - label: "data".to_string(), - ..Default::default() - }; - - let dc0 = DataChannel::dial(&a0, 100, cfg.clone()).await?; - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?; - bridge_process_at_least_one(&br).await; - - assert_eq!(dc0.config, cfg, "local config should match"); - assert_eq!(dc1.config, cfg, "remote config should match"); - - br.reorder_next_nwrites(0, 2); // reordering on the wire - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = dc0.write(&Bytes::from(sbuf.clone())).await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - sbuf[0..4].copy_from_slice(&2u32.to_be_bytes()); - let n = dc0.write(&Bytes::from(sbuf.clone())).await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - bridge_process_at_least_one(&br).await; - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 1, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 2, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - dc0.close().await?; - dc1.close().await?; - bridge_process_at_least_one(&br).await; - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_channel_type_reliable_unordered() -> Result<()> { - let mut sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1500]; - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let cfg = Config { - channel_type: ChannelType::ReliableUnordered, - reliability_parameter: 123, - label: "data".to_string(), - ..Default::default() - }; - - let dc0 = DataChannel::dial(&a0, 100, cfg.clone()).await?; - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?; - bridge_process_at_least_one(&br).await; - - assert_eq!(dc0.config, cfg, "local config should match"); - assert_eq!(dc1.config, cfg, "remote config should match"); - - dc0.commit_reliability_params(); - dc1.commit_reliability_params(); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = dc0 - .write_data_channel(&Bytes::from(sbuf.clone()), true) - .await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - sbuf[0..4].copy_from_slice(&2u32.to_be_bytes()); - let n = dc0 - .write_data_channel(&Bytes::from(sbuf.clone()), true) - .await?; - assert_eq!(sbuf.len(), n, "data length should match"); - - tokio::time::sleep(Duration::from_millis(100)).await; - br.reorder(0).await; // reordering on the wire - bridge_process_at_least_one(&br).await; - - let (n, is_string) = dc1.read_data_channel(&mut rbuf[..]).await?; - assert!(is_string, "should return isString being true"); - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 2, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - let (n, is_string) = dc1.read_data_channel(&mut rbuf[..]).await?; - assert!(is_string, "should return isString being true"); - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 1, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - dc0.close().await?; - dc1.close().await?; - bridge_process_at_least_one(&br).await; - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_data_channel_channel_type_partial_reliable_rexmit() -> Result<()> { - pr_ordered_unordered_test(ChannelType::PartialReliableRexmit, true).await -} - -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_data_channel_channel_type_partial_reliable_rexmit_unordered() -> Result<()> { - pr_ordered_unordered_test(ChannelType::PartialReliableRexmitUnordered, false).await -} - -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_data_channel_channel_type_partial_reliable_timed() -> Result<()> { - pr_ordered_unordered_test(ChannelType::PartialReliableTimed, true).await -} - -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_data_channel_channel_type_partial_reliable_timed_unordered() -> Result<()> { - pr_ordered_unordered_test(ChannelType::PartialReliableTimedUnordered, false).await -} - -//TODO: remove this conditional test -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -#[tokio::test] -async fn test_data_channel_buffered_amount() -> Result<()> { - let sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1000]; - - let n_cbs = Arc::new(AtomicUsize::new(0)); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let dc0 = Arc::new( - DataChannel::dial( - &a0, - 100, - Config { - label: "data".to_owned(), - ..Default::default() - }, - ) - .await?, - ); - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = Arc::new(DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?); - bridge_process_at_least_one(&br).await; - - while dc0.buffered_amount() > 0 { - bridge_process_at_least_one(&br).await; - } - - let n = dc0.write(&Bytes::new()).await?; - assert_eq!(n, 0, "data length should match"); - assert_eq!(dc0.buffered_amount(), 1, "incorrect bufferedAmount"); - - let n = dc0.write(&Bytes::from_static(&[0])).await?; - assert_eq!(n, 1, "data length should match"); - assert_eq!(dc0.buffered_amount(), 2, "incorrect bufferedAmount"); - - bridge_process_at_least_one(&br).await; - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, 0, "received length should match"); - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, 1, "received length should match"); - - dc0.set_buffered_amount_low_threshold(1500); - assert_eq!( - dc0.buffered_amount_low_threshold(), - 1500, - "incorrect bufferedAmountLowThreshold" - ); - let n_cbs2 = Arc::clone(&n_cbs); - dc0.on_buffered_amount_low(Box::new(move || { - n_cbs2.fetch_add(1, Ordering::SeqCst); - Box::pin(async {}) - })); - - // Write 10 1000-byte packets (total 10,000 bytes) - for i in 0..10 { - let n = dc0.write(&Bytes::from(sbuf.clone())).await?; - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - sbuf.len() * (i + 1) + 2, - dc0.buffered_amount(), - "incorrect bufferedAmount" - ); - } - - let dc1_cloned = Arc::clone(&dc1); - tokio::spawn(async move { - while let Ok(n) = dc1_cloned.read(&mut rbuf[..]).await { - if n == 0 { - break; - } - assert_eq!(n, rbuf.len(), "received length should match"); - } - }); - - let since = tokio::time::Instant::now(); - loop { - br.tick().await; - tokio::time::sleep(Duration::from_millis(10)).await; - if tokio::time::Instant::now().duration_since(since) > Duration::from_millis(500) { - break; - } - } - - dc0.close().await?; - dc1.close().await?; - bridge_process_at_least_one(&br).await; - - assert!( - n_cbs.load(Ordering::SeqCst) > 0, - "should make at least one callback" - ); - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//TODO: remove this conditional test -#[cfg(not(any(target_os = "macos", target_os = "windows")))] // this times out in CI on windows. -#[tokio::test] -async fn test_stats() -> Result<()> { - let sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1500]; - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let cfg = Config { - channel_type: ChannelType::Reliable, - reliability_parameter: 123, - label: "data".to_owned(), - ..Default::default() - }; - - let dc0 = DataChannel::dial(&a0, 100, cfg.clone()).await?; - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?; - bridge_process_at_least_one(&br).await; - - let mut bytes_sent = 0; - - let n = dc0.write(&Bytes::from(sbuf.clone())).await?; - assert_eq!(n, sbuf.len(), "data length should match"); - bytes_sent += n; - - assert_eq!(dc0.bytes_sent(), bytes_sent); - assert_eq!(dc0.messages_sent(), 1); - - let n = dc0.write(&Bytes::from(sbuf.clone())).await?; - assert_eq!(n, sbuf.len(), "data length should match"); - bytes_sent += n; - - assert_eq!(dc0.bytes_sent(), bytes_sent); - assert_eq!(dc0.messages_sent(), 2); - - let n = dc0.write(&Bytes::from_static(&[0])).await?; - assert_eq!(n, 1, "data length should match"); - bytes_sent += n; - - assert_eq!(dc0.bytes_sent(), bytes_sent); - assert_eq!(dc0.messages_sent(), 3); - - let n = dc0.write(&Bytes::from_static(&[])).await?; - assert_eq!(n, 0, "data length should match"); - bytes_sent += n; - - assert_eq!(dc0.bytes_sent(), bytes_sent); - assert_eq!(dc0.messages_sent(), 4); - - bridge_process_at_least_one(&br).await; - - let mut bytes_read = 0; - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, sbuf.len(), "data length should match"); - bytes_read += n; - - assert_eq!(dc1.bytes_received(), bytes_read); - assert_eq!(dc1.messages_received(), 1); - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, sbuf.len(), "data length should match"); - bytes_read += n; - - assert_eq!(dc1.bytes_received(), bytes_read); - assert_eq!(dc1.messages_received(), 2); - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, 1, "data length should match"); - bytes_read += n; - - assert_eq!(dc1.bytes_received(), bytes_read); - assert_eq!(dc1.messages_received(), 3); - - let n = dc1.read(&mut rbuf[..]).await?; - assert_eq!(n, 0, "data length should match"); - bytes_read += n; - - assert_eq!(dc1.bytes_received(), bytes_read); - assert_eq!(dc1.messages_received(), 4); - - dc0.close().await?; - dc1.close().await?; - bridge_process_at_least_one(&br).await; - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -#[tokio::test] -async fn test_poll_data_channel() -> Result<()> { - let mut sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1500]; - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, a1) = create_new_association_pair(&br, Arc::new(ca), Arc::new(cb)).await?; - - let cfg = Config { - channel_type: ChannelType::Reliable, - reliability_parameter: 123, - label: "data".to_string(), - ..Default::default() - }; - - let dc0 = Arc::new(DataChannel::dial(&a0, 100, cfg.clone()).await?); - bridge_process_at_least_one(&br).await; - - let existing_data_channels: Vec = Vec::new(); - let dc1 = Arc::new(DataChannel::accept(&a1, Config::default(), &existing_data_channels).await?); - bridge_process_at_least_one(&br).await; - - let mut poll_dc0 = PollDataChannel::new(dc0); - let mut poll_dc1 = PollDataChannel::new(dc1); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = poll_dc0 - .write(&Bytes::from(sbuf.clone())) - .await - .map_err(|e| Error::new(e.to_string()))?; - assert_eq!(sbuf.len(), n, "data length should match"); - - bridge_process_at_least_one(&br).await; - - let n = poll_dc1 - .read(&mut rbuf[..]) - .await - .map_err(|e| Error::new(e.to_string()))?; - assert_eq!(sbuf.len(), n, "data length should match"); - assert_eq!( - 1, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "data should match" - ); - - poll_dc0.into_inner().close().await?; - poll_dc1.into_inner().close().await?; - bridge_process_at_least_one(&br).await; - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} diff --git a/data/src/data_channel/mod.rs b/data/src/data_channel/mod.rs deleted file mode 100644 index 17f12b9e8..000000000 --- a/data/src/data_channel/mod.rs +++ /dev/null @@ -1,682 +0,0 @@ -#[cfg(test)] -mod data_channel_test; - -use std::borrow::Borrow; -use std::future::Future; -use std::net::Shutdown; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::{fmt, io}; - -use bytes::{Buf, Bytes}; -use portable_atomic::AtomicUsize; -use sctp::association::Association; -use sctp::chunk::chunk_payload_data::PayloadProtocolIdentifier; -use sctp::stream::*; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use util::marshal::*; - -use crate::error::{Error, Result}; -use crate::message::message_channel_ack::*; -use crate::message::message_channel_open::*; -use crate::message::*; - -const RECEIVE_MTU: usize = 8192; - -/// Config is used to configure the data channel. -#[derive(Eq, PartialEq, Default, Clone, Debug)] -pub struct Config { - pub channel_type: ChannelType, - pub negotiated: bool, - pub priority: u16, - pub reliability_parameter: u32, - pub label: String, - pub protocol: String, -} - -/// DataChannel represents a data channel -#[derive(Debug, Default, Clone)] -pub struct DataChannel { - pub config: Config, - stream: Arc, - - // stats - messages_sent: Arc, - messages_received: Arc, - bytes_sent: Arc, - bytes_received: Arc, -} - -impl DataChannel { - pub fn new(stream: Arc, config: Config) -> Self { - Self { - config, - stream, - ..Default::default() - } - } - - /// Dial opens a data channels over SCTP - pub async fn dial( - association: &Arc, - identifier: u16, - config: Config, - ) -> Result { - let stream = association - .open_stream(identifier, PayloadProtocolIdentifier::Binary) - .await?; - - Self::client(stream, config).await - } - - /// Accept is used to accept incoming data channels over SCTP - pub async fn accept( - association: &Arc, - config: Config, - existing_channels: &[T], - ) -> Result - where - T: Borrow, - { - let stream = association - .accept_stream() - .await - .ok_or(Error::ErrStreamClosed)?; - - for channel in existing_channels.iter().map(|ch| ch.borrow()) { - if channel.stream_identifier() == stream.stream_identifier() { - let ch = channel.to_owned(); - ch.stream - .set_default_payload_type(PayloadProtocolIdentifier::Binary); - return Ok(ch); - } - } - - stream.set_default_payload_type(PayloadProtocolIdentifier::Binary); - - Self::server(stream, config).await - } - - /// Client opens a data channel over an SCTP stream - pub async fn client(stream: Arc, config: Config) -> Result { - if !config.negotiated { - let msg = Message::DataChannelOpen(DataChannelOpen { - channel_type: config.channel_type, - priority: config.priority, - reliability_parameter: config.reliability_parameter, - label: config.label.bytes().collect(), - protocol: config.protocol.bytes().collect(), - }) - .marshal()?; - - stream - .write_sctp(&msg, PayloadProtocolIdentifier::Dcep) - .await?; - } - Ok(DataChannel::new(stream, config)) - } - - /// Server accepts a data channel over an SCTP stream - pub async fn server(stream: Arc, mut config: Config) -> Result { - let mut buf = vec![0u8; RECEIVE_MTU]; - - let (n, ppi) = stream.read_sctp(&mut buf).await?; - - if ppi != PayloadProtocolIdentifier::Dcep { - return Err(Error::InvalidPayloadProtocolIdentifier(ppi as u8)); - } - - let mut read_buf = &buf[..n]; - let msg = Message::unmarshal(&mut read_buf)?; - - if let Message::DataChannelOpen(dco) = msg { - config.channel_type = dco.channel_type; - config.priority = dco.priority; - config.reliability_parameter = dco.reliability_parameter; - config.label = String::from_utf8(dco.label)?; - config.protocol = String::from_utf8(dco.protocol)?; - } else { - return Err(Error::InvalidMessageType(msg.message_type() as u8)); - }; - - let data_channel = DataChannel::new(stream, config); - - data_channel.write_data_channel_ack().await?; - data_channel.commit_reliability_params(); - - Ok(data_channel) - } - - /// Read reads a packet of len(p) bytes as binary data. - /// - /// See [`sctp::stream::Stream::read_sctp`]. - pub async fn read(&self, buf: &mut [u8]) -> Result { - self.read_data_channel(buf).await.map(|(n, _)| n) - } - - /// ReadDataChannel reads a packet of len(p) bytes. It returns the number of bytes read and - /// `true` if the data read is a string. - /// - /// See [`sctp::stream::Stream::read_sctp`]. - pub async fn read_data_channel(&self, buf: &mut [u8]) -> Result<(usize, bool)> { - loop { - //TODO: add handling of cancel read_data_channel - let (mut n, ppi) = match self.stream.read_sctp(buf).await { - Ok((0, PayloadProtocolIdentifier::Unknown)) => { - // The incoming stream was reset or the reading half was shutdown - return Ok((0, false)); - } - Ok((n, ppi)) => (n, ppi), - Err(err) => { - // Shutdown the stream and send the reset request to the remote. - self.close().await?; - return Err(err.into()); - } - }; - - let mut is_string = false; - match ppi { - PayloadProtocolIdentifier::Dcep => { - let mut data = &buf[..n]; - match self.handle_dcep(&mut data).await { - Ok(()) => {} - Err(err) => { - log::error!("Failed to handle DCEP: {:?}", err); - } - } - continue; - } - PayloadProtocolIdentifier::String | PayloadProtocolIdentifier::StringEmpty => { - is_string = true; - } - _ => {} - }; - - match ppi { - PayloadProtocolIdentifier::StringEmpty | PayloadProtocolIdentifier::BinaryEmpty => { - n = 0; - } - _ => {} - }; - - self.messages_received.fetch_add(1, Ordering::SeqCst); - self.bytes_received.fetch_add(n, Ordering::SeqCst); - - return Ok((n, is_string)); - } - } - - /// MessagesSent returns the number of messages sent - pub fn messages_sent(&self) -> usize { - self.messages_sent.load(Ordering::SeqCst) - } - - /// MessagesReceived returns the number of messages received - pub fn messages_received(&self) -> usize { - self.messages_received.load(Ordering::SeqCst) - } - - /// BytesSent returns the number of bytes sent - pub fn bytes_sent(&self) -> usize { - self.bytes_sent.load(Ordering::SeqCst) - } - - /// BytesReceived returns the number of bytes received - pub fn bytes_received(&self) -> usize { - self.bytes_received.load(Ordering::SeqCst) - } - - /// StreamIdentifier returns the Stream identifier associated to the stream. - pub fn stream_identifier(&self) -> u16 { - self.stream.stream_identifier() - } - - async fn handle_dcep(&self, data: &mut B) -> Result<()> - where - B: Buf, - { - let msg = Message::unmarshal(data)?; - - match msg { - Message::DataChannelOpen(_) => { - // Note: DATA_CHANNEL_OPEN message is handled inside Server() method. - // Therefore, the message will not reach here. - log::debug!("Received DATA_CHANNEL_OPEN"); - let _ = self.write_data_channel_ack().await?; - } - Message::DataChannelAck(_) => { - log::debug!("Received DATA_CHANNEL_ACK"); - self.commit_reliability_params(); - } - }; - - Ok(()) - } - - /// Write writes len(p) bytes from p as binary data - pub async fn write(&self, data: &Bytes) -> Result { - self.write_data_channel(data, false).await - } - - /// WriteDataChannel writes len(p) bytes from p - pub async fn write_data_channel(&self, data: &Bytes, is_string: bool) -> Result { - let data_len = data.len(); - - // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-12#section-6.6 - // SCTP does not support the sending of empty user messages. Therefore, - // if an empty message has to be sent, the appropriate PPID (WebRTC - // String Empty or WebRTC Binary Empty) is used and the SCTP user - // message of one zero byte is sent. When receiving an SCTP user - // message with one of these PPIDs, the receiver MUST ignore the SCTP - // user message and process it as an empty message. - let ppi = match (is_string, data_len) { - (false, 0) => PayloadProtocolIdentifier::BinaryEmpty, - (false, _) => PayloadProtocolIdentifier::Binary, - (true, 0) => PayloadProtocolIdentifier::StringEmpty, - (true, _) => PayloadProtocolIdentifier::String, - }; - - let n = if data_len == 0 { - let _ = self - .stream - .write_sctp(&Bytes::from_static(&[0]), ppi) - .await?; - 0 - } else { - let n = self.stream.write_sctp(data, ppi).await?; - self.bytes_sent.fetch_add(n, Ordering::SeqCst); - n - }; - - self.messages_sent.fetch_add(1, Ordering::SeqCst); - Ok(n) - } - - async fn write_data_channel_ack(&self) -> Result { - let ack = Message::DataChannelAck(DataChannelAck {}).marshal()?; - Ok(self - .stream - .write_sctp(&ack, PayloadProtocolIdentifier::Dcep) - .await?) - } - - /// Close closes the DataChannel and the underlying SCTP stream. - pub async fn close(&self) -> Result<()> { - // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.7 - // Closing of a data channel MUST be signaled by resetting the - // corresponding outgoing streams [RFC6525]. This means that if one - // side decides to close the data channel, it resets the corresponding - // outgoing stream. When the peer sees that an incoming stream was - // reset, it also resets its corresponding outgoing stream. Once this - // is completed, the data channel is closed. Resetting a stream sets - // the Stream Sequence Numbers (SSNs) of the stream back to 'zero' with - // a corresponding notification to the application layer that the reset - // has been performed. Streams are available for reuse after a reset - // has been performed. - Ok(self.stream.shutdown(Shutdown::Both).await?) - } - - /// BufferedAmount returns the number of bytes of data currently queued to be - /// sent over this stream. - pub fn buffered_amount(&self) -> usize { - self.stream.buffered_amount() - } - - /// BufferedAmountLowThreshold returns the number of bytes of buffered outgoing - /// data that is considered "low." Defaults to 0. - pub fn buffered_amount_low_threshold(&self) -> usize { - self.stream.buffered_amount_low_threshold() - } - - /// SetBufferedAmountLowThreshold is used to update the threshold. - /// See BufferedAmountLowThreshold(). - pub fn set_buffered_amount_low_threshold(&self, threshold: usize) { - self.stream.set_buffered_amount_low_threshold(threshold) - } - - /// OnBufferedAmountLow sets the callback handler which would be called when the - /// number of bytes of outgoing data buffered is lower than the threshold. - pub fn on_buffered_amount_low(&self, f: OnBufferedAmountLowFn) { - self.stream.on_buffered_amount_low(f) - } - - fn commit_reliability_params(&self) { - let (unordered, reliability_type) = match self.config.channel_type { - ChannelType::Reliable => (false, ReliabilityType::Reliable), - ChannelType::ReliableUnordered => (true, ReliabilityType::Reliable), - ChannelType::PartialReliableRexmit => (false, ReliabilityType::Rexmit), - ChannelType::PartialReliableRexmitUnordered => (true, ReliabilityType::Rexmit), - ChannelType::PartialReliableTimed => (false, ReliabilityType::Timed), - ChannelType::PartialReliableTimedUnordered => (true, ReliabilityType::Timed), - }; - - self.stream.set_reliability_params( - unordered, - reliability_type, - self.config.reliability_parameter, - ); - } -} - -/// Default capacity of the temporary read buffer used by [`PollStream`]. -const DEFAULT_READ_BUF_SIZE: usize = 8192; - -/// State of the read `Future` in [`PollStream`]. -enum ReadFut { - /// Nothing in progress. - Idle, - /// Reading data from the underlying stream. - Reading(Pin>> + Send>>), - /// Finished reading, but there's unread data in the temporary buffer. - RemainingData(Vec), -} - -impl ReadFut { - /// Gets a mutable reference to the future stored inside `Reading(future)`. - /// - /// # Panics - /// - /// Panics if `ReadFut` variant is not `Reading`. - fn get_reading_mut(&mut self) -> &mut Pin>> + Send>> { - match self { - ReadFut::Reading(ref mut fut) => fut, - _ => panic!("expected ReadFut to be Reading"), - } - } -} - -/// A wrapper around around [`DataChannel`], which implements [`AsyncRead`] and -/// [`AsyncWrite`]. -/// -/// Both `poll_read` and `poll_write` calls allocate temporary buffers, which results in an -/// additional overhead. -pub struct PollDataChannel { - data_channel: Arc, - - read_fut: ReadFut, - write_fut: Option> + Send>>>, - shutdown_fut: Option> + Send>>>, - - read_buf_cap: usize, -} - -impl PollDataChannel { - /// Constructs a new `PollDataChannel`. - /// - /// # Examples - /// - /// ``` - /// use webrtc_data::data_channel::{DataChannel, PollDataChannel, Config}; - /// use sctp::stream::Stream; - /// use std::sync::Arc; - /// - /// let dc = Arc::new(DataChannel::new(Arc::new(Stream::default()), Config::default())); - /// let poll_dc = PollDataChannel::new(dc); - /// ``` - pub fn new(data_channel: Arc) -> Self { - Self { - data_channel, - read_fut: ReadFut::Idle, - write_fut: None, - shutdown_fut: None, - read_buf_cap: DEFAULT_READ_BUF_SIZE, - } - } - - /// Get back the inner data_channel. - pub fn into_inner(self) -> Arc { - self.data_channel - } - - /// Obtain a clone of the inner data_channel. - pub fn clone_inner(&self) -> Arc { - self.data_channel.clone() - } - - /// MessagesSent returns the number of messages sent - pub fn messages_sent(&self) -> usize { - self.data_channel.messages_sent() - } - - /// MessagesReceived returns the number of messages received - pub fn messages_received(&self) -> usize { - self.data_channel.messages_received() - } - - /// BytesSent returns the number of bytes sent - pub fn bytes_sent(&self) -> usize { - self.data_channel.bytes_sent() - } - - /// BytesReceived returns the number of bytes received - pub fn bytes_received(&self) -> usize { - self.data_channel.bytes_received() - } - - /// StreamIdentifier returns the Stream identifier associated to the stream. - pub fn stream_identifier(&self) -> u16 { - self.data_channel.stream_identifier() - } - - /// BufferedAmount returns the number of bytes of data currently queued to be - /// sent over this stream. - pub fn buffered_amount(&self) -> usize { - self.data_channel.buffered_amount() - } - - /// BufferedAmountLowThreshold returns the number of bytes of buffered outgoing - /// data that is considered "low." Defaults to 0. - pub fn buffered_amount_low_threshold(&self) -> usize { - self.data_channel.buffered_amount_low_threshold() - } - - /// Set the capacity of the temporary read buffer (default: 8192). - pub fn set_read_buf_capacity(&mut self, capacity: usize) { - self.read_buf_cap = capacity - } -} - -impl AsyncRead for PollDataChannel { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if buf.remaining() == 0 { - return Poll::Ready(Ok(())); - } - - let fut = match self.read_fut { - ReadFut::Idle => { - // read into a temporary buffer because `buf` has an unonymous lifetime, which can - // be shorter than the lifetime of `read_fut`. - let data_channel = self.data_channel.clone(); - let mut temp_buf = vec![0; self.read_buf_cap]; - self.read_fut = ReadFut::Reading(Box::pin(async move { - data_channel.read(temp_buf.as_mut_slice()).await.map(|n| { - temp_buf.truncate(n); - temp_buf - }) - })); - self.read_fut.get_reading_mut() - } - ReadFut::Reading(ref mut fut) => fut, - ReadFut::RemainingData(ref mut data) => { - let remaining = buf.remaining(); - let len = std::cmp::min(data.len(), remaining); - buf.put_slice(&data[..len]); - if data.len() > remaining { - // ReadFut remains to be RemainingData - data.drain(..len); - } else { - self.read_fut = ReadFut::Idle; - } - return Poll::Ready(Ok(())); - } - }; - - loop { - match fut.as_mut().poll(cx) { - Poll::Pending => return Poll::Pending, - // retry immediately upon empty data or incomplete chunks - // since there's no way to setup a waker. - Poll::Ready(Err(Error::Sctp(sctp::Error::ErrTryAgain))) => {} - // EOF has been reached => don't touch buf and just return Ok - Poll::Ready(Err(Error::Sctp(sctp::Error::ErrEof))) => { - self.read_fut = ReadFut::Idle; - return Poll::Ready(Ok(())); - } - Poll::Ready(Err(e)) => { - self.read_fut = ReadFut::Idle; - return Poll::Ready(Err(e.into())); - } - Poll::Ready(Ok(mut temp_buf)) => { - let remaining = buf.remaining(); - let len = std::cmp::min(temp_buf.len(), remaining); - buf.put_slice(&temp_buf[..len]); - if temp_buf.len() > remaining { - temp_buf.drain(..len); - self.read_fut = ReadFut::RemainingData(temp_buf); - } else { - self.read_fut = ReadFut::Idle; - } - return Poll::Ready(Ok(())); - } - } - } - } -} - -impl AsyncWrite for PollDataChannel { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - if buf.is_empty() { - return Poll::Ready(Ok(0)); - } - - if let Some(fut) = self.write_fut.as_mut() { - match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - let data_channel = self.data_channel.clone(); - let bytes = Bytes::copy_from_slice(buf); - self.write_fut = - Some(Box::pin(async move { data_channel.write(&bytes).await })); - Poll::Ready(Err(e.into())) - } - // Given the data is buffered, it's okay to ignore the number of written bytes. - // - // TODO: In the long term, `data_channel.write` should be made sync. Then we could - // remove the whole `if` condition and just call `data_channel.write`. - Poll::Ready(Ok(_)) => { - let data_channel = self.data_channel.clone(); - let bytes = Bytes::copy_from_slice(buf); - self.write_fut = - Some(Box::pin(async move { data_channel.write(&bytes).await })); - Poll::Ready(Ok(buf.len())) - } - } - } else { - let data_channel = self.data_channel.clone(); - let bytes = Bytes::copy_from_slice(buf); - let fut = self - .write_fut - .insert(Box::pin(async move { data_channel.write(&bytes).await })); - - match fut.as_mut().poll(cx) { - // If it's the first time we're polling the future, `Poll::Pending` can't be - // returned because that would mean the `PollDataChannel` is not ready for writing. - // And this is not true since we've just created a future, which is going to write - // the buf to the underlying stream. - // - // It's okay to return `Poll::Ready` if the data is buffered (this is what the - // buffered writer and `File` do). - Poll::Pending => Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(e)) => { - self.write_fut = None; - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(n)) => { - self.write_fut = None; - Poll::Ready(Ok(n)) - } - } - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.write_fut.as_mut() { - Some(fut) => match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - self.write_fut = None; - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(_)) => { - self.write_fut = None; - Poll::Ready(Ok(())) - } - }, - None => Poll::Ready(Ok(())), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(_) => {} - } - - let fut = match self.shutdown_fut.as_mut() { - Some(fut) => fut, - None => { - let data_channel = self.data_channel.clone(); - self.shutdown_fut.get_or_insert(Box::pin(async move { - data_channel - .stream - .shutdown(Shutdown::Write) - .await - .map_err(Error::Sctp) - })) - } - }; - - match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - self.shutdown_fut = None; - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(_)) => { - self.shutdown_fut = None; - Poll::Ready(Ok(())) - } - } - } -} - -impl Clone for PollDataChannel { - fn clone(&self) -> PollDataChannel { - PollDataChannel::new(self.clone_inner()) - } -} - -impl fmt::Debug for PollDataChannel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PollDataChannel") - .field("data_channel", &self.data_channel) - .field("read_buf_cap", &self.read_buf_cap) - .finish() - } -} - -impl AsRef for PollDataChannel { - fn as_ref(&self) -> &DataChannel { - &self.data_channel - } -} diff --git a/data/src/error.rs b/data/src/error.rs deleted file mode 100644 index 4d6b1b84c..000000000 --- a/data/src/error.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::io; -use std::string::FromUtf8Error; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error( - "DataChannel message is not long enough to determine type: (expected: {expected}, actual: {actual})" - )] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, - #[error("Unknown MessageType {0}")] - InvalidMessageType(u8), - #[error("Unknown ChannelType {0}")] - InvalidChannelType(u8), - #[error("Unknown PayloadProtocolIdentifier {0}")] - InvalidPayloadProtocolIdentifier(u8), - #[error("Stream closed")] - ErrStreamClosed, - - #[error("{0}")] - Util(#[from] util::Error), - #[error("{0}")] - Sctp(#[from] sctp::Error), - #[error("utf-8 error: {0}")] - Utf8(#[from] FromUtf8Error), - - #[allow(non_camel_case_types)] - #[error("{0}")] - new(String), -} - -impl From for util::Error { - fn from(e: Error) -> Self { - util::Error::from_std(e) - } -} - -impl From for io::Error { - fn from(error: Error) -> Self { - match error { - e @ Error::Sctp(sctp::Error::ErrEof) => { - io::Error::new(io::ErrorKind::UnexpectedEof, e.to_string()) - } - e @ Error::ErrStreamClosed => { - io::Error::new(io::ErrorKind::ConnectionAborted, e.to_string()) - } - e => io::Error::new(io::ErrorKind::Other, e.to_string()), - } - } -} - -impl PartialEq for Error { - fn eq(&self, other: &util::Error) -> bool { - if let Some(down) = other.downcast_ref::() { - return self == down; - } - false - } -} - -impl PartialEq for util::Error { - fn eq(&self, other: &Error) -> bool { - if let Some(down) = self.downcast_ref::() { - return other == down; - } - false - } -} diff --git a/data/src/lib.rs b/data/src/lib.rs deleted file mode 100644 index 7dff238a4..000000000 --- a/data/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod data_channel; -mod error; -pub mod message; - -pub use error::Error; diff --git a/data/src/message/message_channel_ack.rs b/data/src/message/message_channel_ack.rs deleted file mode 100644 index dbe4796ca..000000000 --- a/data/src/message/message_channel_ack.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::*; - -type Result = std::result::Result; - -/// The data-part of an data-channel ACK message without the message type. -/// -/// # Memory layout -/// -/// ```plain -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Message Type | -///+-+-+-+-+-+-+-+-+ -/// ``` -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct DataChannelAck; - -impl MarshalSize for DataChannelAck { - fn marshal_size(&self) -> usize { - 0 - } -} - -impl Marshal for DataChannelAck { - fn marshal_to(&self, _buf: &mut [u8]) -> Result { - Ok(0) - } -} - -impl Unmarshal for DataChannelAck { - fn unmarshal(_buf: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - Ok(Self) - } -} - -#[cfg(test)] -mod tests { - use bytes::{Bytes, BytesMut}; - - use super::*; - - #[test] - fn test_channel_ack_unmarshal() -> Result<()> { - let mut bytes = Bytes::from_static(&[]); - - let channel_ack = DataChannelAck::unmarshal(&mut bytes)?; - - assert_eq!(channel_ack, DataChannelAck); - Ok(()) - } - - #[test] - fn test_channel_ack_marshal_size() -> Result<()> { - let channel_ack = DataChannelAck; - let marshal_size = channel_ack.marshal_size(); - - assert_eq!(marshal_size, 0); - Ok(()) - } - - #[test] - fn test_channel_ack_marshal() -> Result<()> { - let channel_ack = DataChannelAck; - let mut buf = BytesMut::with_capacity(0); - let bytes_written = channel_ack.marshal_to(&mut buf)?; - let bytes = buf.freeze(); - - assert_eq!(bytes_written, channel_ack.marshal_size()); - assert_eq!(&bytes[..], &[]); - Ok(()) - } -} diff --git a/data/src/message/message_channel_open.rs b/data/src/message/message_channel_open.rs deleted file mode 100644 index a93a017d3..000000000 --- a/data/src/message/message_channel_open.rs +++ /dev/null @@ -1,441 +0,0 @@ -use super::*; -use crate::error::Error; - -type Result = std::result::Result; - -const CHANNEL_TYPE_RELIABLE: u8 = 0x00; -const CHANNEL_TYPE_RELIABLE_UNORDERED: u8 = 0x80; -const CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT: u8 = 0x01; -const CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT_UNORDERED: u8 = 0x81; -const CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED: u8 = 0x02; -const CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED_UNORDERED: u8 = 0x82; -const CHANNEL_TYPE_LEN: usize = 1; - -/// ChannelPriority -pub const CHANNEL_PRIORITY_BELOW_NORMAL: u16 = 128; -pub const CHANNEL_PRIORITY_NORMAL: u16 = 256; -pub const CHANNEL_PRIORITY_HIGH: u16 = 512; -pub const CHANNEL_PRIORITY_EXTRA_HIGH: u16 = 1024; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum ChannelType { - // `Reliable` determines the Data Channel provides a - // reliable in-order bi-directional communication. - Reliable, - // `ReliableUnordered` determines the Data Channel - // provides a reliable unordered bi-directional communication. - ReliableUnordered, - // `PartialReliableRexmit` determines the Data Channel - // provides a partially-reliable in-order bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. - PartialReliableRexmit, - // `PartialReliableRexmitUnordered` determines - // the Data Channel provides a partial reliable unordered bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. - PartialReliableRexmitUnordered, - // `PartialReliableTimed` determines the Data Channel - // provides a partial reliable in-order bi-directional communication. - // User messages might not be transmitted or retransmitted after - // a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. - PartialReliableTimed, - // The Data Channel provides a partial reliable unordered bi-directional - // communication. User messages might not be transmitted or retransmitted - // after a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. - PartialReliableTimedUnordered, -} - -impl Default for ChannelType { - fn default() -> Self { - Self::Reliable - } -} - -impl MarshalSize for ChannelType { - fn marshal_size(&self) -> usize { - CHANNEL_TYPE_LEN - } -} - -impl Marshal for ChannelType { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let required_len = self.marshal_size(); - if buf.remaining_mut() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining_mut(), - } - .into()); - } - - let byte = match self { - Self::Reliable => CHANNEL_TYPE_RELIABLE, - Self::ReliableUnordered => CHANNEL_TYPE_RELIABLE_UNORDERED, - Self::PartialReliableRexmit => CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT, - Self::PartialReliableRexmitUnordered => CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT_UNORDERED, - Self::PartialReliableTimed => CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED, - Self::PartialReliableTimedUnordered => CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED_UNORDERED, - }; - - buf.put_u8(byte); - - Ok(1) - } -} - -impl Unmarshal for ChannelType { - fn unmarshal(buf: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let required_len = CHANNEL_TYPE_LEN; - if buf.remaining() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining(), - } - .into()); - } - - let b0 = buf.get_u8(); - - match b0 { - CHANNEL_TYPE_RELIABLE => Ok(Self::Reliable), - CHANNEL_TYPE_RELIABLE_UNORDERED => Ok(Self::ReliableUnordered), - CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT => Ok(Self::PartialReliableRexmit), - CHANNEL_TYPE_PARTIAL_RELIABLE_REXMIT_UNORDERED => { - Ok(Self::PartialReliableRexmitUnordered) - } - CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED => Ok(Self::PartialReliableTimed), - CHANNEL_TYPE_PARTIAL_RELIABLE_TIMED_UNORDERED => { - Ok(Self::PartialReliableTimedUnordered) - } - _ => Err(Error::InvalidChannelType(b0).into()), - } - } -} - -const CHANNEL_OPEN_HEADER_LEN: usize = 11; - -/// The data-part of an data-channel OPEN message without the message type. -/// -/// # Memory layout -/// -/// ```plain -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | (Message Type)| Channel Type | Priority | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Reliability Parameter | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Label Length | Protocol Length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// | Label | -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// | Protocol | -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// ``` -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct DataChannelOpen { - pub channel_type: ChannelType, - pub priority: u16, - pub reliability_parameter: u32, - pub label: Vec, - pub protocol: Vec, -} - -impl MarshalSize for DataChannelOpen { - fn marshal_size(&self) -> usize { - let label_len = self.label.len(); - let protocol_len = self.protocol.len(); - - CHANNEL_OPEN_HEADER_LEN + label_len + protocol_len - } -} - -impl Marshal for DataChannelOpen { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let required_len = self.marshal_size(); - if buf.remaining_mut() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining_mut(), - } - .into()); - } - - let n = self.channel_type.marshal_to(buf)?; - buf = &mut buf[n..]; - buf.put_u16(self.priority); - buf.put_u32(self.reliability_parameter); - buf.put_u16(self.label.len() as u16); - buf.put_u16(self.protocol.len() as u16); - buf.put_slice(self.label.as_slice()); - buf.put_slice(self.protocol.as_slice()); - Ok(self.marshal_size()) - } -} - -impl Unmarshal for DataChannelOpen { - fn unmarshal(buf: &mut B) -> Result - where - B: Buf, - { - let required_len = CHANNEL_OPEN_HEADER_LEN; - if buf.remaining() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining(), - } - .into()); - } - - let channel_type = ChannelType::unmarshal(buf)?; - let priority = buf.get_u16(); - let reliability_parameter = buf.get_u32(); - let label_len = buf.get_u16() as usize; - let protocol_len = buf.get_u16() as usize; - - let required_len = label_len + protocol_len; - if buf.remaining() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining(), - } - .into()); - } - - let mut label = vec![0; label_len]; - let mut protocol = vec![0; protocol_len]; - - buf.copy_to_slice(&mut label[..]); - buf.copy_to_slice(&mut protocol[..]); - - Ok(Self { - channel_type, - priority, - reliability_parameter, - label, - protocol, - }) - } -} - -#[cfg(test)] -mod tests { - use bytes::{Bytes, BytesMut}; - - use super::*; - - #[test] - fn test_channel_type_unmarshal_success() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x00]); - let channel_type = ChannelType::unmarshal(&mut bytes)?; - - assert_eq!(channel_type, ChannelType::Reliable); - Ok(()) - } - - #[test] - fn test_channel_type_unmarshal_invalid() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x11]); - match ChannelType::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::InvalidChannelType(0x11)) = err.downcast_ref::() { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_channel_type_unmarshal_unexpected_end_of_buffer() -> Result<()> { - let mut bytes = Bytes::from_static(&[]); - match ChannelType::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::UnexpectedEndOfBuffer { - expected: 1, - actual: 0, - }) = err.downcast_ref::() - { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_channel_type_marshal_size() -> Result<()> { - let channel_type = ChannelType::Reliable; - let marshal_size = channel_type.marshal_size(); - - assert_eq!(marshal_size, 1); - Ok(()) - } - - #[test] - fn test_channel_type_marshal() -> Result<()> { - let mut buf = BytesMut::with_capacity(1); - buf.resize(1, 0u8); - let channel_type = ChannelType::Reliable; - let bytes_written = channel_type.marshal_to(&mut buf)?; - assert_eq!(bytes_written, channel_type.marshal_size()); - - let bytes = buf.freeze(); - assert_eq!(&bytes[..], &[0x00]); - Ok(()) - } - - static MARSHALED_BYTES: [u8; 24] = [ - 0x00, // channel type - 0x0f, 0x35, // priority - 0x00, 0xff, 0x0f, 0x35, // reliability parameter - 0x00, 0x05, // label length - 0x00, 0x08, // protocol length - 0x6c, 0x61, 0x62, 0x65, 0x6c, // label - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, // protocol - ]; - - #[test] - fn test_channel_open_unmarshal_success() -> Result<()> { - let mut bytes = Bytes::from_static(&MARSHALED_BYTES); - - let channel_open = DataChannelOpen::unmarshal(&mut bytes)?; - - assert_eq!(channel_open.channel_type, ChannelType::Reliable); - assert_eq!(channel_open.priority, 3893); - assert_eq!(channel_open.reliability_parameter, 16715573); - assert_eq!(channel_open.label, b"label"); - assert_eq!(channel_open.protocol, b"protocol"); - Ok(()) - } - - #[test] - fn test_channel_open_unmarshal_invalid_channel_type() -> Result<()> { - let mut bytes = Bytes::from_static(&[ - 0x11, // channel type - 0x0f, 0x35, // priority - 0x00, 0xff, 0x0f, 0x35, // reliability parameter - 0x00, 0x05, // label length - 0x00, 0x08, // protocol length - ]); - match DataChannelOpen::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::InvalidChannelType(0x11)) = err.downcast_ref::() { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_channel_open_unmarshal_unexpected_end_of_buffer() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x00; 5]); - match DataChannelOpen::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::UnexpectedEndOfBuffer { - expected: 11, - actual: 5, - }) = err.downcast_ref::() - { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_channel_open_unmarshal_unexpected_length_mismatch() -> Result<()> { - let mut bytes = Bytes::from_static(&[ - 0x01, // channel type - 0x00, 0x00, // priority - 0x00, 0x00, 0x00, 0x00, // Reliability parameter - 0x00, 0x05, // Label length - 0x00, 0x08, // Protocol length - ]); - match DataChannelOpen::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::UnexpectedEndOfBuffer { - expected: 13, - actual: 0, - }) = err.downcast_ref::() - { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_channel_open_marshal_size() -> Result<()> { - let channel_open = DataChannelOpen { - channel_type: ChannelType::Reliable, - priority: 3893, - reliability_parameter: 16715573, - label: b"label".to_vec(), - protocol: b"protocol".to_vec(), - }; - - let marshal_size = channel_open.marshal_size(); - - assert_eq!(marshal_size, 11 + 5 + 8); - Ok(()) - } - - #[test] - fn test_channel_open_marshal() -> Result<()> { - let channel_open = DataChannelOpen { - channel_type: ChannelType::Reliable, - priority: 3893, - reliability_parameter: 16715573, - label: b"label".to_vec(), - protocol: b"protocol".to_vec(), - }; - - let mut buf = BytesMut::with_capacity(11 + 5 + 8); - buf.resize(11 + 5 + 8, 0u8); - let bytes_written = channel_open.marshal_to(&mut buf).unwrap(); - let bytes = buf.freeze(); - - assert_eq!(bytes_written, channel_open.marshal_size()); - assert_eq!(&bytes[..], &MARSHALED_BYTES); - Ok(()) - } -} diff --git a/data/src/message/message_test.rs b/data/src/message/message_test.rs deleted file mode 100644 index 932c501bf..000000000 --- a/data/src/message/message_test.rs +++ /dev/null @@ -1,96 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_message_unmarshal_open_success() { - let mut bytes = Bytes::from_static(&[ - 0x03, // message type - 0x00, // channel type - 0x0f, 0x35, // priority - 0x00, 0xff, 0x0f, 0x35, // reliability parameter - 0x00, 0x05, // label length - 0x00, 0x08, // protocol length - 0x6c, 0x61, 0x62, 0x65, 0x6c, // label - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, // protocol - ]); - - let actual = Message::unmarshal(&mut bytes).unwrap(); - - let expected = Message::DataChannelOpen(DataChannelOpen { - channel_type: ChannelType::Reliable, - priority: 3893, - reliability_parameter: 16715573, - label: b"label".to_vec(), - protocol: b"protocol".to_vec(), - }); - - assert_eq!(actual, expected); -} - -#[test] -fn test_message_unmarshal_ack_success() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x02]); - - let actual = Message::unmarshal(&mut bytes)?; - let expected = Message::DataChannelAck(DataChannelAck {}); - - assert_eq!(actual, expected); - - Ok(()) -} - -#[test] -fn test_message_unmarshal_invalid_message_type() { - let mut bytes = Bytes::from_static(&[0x01]); - let expected = Error::InvalidMessageType(0x01); - let result = Message::unmarshal(&mut bytes); - let actual = result.expect_err("expected err, but got ok"); - assert_eq!(actual, expected); -} - -#[test] -fn test_message_marshal_size() { - let msg = Message::DataChannelAck(DataChannelAck {}); - - let actual = msg.marshal_size(); - let expected = 1; - - assert_eq!(actual, expected); -} - -#[test] -fn test_message_marshal() { - let marshal_size = 12 + 5 + 8; - let mut buf = BytesMut::with_capacity(marshal_size); - buf.resize(marshal_size, 0u8); - - let msg = Message::DataChannelOpen(DataChannelOpen { - channel_type: ChannelType::Reliable, - priority: 3893, - reliability_parameter: 16715573, - label: b"label".to_vec(), - protocol: b"protocol".to_vec(), - }); - - let actual = msg.marshal_to(&mut buf).unwrap(); - let expected = marshal_size; - assert_eq!(actual, expected); - - let bytes = buf.freeze(); - - let actual = &bytes[..]; - let expected = &[ - 0x03, // message type - 0x00, // channel type - 0x0f, 0x35, // priority - 0x00, 0xff, 0x0f, 0x35, // reliability parameter - 0x00, 0x05, // label length - 0x00, 0x08, // protocol length - 0x6c, 0x61, 0x62, 0x65, 0x6c, // label - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, // protocol - ]; - - assert_eq!(actual, expected); -} diff --git a/data/src/message/message_type.rs b/data/src/message/message_type.rs deleted file mode 100644 index 0a62af6f6..000000000 --- a/data/src/message/message_type.rs +++ /dev/null @@ -1,125 +0,0 @@ -use super::*; -use crate::error::Error; - -// The first byte in a `Message` that specifies its type: -pub(crate) const MESSAGE_TYPE_ACK: u8 = 0x02; -pub(crate) const MESSAGE_TYPE_OPEN: u8 = 0x03; -pub(crate) const MESSAGE_TYPE_LEN: usize = 1; - -type Result = std::result::Result; - -// A parsed DataChannel message -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum MessageType { - DataChannelAck, - DataChannelOpen, -} - -impl MarshalSize for MessageType { - fn marshal_size(&self) -> usize { - MESSAGE_TYPE_LEN - } -} - -impl Marshal for MessageType { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let b = match self { - MessageType::DataChannelAck => MESSAGE_TYPE_ACK, - MessageType::DataChannelOpen => MESSAGE_TYPE_OPEN, - }; - - buf.put_u8(b); - - Ok(1) - } -} - -impl Unmarshal for MessageType { - fn unmarshal(buf: &mut B) -> Result - where - B: Buf, - { - let required_len = MESSAGE_TYPE_LEN; - if buf.remaining() < required_len { - return Err(Error::UnexpectedEndOfBuffer { - expected: required_len, - actual: buf.remaining(), - } - .into()); - } - - let b = buf.get_u8(); - - match b { - MESSAGE_TYPE_ACK => Ok(Self::DataChannelAck), - MESSAGE_TYPE_OPEN => Ok(Self::DataChannelOpen), - _ => Err(Error::InvalidMessageType(b).into()), - } - } -} - -#[cfg(test)] -mod tests { - use bytes::{Bytes, BytesMut}; - - use super::*; - - #[test] - fn test_message_type_unmarshal_open_success() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x03]); - let msg_type = MessageType::unmarshal(&mut bytes)?; - - assert_eq!(msg_type, MessageType::DataChannelOpen); - - Ok(()) - } - - #[test] - fn test_message_type_unmarshal_ack_success() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x02]); - let msg_type = MessageType::unmarshal(&mut bytes)?; - - assert_eq!(msg_type, MessageType::DataChannelAck); - Ok(()) - } - - #[test] - fn test_message_type_unmarshal_invalid() -> Result<()> { - let mut bytes = Bytes::from_static(&[0x01]); - match MessageType::unmarshal(&mut bytes) { - Ok(_) => panic!("expected Error, but got Ok"), - Err(err) => { - if let Some(&Error::InvalidMessageType(0x01)) = err.downcast_ref::() { - return Ok(()); - } - panic!( - "unexpected err {:?}, want {:?}", - err, - Error::InvalidMessageType(0x01) - ); - } - } - } - - #[test] - fn test_message_type_marshal_size() -> Result<()> { - let ack = MessageType::DataChannelAck; - let marshal_size = ack.marshal_size(); - - assert_eq!(marshal_size, MESSAGE_TYPE_LEN); - Ok(()) - } - - #[test] - fn test_message_type_marshal() -> Result<()> { - let mut buf = BytesMut::with_capacity(MESSAGE_TYPE_LEN); - buf.resize(MESSAGE_TYPE_LEN, 0u8); - let msg_type = MessageType::DataChannelAck; - let n = msg_type.marshal_to(&mut buf)?; - let bytes = buf.freeze(); - - assert_eq!(n, MESSAGE_TYPE_LEN); - assert_eq!(&bytes[..], &[0x02]); - Ok(()) - } -} diff --git a/data/src/message/mod.rs b/data/src/message/mod.rs deleted file mode 100644 index d9a450fe0..000000000 --- a/data/src/message/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -#[cfg(test)] -mod message_test; - -pub mod message_channel_ack; -pub mod message_channel_open; -pub mod message_type; - -use bytes::{Buf, BufMut}; -use message_channel_ack::*; -use message_channel_open::*; -use message_type::*; -use util::marshal::*; - -use crate::error::Error; - -/// A parsed DataChannel message -#[derive(Eq, PartialEq, Clone, Debug)] -pub enum Message { - DataChannelAck(DataChannelAck), - DataChannelOpen(DataChannelOpen), -} - -impl MarshalSize for Message { - fn marshal_size(&self) -> usize { - match self { - Message::DataChannelAck(m) => m.marshal_size() + MESSAGE_TYPE_LEN, - Message::DataChannelOpen(m) => m.marshal_size() + MESSAGE_TYPE_LEN, - } - } -} - -impl Marshal for Message { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let mut bytes_written = 0; - let n = self.message_type().marshal_to(buf)?; - buf = &mut buf[n..]; - bytes_written += n; - bytes_written += match self { - Message::DataChannelAck(_) => 0, - Message::DataChannelOpen(open) => open.marshal_to(buf)?, - }; - Ok(bytes_written) - } -} - -impl Unmarshal for Message { - fn unmarshal(buf: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if buf.remaining() < MESSAGE_TYPE_LEN { - return Err(Error::UnexpectedEndOfBuffer { - expected: MESSAGE_TYPE_LEN, - actual: buf.remaining(), - } - .into()); - } - - match MessageType::unmarshal(buf)? { - MessageType::DataChannelAck => Ok(Self::DataChannelAck(DataChannelAck {})), - MessageType::DataChannelOpen => { - Ok(Self::DataChannelOpen(DataChannelOpen::unmarshal(buf)?)) - } - } - } -} - -impl Message { - pub fn message_type(&self) -> MessageType { - match self { - Self::DataChannelAck(_) => MessageType::DataChannelAck, - Self::DataChannelOpen(_) => MessageType::DataChannelOpen, - } - } -} diff --git a/doc/AVStack.jpg b/doc/AVStack.jpg deleted file mode 100644 index 2c1d5d5c401104721e5e1be6c947e8bf9676cf58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4842 zcmbVPcT`i`(mydEReDiSqjW_;kY3DH6a<8bN>R~VMFd4aM0!ZXLQ_EGQUoCv0qI7X zAc~=*6j3RGAS9ue1QJRJq?!;3hxuec%HGD#27o{y zz@Oj)@F~FM(_y!70f3_;a2NmpVL%WH0R%w_0@i-?H`3yTU135o6! z6%`W)Nk~LOa+kQow!FQ__Vn#pU=tS>65by1e00N;96<~)n1S$>Tw*fFv zQ$f%Y+s618L3Tg|1b2ceiHd;(YNbH+q0k+mvVsC2Tu2o79T1QflsTw>VyCS0bs?2N zIfLkwJYm(7mCf?!f6~>C-MIfqL{vd>_Z}s64Na{>hYgL4O-#+qtxuh{u{~pFf8nBw ztDF0!%bqvAZh8CMz7zByI3zSIJmT?_r!lc{@d+K_;!A`FiZDHD@Z(=)Sk^9ziX)wOlz25WO`8y5tC{u9=} zk^MJZ(jcxK;28)AZR3LM2n87`Eg*PMf2Yg|XQAtXvML7A!g425@+zA}RF9pf%ip;F zQ&d6CkfP4mhV~C+|2trh{$I%c4eUR-h=4fgm^-AQ(f}Oba^56pK~q`FjB9;lL^Dz$ ztpD5!uan}ZF?%uXDzhvu-&-tqG!DwZwaoH?$JJkq1N`uk=-qyOdlEK^cmi%jK0pr= zq6vjkl1KDfF(Q^GA-xmJ&xll|2=^vd*2H&X!RFbg;m<|-gpQcs!kh2`@<(urqXT9b z*}?~Q)H<3VVn%8aA}8LklCJpsO%LffpRIZv-IFl;T0E|LAZv>n3w`>R>U#0mKtRSX zZ6tn8#cd8v<^#=oaDN{>IS*So8;e%4^YSZPJXZ6;ygl)gLJV<5gUB|K2{}q!5K$8X zUfLjfz)fA?ENx7CGE1|QrWioUF=;(6G-PQ}K3};I;WhT#C}ra| zV5KY_@oee#u)7LTHby>f{yBw_hq}oTM$Ga?WeYyQQj_G!YoF%>k7DP)7*sNBUggc9 zHRwI>9d~`nnK=^b`Z7kjDP`oDTAa1iJ%NZmQW-wML?Sq#T!-yM_Of9GxB zcMW z(v6)(a%9*%2}?#SEt2iU@}}V15aKM&)&_XOugEJFG<|>76unco&Nx4>s-i_be+YTG zl)JvQiR{3);9(0G2-<*!X&ZWh*Km^=?Sgqm+^;v{qR@R>e= zPIz8TtR2HYEui#@(C!D9&KVEE#?PNQP9~57b*~_HnO@9OK3#?)Y~sFh4ol2lKP~6h zoh~r(MS&X&Cl~V!xgRj(H+(?h_Rn;Me1Jsu>%ee*siu1ow3NZPB{@P5obG2rP19^b z4C|x(N=nQ(+Ou2BKYFG+7>@0GuQ`Yh*Zk%D+pQ!v-^tWGGa+GVL4SP=|5CGL>kz13 z`Z?#*;aJ)$wl$IWjSp1SC8JfYmfx2YnIkB^Ke=4#@4rJ@{1WZU`DeN4br(~!ZznAp z0Z!{6!3jLiH<;#51^ShmUn~nIs#5Y8>{D=%mx)EBph>D$ALEGHhWz?0M=_gu0z+Ko zi4Z0}MG9}Q9yh}n7jHk5%(~#8JbU^FHdi4P@H`^(`3i4W3QO6jj@mn^_ngRmoj8F@ z7(ZD57YU)DR$mj}kf&;)6p~55XP<1()gG#e#7NB%BBG|Wt#a1%0xZuORH>G@UZ9>( zY3ym0r|Q2*)G4{7=Yk(!vLEK1fs=EQH3g(96IvmCv9mkas4zXgyp>z<6N~p!4G$Hy zy?4WVazJ98>&EeBVG7ax`0AoKlzE-P#$`m}13N$3(#7S(bf4rjtu zY_;Dk^%?s*>I5q?JV56&djF7S($@ysDs}90ceri!$*R(mUH)$2(R9W$-C{k zW;FN7X_wvcUW=j?Z*u2zOPvaoG^?<9ZsTWnkuOhvAeZRH#tF8`NO%6sI6zqIX3KFR zVwqXoG)u8bpCz3oU87zN#m1x9G~KiiryM8IukVhA%d@({yd!|_SwYEi>(m%7pDhfN zg7rH7<8Yf{_!fZ!pR z1!LUlAVqaPTu#a;F@NiR`D)dZTL+>Bb6jj9_B1Y%;VctdW=<_P2SY9}myanaF~;Vj z{-{Z9t@cm7DdTa*`bxmJjscU+NKe2z-l|Z)cP_}Y*MiqHWJVh>fXR$Rwxvft6jR4DE?-* z_`{&-zmIh_C2!s=dXF2x#2_UN@lP-kHE=PX_gw55I_-&h@aw1{trse@Bq%n5g9RPw z!4(`wE}JLzigAJYsP?vU6CV)&aohY*rhRxJryxV?Ylx)Ch1{7FATJZ~SzkeiFrtwM%%PUJQQ+`CJ`V5#$( zX`=7@>~!~fTB4^Lwbym=Fti|+73R&^n=&aWZ>Ne3b0ZrSq=(3~uQsXIX^bg`s%FRg ztL?U=sy#4ToMPCQSIQ8x?r*Mf>@qwTD}s2UI&4eOBu6;MD6mIgj2Uly8c0u27M_sW zx_(`M7f)$HR}HP<9it9^z9v5&ui^s}Du@wnUV=*}PZ%tbtlr3tcFAE`GOh!!JRX+O zP7+5cPv}YAu{hped`IbS&~HSgyll|XZ!M+8Yk(VAl_YT7*O<8kj@kCEaEh~1rikvH zQh!9Gk?;DP72+$2W#&zBO=Xl4DBd(Pvi721P}3LlU;Ss&nF5DTeZ6*OPc)GDZRVgW zB<=45t4(EmU}rgR2uELJ7BDnhd^c6E>P_s-B{0>q1p{a{w7_u77{bR3FMv>wp-4i-8txk3Bl!TelsAiWxOjqGyMSExLc>#D&QgQ~Y zia@g?!{dy3*8@ZrT{rqwWR%=pn$k>pYhDBfuB_W5aO5Vvn>wbwJPw~;IXJh3v4+#{ zg;E!T7}?ANeBjA5$4d6CLOT20Rp&0(`p^`p(y?If5oqA7ri$LCc$;REo z%kr+t;v$R%OW_W#`Sfae|JKR8=WpTOZV@_Fnw5rcRmWUX-TIST&4aeGop@a^DHPOD z+QTHl&?4JZ<`3ieE`_Hb=UiQEgen2Tik{JjGJ3{@wsmlTF}8x`>pXhIpu|pR=|F1Q zpW%X6@7dO3+(`7ZB%%s}_?W3emrQu~?$@eol-1o}TcH{DMOed&_kH4{)|jT~W#l6` zJy4I@LaGMiDZh=F_*uU-pK~e(E<+S1h~hNPlcP{mA$%Y@fs1>zDm>tnMZ9f?xrR;Q zAacE2R(hiD;$;~>U+3w$f51**K+{%9b}s4>vzbV`W}5}%1F1dBsF7`3uOH)jPW}1h z^MdFBb_vb^jNnh`K?n9e&?%4e4zd~!%4O9I!AF!loY<)Dvizx%bqo2dvl4F%v?q}) zBMW9>1M4Z&e5~B`hRt#FXcP*{~xep0lc>3LCYCo(cVE8a%Kk{-F z>)FKHBIBaK;DbG>m)m;fpzyJ;Y22PCTb?-3pp)npxICQv1}1@mk`a&`8A9t&1Br=^ z(_O~(NFOVCM7&?Cy*O)ph$lq{AQlF^^Ql zG*CgM#Iv^Mdxb&*6@K4;zp2T>ulab_UhD6g`Hm!f{m5 z%E@QJG*o6kcwnIBW5CUo7oPX+W6sLt<=+qdUApDg9oT_H$SYO08tK(4`{G5Z(g2(>p@^~3gRa->3;IOK>4Pt=Oc9$l=b zS;8kMnlZJmDIYpi>qn>cHKRW~cryFr=rS@7OF!Kx#~Z`YU6PZLHNSs32L659)c^cOr)Dt56(#6NotXFHExPh5M Z%gSwGot#h0(`TYj%bkP#4=ltV`7e14WN-ig diff --git a/doc/ChannelTalk_logo.png b/doc/ChannelTalk_logo.png deleted file mode 100644 index d9cd28428543bc17469fff415b94c582fd17a884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9235 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 diff --git a/doc/embark.jpg b/doc/embark.jpg deleted file mode 100644 index 3ae23a6be15931a1b29bf47a923e0fade189b4f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12976 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 Vhl4^1IGc-5R#2CJBWD)&e*j#+S%d%p diff --git a/doc/uncheck.png b/doc/uncheck.png deleted file mode 100644 index d71f0f7e7b92293157a074f3e1bc21cf4108c389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5898 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 WOqO~71>AHCk(E@Gs1!5$?|%Sr!}})y diff --git a/doc/webrtc.rs.xcf b/doc/webrtc.rs.xcf deleted file mode 100644 index 8cc72dfe5ab9ccb8c5efd189c5d0a61309ac106c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54671 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 DlMT>V diff --git a/doc/webrtc_crab.png b/doc/webrtc_crab.png deleted file mode 100644 index 42f785d73c840175ef772cab3bb1e0f6c7a14f38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37282 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 QG8f>6k-1?N{NeNe0cq_U_y7O^ diff --git a/doc/webrtc_crates_dep_graph.odg b/doc/webrtc_crates_dep_graph.odg deleted file mode 100644 index c489207a0b51b669d365264e672520159a826432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16421 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 Du0nQv diff --git a/doc/webrtc_crates_dep_graph.png b/doc/webrtc_crates_dep_graph.png deleted file mode 100644 index c041bb70569617cb667d25b0e4863d645bfaf038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36071 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 bKA-(R98`e3_S}<-00000NkvXXu0mjfT-rdQ diff --git a/doc/webrtc_stack.png b/doc/webrtc_stack.png deleted file mode 100644 index 9a9bc84df38a113c1c9511fb8bdc02722a8a0bc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39555 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 Q7XuJ@y85}Sb4q9e0L-UBVgLXD diff --git a/dtls/.gitignore b/dtls/.gitignore deleted file mode 100644 index 9db22ce71..000000000 --- a/dtls/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/examples/hub/target -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/dtls/CHANGELOG.md b/dtls/CHANGELOG.md deleted file mode 100644 index 071897700..000000000 --- a/dtls/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# webrtc-dtls changelog - -## Unreleased - -## v0.7.1 - -* Added support for insecure/deprecated signature verification algorithms [#342](https://github.com/webrtc-rs/webrtc/pull/342) by [@chuigda](https://github.com/chuigda). - -## v0.7.0 - -* Increased minimum support rust version to `1.60.0`. -* Add `RTCCertificate::from_pem` and `RTCCertificate::serialize_pem` (only work with `pem` feature enabled) [#333](https://github.com/webrtc-rs/webrtc/pull/333) - -### Breaking - -* Increased required `webrtc-util` version to `0.7.0`, with this change some methods in `DTLSConn` that implement `webrtc_util::Conn` have changed from async to sync. - -## v0.6.0 - -* [#254 [DTLS] Add NamedCurve::P384](https://github.com/webrtc-rs/webrtc/pull/254) contributed by [neonphog](https://github.com/neonphog) -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). -* Increased serde's minimum version to 1.0.110 [#243 Fixes for cargo minimal-versions](https://github.com/webrtc-rs/webrtc/pull/243) contributed by [algesten](https://github.com/algesten) - -## Prior to 0.6.0 - -Before 0.6.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/dtls/releases). diff --git a/dtls/Cargo.toml b/dtls/Cargo.toml deleted file mode 100644 index 7f31126d7..000000000 --- a/dtls/Cargo.toml +++ /dev/null @@ -1,94 +0,0 @@ -[package] -name = "webrtc-dtls" -version = "0.10.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of DTLS" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-dtls" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/dtls" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn"] } - -byteorder = "1" -rand_core = "0.6" -hkdf = "0.12" -p256 = { version = "0.13", features = ["default", "ecdh", "ecdsa"] } -p384 = "0.13" -rand = "0.8" -hmac = "0.12" -sec1 = { version = "0.7", features = [ "std" ] } -sha1 = "0.10" -sha2 = "0.10" -aes = "0.8" -cbc = { version = "0.1", features = [ "block-padding", "alloc"] } -aes-gcm = "0.10" -ccm = "0.5" -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -async-trait = "0.1" -x25519-dalek = { version = "2", features = ["static_secrets"] } -x509-parser = "0.16" -der-parser = "9.0" -rcgen = "0.13" -ring = "0.17" -rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } -bincode = "1" -serde = { version = "1", features = ["derive"] } -subtle = "2" -log = "0.4" -thiserror = "1" -pem = { version = "3", optional = true } -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -env_logger = "0.10" -chrono = "0.4.28" -clap = "3" -hub = {path = "examples/hub"} - -[features] -pem = ["dep:pem"] - -[[example]] -name = "dial_psk" -path = "examples/dial/psk/dial_psk.rs" -bench = false - -[[example]] -name = "dial_selfsign" -path = "examples/dial/selfsign/dial_selfsign.rs" -bench = false - -[[example]] -name = "dial_verify" -path = "examples/dial/verify/dial_verify.rs" -bench = false - -[[example]] -name = "listen_psk" -path = "examples/listen/psk/listen_psk.rs" -bench = false - -[[example]] -name = "listen_selfsign" -path = "examples/listen/selfsign/listen_selfsign.rs" -bench = false - -[[example]] -name = "listen_verify" -path = "examples/listen/verify/listen_verify.rs" -bench = false diff --git a/dtls/LICENSE-APACHE b/dtls/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/dtls/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/dtls/LICENSE-MIT b/dtls/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/dtls/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/dtls/README.md b/dtls/README.md deleted file mode 100644 index 1ef5cb95c..000000000 --- a/dtls/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of DTLS. Rewrite Pion DTLS in Rust -

diff --git a/dtls/codecov.yml b/dtls/codecov.yml deleted file mode 100644 index a405022ef..000000000 --- a/dtls/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 14bc9fb7-bdcf-4355-8e0e-ebec14066ae5 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/dtls/doc/webrtc.rs.png b/dtls/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/dtls/examples/certificates/README.md b/dtls/examples/certificates/README.md deleted file mode 100644 index cd498e0ef..000000000 --- a/dtls/examples/certificates/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Certificates - -The certificates in for the examples are generated using the commands shown below. - -Note that this was run on OpenSSL 1.1.1d, of which the arguments can be found in the [OpenSSL Manpages](https://www.openssl.org/docs/man1.1.1/man1), and is not guaranteed to work on different OpenSSL versions. - -```shell -# Extensions required for certificate validation. -$ EXTFILE='extfile.conf' -$ echo 'subjectAltName = DNS:webrtc.rs' > "${EXTFILE}" - -# Server. -$ SERVER_NAME='server' -$ openssl ecparam -name prime256v1 -genkey -noout -out "${SERVER_NAME}.pem" -$ openssl req -key "${SERVER_NAME}.pem" -new -sha256 -subj '/C=NL' -out "${SERVER_NAME}.csr" -$ openssl x509 -req -in "${SERVER_NAME}.csr" -extfile "${EXTFILE}" -days 365 -signkey "${SERVER_NAME}.pem" -sha256 -out "${SERVER_NAME}.pub.pem" - -# Client. -$ CLIENT_NAME='client' -$ openssl ecparam -name prime256v1 -genkey -noout -out "${CLIENT_NAME}.pem" -$ openssl req -key "${CLIENT_NAME}.pem" -new -sha256 -subj '/C=NL' -out "${CLIENT_NAME}.csr" -$ openssl x509 -req -in "${CLIENT_NAME}.csr" -extfile "${EXTFILE}" -days 365 -CA "${SERVER_NAME}.pub.pem" -CAkey "${SERVER_NAME}.pem" -set_serial '0xabcd' -sha256 -out "${CLIENT_NAME}.pub.pem" - -# Cleanup. -$ rm "${EXTFILE}" "${SERVER_NAME}.csr" "${CLIENT_NAME}.csr" -``` - -## Converting EC private key to PKCS#8 in Rust - -`Cargo.toml`: - -```toml -[dependencies] -topk8 = "0.0.1" -``` - -`main.rs`: - -```rust -fn main() { - let ec_pem = " ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIAL4r6d9lPq3XEDSZTL9l0D6thrPM7RiAhl3Fjuw9Ji2oAoGCCqGSM49 -AwEHoUQDQgAE4U64dviQRMujGK0g80dwzgjV7fnwLkj6RfvINMHvD6eiCsphWIlq -cddTAoOjXVQDu3qMAS1Ghfyk1F377EW1Sw== ------END EC PRIVATE KEY----- -"; - - let pkcs8_pem = topk8::from_sec1_pem(ec_pem).unwrap(); - - println!("{}", pkcs8_pem); - - // -----BEGIN PRIVATE KEY----- - // MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgAvivp32U+rdcQNJl - // Mv2XQPq2Gs8ztGICGXcWO7D0mLagCgYIKoZIzj0DAQehRANCAAThTrh2+JBEy6MY - // rSDzR3DOCNXt+fAuSPpF+8g0we8Pp6IKymFYiWpx11MCg6NdVAO7eowBLUaF/KTU - // XfvsRbVL - // -----END PRIVATE KEY----- -} -``` diff --git a/dtls/examples/certificates/client.csr b/dtls/examples/certificates/client.csr deleted file mode 100644 index 3a41d57b8..000000000 --- a/dtls/examples/certificates/client.csr +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIHHMG8CAQAwDTELMAkGA1UEBhMCTkwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC -AAQhwOK0F+5DQy5FG0dRdN0GF20p3MsTaBk73IpNzrlK+WtgxdVxmRm55LWCTgkA -RhnOcmzXW+raCEWQgTadaLd5oAAwCgYIKoZIzj0EAwIDSAAwRQIhANDZpyL2lr50 -Xr5DrD19SOa7LXpXz3DcM8RDLcBQvx05AiB7mbtcY6I18diHU0jSxHAGcUn5nAeD -EP4tqFOz7QRzgQ== ------END CERTIFICATE REQUEST----- diff --git a/dtls/examples/certificates/client.pem b/dtls/examples/certificates/client.pem deleted file mode 100644 index aa73a43a2..000000000 --- a/dtls/examples/certificates/client.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIO7fb5dmM2P0F71o/Clo0ElO29ud+JbtA3fhDIL15AgioAoGCCqGSM49 -AwEHoUQDQgAEIcDitBfuQ0MuRRtHUXTdBhdtKdzLE2gZO9yKTc65SvlrYMXVcZkZ -ueS1gk4JAEYZznJs11vq2ghFkIE2nWi3eQ== ------END EC PRIVATE KEY----- diff --git a/dtls/examples/certificates/client.pem.private_key.pem b/dtls/examples/certificates/client.pem.private_key.pem deleted file mode 100644 index fbcbd476f..000000000 --- a/dtls/examples/certificates/client.pem.private_key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7t9vl2YzY/QXvWj8 -KWjQSU7b2534lu0Dd+EMgvXkCCKhRANCAAQhwOK0F+5DQy5FG0dRdN0GF20p3MsT -aBk73IpNzrlK+WtgxdVxmRm55LWCTgkARhnOcmzXW+raCEWQgTadaLd5 ------END PRIVATE KEY----- diff --git a/dtls/examples/certificates/client.pub.pem b/dtls/examples/certificates/client.pub.pem deleted file mode 100644 index 688c9f05a..000000000 --- a/dtls/examples/certificates/client.pub.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBITCByaADAgECAgMAq80wCgYIKoZIzj0EAwIwDTELMAkGA1UEBhMCTkwwHhcN -MjEwOTE4MjAzNzE1WhcNMjIwOTE4MjAzNzE1WjANMQswCQYDVQQGEwJOTDBZMBMG -ByqGSM49AgEGCCqGSM49AwEHA0IABCHA4rQX7kNDLkUbR1F03QYXbSncyxNoGTvc -ik3OuUr5a2DF1XGZGbnktYJOCQBGGc5ybNdb6toIRZCBNp1ot3mjGDAWMBQGA1Ud -EQQNMAuCCXdlYnJ0Yy5yczAKBggqhkjOPQQDAgNHADBEAiA8mpJVfaCw+RwALmxN -XD28Ze3DUPomlfXhx+NGuePt5QIgAcRxvuDctyL07f8pQ5n22NOioNHdjwOjxww+ -ZekD+Lg= ------END CERTIFICATE----- diff --git a/dtls/examples/certificates/extfile.conf b/dtls/examples/certificates/extfile.conf deleted file mode 100644 index c4b7ccea7..000000000 --- a/dtls/examples/certificates/extfile.conf +++ /dev/null @@ -1 +0,0 @@ -subjectAltName = DNS:webrtc.rs diff --git a/dtls/examples/certificates/server.csr b/dtls/examples/certificates/server.csr deleted file mode 100644 index cf9a98384..000000000 --- a/dtls/examples/certificates/server.csr +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIHHMG8CAQAwDTELMAkGA1UEBhMCTkwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC -AASRyJbgbcieCbC1/HbiqmADkPxfk5Bmwjei2YXhPE+oYS3F5+df4BKNBgs7py7H -sxc768+6X8HmvYlfvk2kHXAVoAAwCgYIKoZIzj0EAwIDSAAwRQIhAKR9rI22Xk/U -L3xp2dzn7q3nyWqgDvp5uTflP4t0MBpJAiAJDKmcOCXNMhhgg4T2lhdfz/pZVfu5 -lxLcZm2ELiYImQ== ------END CERTIFICATE REQUEST----- diff --git a/dtls/examples/certificates/server.pem b/dtls/examples/certificates/server.pem deleted file mode 100644 index c6dd91395..000000000 --- a/dtls/examples/certificates/server.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEID358pSfZXZqwqURqBLvLYcqhOdZVVNR2toCMER39YHboAoGCCqGSM49 -AwEHoUQDQgAEkciW4G3Ingmwtfx24qpgA5D8X5OQZsI3otmF4TxPqGEtxefnX+AS -jQYLO6cux7MXO+vPul/B5r2JX75NpB1wFQ== ------END EC PRIVATE KEY----- diff --git a/dtls/examples/certificates/server.pem.private_key.pem b/dtls/examples/certificates/server.pem.private_key.pem deleted file mode 100644 index b5088e564..000000000 --- a/dtls/examples/certificates/server.pem.private_key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPfnylJ9ldmrCpRGo -Eu8thyqE51lVU1Ha2gIwRHf1gduhRANCAASRyJbgbcieCbC1/HbiqmADkPxfk5Bm -wjei2YXhPE+oYS3F5+df4BKNBgs7py7Hsxc768+6X8HmvYlfvk2kHXAV ------END PRIVATE KEY----- diff --git a/dtls/examples/certificates/server.pub.pem b/dtls/examples/certificates/server.pub.pem deleted file mode 100644 index bfdd542be..000000000 --- a/dtls/examples/certificates/server.pub.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBMjCB2qADAgECAhQzsfVoH1cRfcxCY/drp/cKdjvBDDAKBggqhkjOPQQDAjAN -MQswCQYDVQQGEwJOTDAeFw0yMTA5MTgyMDM2NTVaFw0yMjA5MTgyMDM2NTVaMA0x -CzAJBgNVBAYTAk5MMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkciW4G3Ingmw -tfx24qpgA5D8X5OQZsI3otmF4TxPqGEtxefnX+ASjQYLO6cux7MXO+vPul/B5r2J -X75NpB1wFaMYMBYwFAYDVR0RBA0wC4IJd2VicnRjLnJzMAoGCCqGSM49BAMCA0cA -MEQCIBZBGmNM3qig7OTMZLL4PYj4JrGMjIj/jZFHEhqeQn6HAiBpRte9WzCjJzZX -vzRkUKfCs1NMa/XR0hfdaa8KJAdKyQ== ------END CERTIFICATE----- diff --git a/dtls/examples/dial/psk/dial_psk.rs b/dtls/examples/dial/psk/dial_psk.rs deleted file mode 100644 index 9f73de915..000000000 --- a/dtls/examples/dial/psk/dial_psk.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use util::Conn; -use webrtc_dtls::cipher_suite::CipherSuiteId; -use webrtc_dtls::config::*; -use webrtc_dtls::conn::DTLSConn; -use webrtc_dtls::Error; - -// cargo run --example dial_psk -- --server 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Client") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Client") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("server") - .help("DTLS Server name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - conn.connect(server).await?; - println!("connecting {server}.."); - - let config = Config { - psk: Some(Arc::new(|hint: &[u8]| -> Result, Error> { - println!("Server's hint: {}", String::from_utf8(hint.to_vec())?); - Ok(vec![0xAB, 0xC1, 0x23]) - })), - psk_identity_hint: Some("webrtc-rs DTLS Server".as_bytes().to_vec()), - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }; - let dtls_conn: Arc = - Arc::new(DTLSConn::new(conn, config, true, None).await?); - - println!("Connected; type 'exit' to shutdown gracefully"); - let _ = hub::utilities::chat(Arc::clone(&dtls_conn)).await; - - dtls_conn.close().await?; - - Ok(()) -} diff --git a/dtls/examples/dial/selfsign/dial_selfsign.rs b/dtls/examples/dial/selfsign/dial_selfsign.rs deleted file mode 100644 index 340a4635d..000000000 --- a/dtls/examples/dial/selfsign/dial_selfsign.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use util::Conn; -use webrtc_dtls::config::*; -use webrtc_dtls::conn::DTLSConn; -use webrtc_dtls::crypto::Certificate; -use webrtc_dtls::Error; - -// cargo run --example dial_selfsign -- --server 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Client") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Client") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("server") - .help("DTLS Server name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - conn.connect(server).await?; - println!("connecting {server}.."); - - // Generate a certificate and private key to secure the connection - let certificate = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - - let config = Config { - certificates: vec![certificate], - insecure_skip_verify: true, - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }; - let dtls_conn: Arc = - Arc::new(DTLSConn::new(conn, config, true, None).await?); - - println!("Connected; type 'exit' to shutdown gracefully"); - let _ = hub::utilities::chat(Arc::clone(&dtls_conn)).await; - - dtls_conn.close().await?; - - Ok(()) -} diff --git a/dtls/examples/dial/verify/dial_verify.rs b/dtls/examples/dial/verify/dial_verify.rs deleted file mode 100644 index 2f79ef224..000000000 --- a/dtls/examples/dial/verify/dial_verify.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use hub::utilities::load_certificate; -use tokio::net::UdpSocket; -use util::Conn; -use webrtc_dtls::config::*; -use webrtc_dtls::conn::DTLSConn; -use webrtc_dtls::Error; - -// cargo run --example dial_verify -- --server 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Client") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Client") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("server") - .help("DTLS Server name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - conn.connect(server).await?; - println!("connecting {server}.."); - - let certificate = hub::utilities::load_key_and_certificate( - "dtls/examples/certificates/client.pem.private_key.pem".into(), - "dtls/examples/certificates/client.pub.pem".into(), - )?; - - let mut cert_pool = rustls::RootCertStore::empty(); - let certs = load_certificate("dtls/examples/certificates/server.pub.pem".into())?; - for cert in &certs { - if cert_pool.add(cert.to_owned()).is_err() { - return Err(Error::Other("cert_pool add_pem_file failed".to_owned())); - } - } - - let config = Config { - certificates: vec![certificate], - extended_master_secret: ExtendedMasterSecretType::Require, - roots_cas: cert_pool, - server_name: "webrtc.rs".to_owned(), - ..Default::default() - }; - let dtls_conn: Arc = - Arc::new(DTLSConn::new(conn, config, true, None).await?); - - println!("Connected; type 'exit' to shutdown gracefully"); - let _ = hub::utilities::chat(Arc::clone(&dtls_conn)).await; - - dtls_conn.close().await?; - - Ok(()) -} diff --git a/dtls/examples/hub/Cargo.toml b/dtls/examples/hub/Cargo.toml deleted file mode 100644 index 72f3992e8..000000000 --- a/dtls/examples/hub/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "hub" -version = "0.1.0" -edition = "2021" - -[dependencies] -util = { path = "../../../util", package = "webrtc-util", default-features = false, features = [ - "conn" -] } -dtls = { package = "webrtc-dtls", path = "../../" } - -tokio = { version = "1.32.0", features = ["full"] } -rcgen = { version = "0.13", features = ["pem", "x509-parser"] } -rustls = { version = "0.23", default-features = false } -rustls-pemfile = "2" -thiserror = "1" diff --git a/dtls/examples/hub/src/lib.rs b/dtls/examples/hub/src/lib.rs deleted file mode 100644 index 2b07063a2..000000000 --- a/dtls/examples/hub/src/lib.rs +++ /dev/null @@ -1,112 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod utilities; - -use std::collections::HashMap; -use std::io::{BufRead, BufReader}; -use std::sync::Arc; - -use dtls::Error; -use tokio::sync::Mutex; -use util::Conn; - -const BUF_SIZE: usize = 8192; - -/// Hub is a helper to handle one to many chat -#[derive(Default)] -pub struct Hub { - conns: Arc>>>, -} - -impl Hub { - /// new builds a new hub - pub fn new() -> Self { - Hub { - conns: Arc::new(Mutex::new(HashMap::new())), - } - } - - /// register adds a new conn to the Hub - pub async fn register(&self, conn: Arc) { - println!("Connected to {}", conn.remote_addr().unwrap()); - - if let Some(remote_addr) = conn.remote_addr() { - let mut conns = self.conns.lock().await; - conns.insert(remote_addr.to_string(), Arc::clone(&conn)); - } - - let conns = Arc::clone(&self.conns); - tokio::spawn(async move { - let _ = Hub::read_loop(conns, conn).await; - }); - } - - async fn read_loop( - conns: Arc>>>, - conn: Arc, - ) -> Result<(), Error> { - let mut b = vec![0u8; BUF_SIZE]; - - while let Ok(n) = conn.recv(&mut b).await { - let msg = String::from_utf8(b[..n].to_vec())?; - print!("Got message: {msg}"); - } - - Hub::unregister(conns, conn).await - } - - async fn unregister( - conns: Arc>>>, - conn: Arc, - ) -> Result<(), Error> { - if let Some(remote_addr) = conn.remote_addr() { - { - let mut cs = conns.lock().await; - cs.remove(&remote_addr.to_string()); - } - - if let Err(err) = conn.close().await { - println!("Failed to disconnect: {remote_addr} with err {err}"); - } else { - println!("Disconnected: {remote_addr} "); - } - } - - Ok(()) - } - - async fn broadcast(&self, msg: &[u8]) { - let conns = self.conns.lock().await; - for conn in conns.values() { - if let Err(err) = conn.send(msg).await { - println!( - "Failed to write message to {:?}: {}", - conn.remote_addr(), - err - ); - } - } - } - - /// Chat starts the stdin readloop to dispatch messages to the hub - pub async fn chat(&self) { - let input = std::io::stdin(); - let mut reader = BufReader::new(input.lock()); - loop { - let mut msg = String::new(); - match reader.read_line(&mut msg) { - Ok(0) => return, - Err(err) => { - println!("stdin read err: {err}"); - return; - } - _ => {} - }; - if msg.trim() == "exit" { - return; - } - self.broadcast(msg.as_bytes()).await; - } - } -} diff --git a/dtls/examples/hub/src/utilities.rs b/dtls/examples/hub/src/utilities.rs deleted file mode 100644 index 912b74bc7..000000000 --- a/dtls/examples/hub/src/utilities.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::fs::File; -use std::io::{self, Read}; -use std::path::PathBuf; - -use dtls::crypto::{Certificate, CryptoPrivateKey}; -use rcgen::KeyPair; -use rustls::pki_types::CertificateDer; -use thiserror::Error; - -use super::*; - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum Error { - #[error("block is not a private key, unable to load key")] - ErrBlockIsNotPrivateKey, - #[error("unknown key time in PKCS#8 wrapping, unable to load key")] - ErrUnknownKeyTime, - #[error("no private key found, unable to load key")] - ErrNoPrivateKeyFound, - #[error("block is not a certificate, unable to load certificates")] - ErrBlockIsNotCertificate, - #[error("no certificate found, unable to load certificates")] - ErrNoCertificateFound, - - #[error("{0}")] - Other(String), -} - -impl From for dtls::Error { - fn from(e: Error) -> Self { - dtls::Error::Other(e.to_string()) - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Other(e.to_string()) - } -} - -/// chat simulates a simple text chat session over the connection -pub async fn chat(conn: Arc) -> Result<(), Error> { - let conn_rx = Arc::clone(&conn); - tokio::spawn(async move { - let mut b = vec![0u8; BUF_SIZE]; - - while let Ok(n) = conn_rx.recv(&mut b).await { - let msg = String::from_utf8(b[..n].to_vec()).expect("utf8"); - print!("Got message: {msg}"); - } - - Result::<(), Error>::Ok(()) - }); - - let input = std::io::stdin(); - let mut reader = BufReader::new(input.lock()); - loop { - let mut msg = String::new(); - match reader.read_line(&mut msg) { - Ok(0) => return Ok(()), - Err(err) => { - println!("stdin read err: {err}"); - return Ok(()); - } - _ => {} - }; - if msg.trim() == "exit" { - return Ok(()); - } - - let _ = conn.send(msg.as_bytes()).await; - } -} - -/// load_key_and_certificate reads certificates or key from file -pub fn load_key_and_certificate( - key_path: PathBuf, - certificate_path: PathBuf, -) -> Result { - let private_key = load_key(key_path)?; - - let certificate = load_certificate(certificate_path)?; - - Ok(Certificate { - certificate, - private_key, - }) -} - -/// load_key Load/read key from file -pub fn load_key(path: PathBuf) -> Result { - let f = File::open(path)?; - let mut reader = BufReader::new(f); - let mut buf = vec![]; - reader.read_to_end(&mut buf)?; - - let s = String::from_utf8(buf).expect("utf8 of file"); - - let key_pair = KeyPair::from_pem(s.as_str()).expect("key pair in file"); - - Ok(CryptoPrivateKey::from_key_pair(&key_pair).expect("crypto key pair")) -} - -/// load_certificate Load/read certificate(s) from file -pub fn load_certificate(path: PathBuf) -> Result>, Error> { - let f = File::open(path)?; - - let mut reader = BufReader::new(f); - match rustls_pemfile::certs(&mut reader).collect::, _>>() { - Ok(certs) => Ok(certs.into_iter().map(CertificateDer::from).collect()), - Err(_) => Err(Error::ErrNoCertificateFound), - } -} diff --git a/dtls/examples/listen/psk/listen_psk.rs b/dtls/examples/listen/psk/listen_psk.rs deleted file mode 100644 index fc536bb6e..000000000 --- a/dtls/examples/listen/psk/listen_psk.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use util::conn::*; -use webrtc_dtls::cipher_suite::CipherSuiteId; -use webrtc_dtls::config::{Config, ExtendedMasterSecretType}; -use webrtc_dtls::listener::listen; -use webrtc_dtls::Error; - -// cargo run --example listen_psk -- --host 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Server") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Server") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("host") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("host") - .help("DTLS host name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let host = matches.value_of("host").unwrap().to_owned(); - - let cfg = Config { - psk: Some(Arc::new(|hint: &[u8]| -> Result, Error> { - println!("Client's hint: {}", String::from_utf8(hint.to_vec())?); - Ok(vec![0xAB, 0xC1, 0x23]) - })), - psk_identity_hint: Some("webrtc-rs DTLS Client".as_bytes().to_vec()), - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }; - - println!("listening {host}...\ntype 'exit' to shutdown gracefully"); - - let listener = Arc::new(listen(host, cfg).await?); - - // Simulate a chat session - let h = Arc::new(hub::Hub::new()); - - let listener2 = Arc::clone(&listener); - let h2 = Arc::clone(&h); - tokio::spawn(async move { - while let Ok((dtls_conn, _remote_addr)) = listener2.accept().await { - // Register the connection with the chat hub - h2.register(dtls_conn).await; - } - }); - - h.chat().await; - - Ok(listener.close().await?) -} diff --git a/dtls/examples/listen/selfsign/listen_selfsign.rs b/dtls/examples/listen/selfsign/listen_selfsign.rs deleted file mode 100644 index 19a54fdc9..000000000 --- a/dtls/examples/listen/selfsign/listen_selfsign.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use util::conn::*; -use webrtc_dtls::config::{Config, ExtendedMasterSecretType}; -use webrtc_dtls::crypto::Certificate; -use webrtc_dtls::listener::listen; -use webrtc_dtls::Error; - -// cargo run --example listen_selfsign -- --host 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Server") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Server") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("host") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("host") - .help("DTLS host name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let host = matches.value_of("host").unwrap().to_owned(); - - // Generate a certificate and private key to secure the connection - let certificate = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - - let cfg = Config { - certificates: vec![certificate], - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }; - - println!("listening {host}...\ntype 'exit' to shutdown gracefully"); - - let listener = Arc::new(listen(host, cfg).await?); - - // Simulate a chat session - let h = Arc::new(hub::Hub::new()); - - let listener2 = Arc::clone(&listener); - let h2 = Arc::clone(&h); - tokio::spawn(async move { - while let Ok((dtls_conn, _remote_addr)) = listener2.accept().await { - // Register the connection with the chat hub - h2.register(dtls_conn).await; - } - }); - - h.chat().await; - - Ok(listener.close().await?) -} diff --git a/dtls/examples/listen/verify/listen_verify.rs b/dtls/examples/listen/verify/listen_verify.rs deleted file mode 100644 index e63f7ea64..000000000 --- a/dtls/examples/listen/verify/listen_verify.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use hub::utilities::load_certificate; -use util::conn::*; -use webrtc_dtls::config::{ClientAuthType, Config, ExtendedMasterSecretType}; -use webrtc_dtls::listener::listen; -use webrtc_dtls::Error; - -// cargo run --example listen_verify -- --host 127.0.0.1:4444 - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("DTLS Server") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of DTLS Server") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("host") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("127.0.0.1:4444") - .long("host") - .help("DTLS host name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let host = matches.value_of("host").unwrap().to_owned(); - - let certificate = hub::utilities::load_key_and_certificate( - "dtls/examples/certificates/server.pem.private_key.pem".into(), - "dtls/examples/certificates/server.pub.pem".into(), - )?; - - let mut cert_pool = rustls::RootCertStore::empty(); - let certs = load_certificate("dtls/examples/certificates/server.pub.pem".into())?; - for cert in &certs { - if cert_pool.add(cert.to_owned()).is_err() { - return Err(Error::Other("cert_pool add_pem_file failed".to_owned())); - } - } - - let cfg = Config { - certificates: vec![certificate], - extended_master_secret: ExtendedMasterSecretType::Require, - client_auth: ClientAuthType::RequireAndVerifyClientCert, //RequireAnyClientCert, // - client_cas: cert_pool, - ..Default::default() - }; - - println!("listening {host}...\ntype 'exit' to shutdown gracefully"); - - let listener = Arc::new(listen(host, cfg).await?); - - // Simulate a chat session - let h = Arc::new(hub::Hub::new()); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let mut done_tx = Some(done_tx); - - let listener2 = Arc::clone(&listener); - let h2 = Arc::clone(&h); - tokio::spawn(async move { - loop { - tokio::select! { - _ = done_rx.recv() => { - break; - } - result = listener2.accept() => { - match result{ - Ok((dtls_conn, _)) => { - // Register the connection with the chat hub - h2.register(dtls_conn).await; - } - Err(err) => { - println!("connecting failed with error: {err}"); - } - } - } - } - } - }); - - h.chat().await; - - done_tx.take(); - - Ok(listener.close().await?) -} diff --git a/dtls/src/alert/alert_test.rs b/dtls/src/alert/alert_test.rs deleted file mode 100644 index ad9fff9fb..000000000 --- a/dtls/src/alert/alert_test.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; -use crate::error::Error; - -#[test] -fn test_alert() -> Result<()> { - let tests = vec![ - ( - "Valid Alert", - vec![0x02, 0x0A], - Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnexpectedMessage, - }, - None, - ), - ( - "Invalid alert length", - vec![0x00], - Alert { - alert_level: AlertLevel::Invalid, - alert_description: AlertDescription::Invalid, - }, - Some(Error::Other("io".to_owned())), - ), - ]; - - for (name, data, wanted, unmarshal_error) in tests { - let mut reader = BufReader::new(data.as_slice()); - let result = Alert::unmarshal(&mut reader); - - if let Some(err) = unmarshal_error { - assert!(result.is_err(), "{name} expected error: {err}"); - } else if let Ok(alert) = result { - assert_eq!(wanted, alert, "{name} expected {wanted}, but got {alert}"); - - let mut data2: Vec = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(data2.as_mut()); - alert.marshal(&mut writer)?; - } - assert_eq!(data, data2, "{name} expected {data:?}, but got {data2:?}"); - } else { - assert!(result.is_ok(), "{name} expected Ok, but has error"); - } - } - - Ok(()) -} diff --git a/dtls/src/alert/mod.rs b/dtls/src/alert/mod.rs deleted file mode 100644 index 197aec22a..000000000 --- a/dtls/src/alert/mod.rs +++ /dev/null @@ -1,185 +0,0 @@ -#[cfg(test)] -mod alert_test; - -use std::fmt; -use std::io::{Read, Write}; - -use byteorder::{ReadBytesExt, WriteBytesExt}; - -use super::content::*; -use crate::error::Result; - -#[derive(Copy, Clone, PartialEq, Debug)] -pub(crate) enum AlertLevel { - Warning = 1, - Fatal = 2, - Invalid, -} - -impl fmt::Display for AlertLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - AlertLevel::Warning => write!(f, "LevelWarning"), - AlertLevel::Fatal => write!(f, "LevelFatal"), - _ => write!(f, "Invalid alert level"), - } - } -} - -impl From for AlertLevel { - fn from(val: u8) -> Self { - match val { - 1 => AlertLevel::Warning, - 2 => AlertLevel::Fatal, - _ => AlertLevel::Invalid, - } - } -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub(crate) enum AlertDescription { - CloseNotify = 0, - UnexpectedMessage = 10, - BadRecordMac = 20, - DecryptionFailed = 21, - RecordOverflow = 22, - DecompressionFailure = 30, - HandshakeFailure = 40, - NoCertificate = 41, - BadCertificate = 42, - UnsupportedCertificate = 43, - CertificateRevoked = 44, - CertificateExpired = 45, - CertificateUnknown = 46, - IllegalParameter = 47, - UnknownCa = 48, - AccessDenied = 49, - DecodeError = 50, - DecryptError = 51, - ExportRestriction = 60, - ProtocolVersion = 70, - InsufficientSecurity = 71, - InternalError = 80, - UserCanceled = 90, - NoRenegotiation = 100, - UnsupportedExtension = 110, - UnknownPskIdentity = 115, - Invalid, -} - -impl fmt::Display for AlertDescription { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - AlertDescription::CloseNotify => write!(f, "CloseNotify"), - AlertDescription::UnexpectedMessage => write!(f, "UnexpectedMessage"), - AlertDescription::BadRecordMac => write!(f, "BadRecordMac"), - AlertDescription::DecryptionFailed => write!(f, "DecryptionFailed"), - AlertDescription::RecordOverflow => write!(f, "RecordOverflow"), - AlertDescription::DecompressionFailure => write!(f, "DecompressionFailure"), - AlertDescription::HandshakeFailure => write!(f, "HandshakeFailure"), - AlertDescription::NoCertificate => write!(f, "NoCertificate"), - AlertDescription::BadCertificate => write!(f, "BadCertificate"), - AlertDescription::UnsupportedCertificate => write!(f, "UnsupportedCertificate"), - AlertDescription::CertificateRevoked => write!(f, "CertificateRevoked"), - AlertDescription::CertificateExpired => write!(f, "CertificateExpired"), - AlertDescription::CertificateUnknown => write!(f, "CertificateUnknown"), - AlertDescription::IllegalParameter => write!(f, "IllegalParameter"), - AlertDescription::UnknownCa => write!(f, "UnknownCA"), - AlertDescription::AccessDenied => write!(f, "AccessDenied"), - AlertDescription::DecodeError => write!(f, "DecodeError"), - AlertDescription::DecryptError => write!(f, "DecryptError"), - AlertDescription::ExportRestriction => write!(f, "ExportRestriction"), - AlertDescription::ProtocolVersion => write!(f, "ProtocolVersion"), - AlertDescription::InsufficientSecurity => write!(f, "InsufficientSecurity"), - AlertDescription::InternalError => write!(f, "InternalError"), - AlertDescription::UserCanceled => write!(f, "UserCanceled"), - AlertDescription::NoRenegotiation => write!(f, "NoRenegotiation"), - AlertDescription::UnsupportedExtension => write!(f, "UnsupportedExtension"), - AlertDescription::UnknownPskIdentity => write!(f, "UnknownPskIdentity"), - _ => write!(f, "Invalid alert description"), - } - } -} - -impl From for AlertDescription { - fn from(val: u8) -> Self { - match val { - 0 => AlertDescription::CloseNotify, - 10 => AlertDescription::UnexpectedMessage, - 20 => AlertDescription::BadRecordMac, - 21 => AlertDescription::DecryptionFailed, - 22 => AlertDescription::RecordOverflow, - 30 => AlertDescription::DecompressionFailure, - 40 => AlertDescription::HandshakeFailure, - 41 => AlertDescription::NoCertificate, - 42 => AlertDescription::BadCertificate, - 43 => AlertDescription::UnsupportedCertificate, - 44 => AlertDescription::CertificateRevoked, - 45 => AlertDescription::CertificateExpired, - 46 => AlertDescription::CertificateUnknown, - 47 => AlertDescription::IllegalParameter, - 48 => AlertDescription::UnknownCa, - 49 => AlertDescription::AccessDenied, - 50 => AlertDescription::DecodeError, - 51 => AlertDescription::DecryptError, - 60 => AlertDescription::ExportRestriction, - 70 => AlertDescription::ProtocolVersion, - 71 => AlertDescription::InsufficientSecurity, - 80 => AlertDescription::InternalError, - 90 => AlertDescription::UserCanceled, - 100 => AlertDescription::NoRenegotiation, - 110 => AlertDescription::UnsupportedExtension, - 115 => AlertDescription::UnknownPskIdentity, - _ => AlertDescription::Invalid, - } - } -} - -// One of the content types supported by the TLS record layer is the -// alert type. Alert messages convey the severity of the message -// (warning or fatal) and a description of the alert. Alert messages -// with a level of fatal result in the immediate termination of the -// connection. In this case, other connections corresponding to the -// session may continue, but the session identifier MUST be invalidated, -// preventing the failed session from being used to establish new -// connections. Like other messages, alert messages are encrypted and -// compressed, as specified by the current connection state. -// https://tools.ietf.org/html/rfc5246#section-7.2 -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct Alert { - pub(crate) alert_level: AlertLevel, - pub(crate) alert_description: AlertDescription, -} - -impl fmt::Display for Alert { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Alert {}: {}", self.alert_level, self.alert_description) - } -} - -impl Alert { - pub fn content_type(&self) -> ContentType { - ContentType::Alert - } - - pub fn size(&self) -> usize { - 2 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.alert_level as u8)?; - writer.write_u8(self.alert_description as u8)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let alert_level = reader.read_u8()?.into(); - let alert_description = reader.read_u8()?.into(); - - Ok(Alert { - alert_level, - alert_description, - }) - } -} diff --git a/dtls/src/application_data.rs b/dtls/src/application_data.rs deleted file mode 100644 index 4897430dc..000000000 --- a/dtls/src/application_data.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::io::{Read, Write}; - -use super::content::*; -use crate::error::Result; - -// Application data messages are carried by the record layer and are -// fragmented, compressed, and encrypted based on the current connection -// state. The messages are treated as transparent data to the record -// layer. -// https://tools.ietf.org/html/rfc5246#section-10 -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct ApplicationData { - pub data: Vec, -} - -impl ApplicationData { - pub fn content_type(&self) -> ContentType { - ContentType::ApplicationData - } - - pub fn size(&self) -> usize { - self.data.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_all(&self.data)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let mut data: Vec = vec![]; - reader.read_to_end(&mut data)?; - - Ok(ApplicationData { data }) - } -} diff --git a/dtls/src/change_cipher_spec/change_cipher_spec_test.rs b/dtls/src/change_cipher_spec/change_cipher_spec_test.rs deleted file mode 100644 index 868d96df0..000000000 --- a/dtls/src/change_cipher_spec/change_cipher_spec_test.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_change_cipher_spec_round_trip() -> Result<()> { - let c = ChangeCipherSpec {}; - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - - let mut reader = BufReader::new(raw.as_slice()); - let cnew = ChangeCipherSpec::unmarshal(&mut reader)?; - assert_eq!( - c, cnew, - "ChangeCipherSpec round trip: got {cnew:?}, want {c:?}" - ); - - Ok(()) -} - -#[test] -fn test_change_cipher_spec_invalid() -> Result<()> { - let data = vec![0x00]; - - let mut reader = BufReader::new(data.as_slice()); - let result = ChangeCipherSpec::unmarshal(&mut reader); - - match result { - Ok(_) => panic!("must be error"), - Err(err) => assert_eq!(err.to_string(), Error::ErrInvalidCipherSpec.to_string()), - }; - - Ok(()) -} diff --git a/dtls/src/change_cipher_spec/mod.rs b/dtls/src/change_cipher_spec/mod.rs deleted file mode 100644 index 51c08b055..000000000 --- a/dtls/src/change_cipher_spec/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -#[cfg(test)] -mod change_cipher_spec_test; - -use std::io::{Read, Write}; - -use byteorder::{ReadBytesExt, WriteBytesExt}; - -use super::content::*; -use super::error::*; - -// The change cipher spec protocol exists to signal transitions in -// ciphering strategies. The protocol consists of a single message, -// which is encrypted and compressed under the current (not the pending) -// connection state. The message consists of a single byte of value 1. -// https://tools.ietf.org/html/rfc5246#section-7.1 -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct ChangeCipherSpec; - -impl ChangeCipherSpec { - pub fn content_type(&self) -> ContentType { - ContentType::ChangeCipherSpec - } - - pub fn size(&self) -> usize { - 1 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(0x01)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let data = reader.read_u8()?; - if data != 0x01 { - return Err(Error::ErrInvalidCipherSpec); - } - - Ok(ChangeCipherSpec {}) - } -} diff --git a/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs deleted file mode 100644 index a28ee0f29..000000000 --- a/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs +++ /dev/null @@ -1,118 +0,0 @@ -use super::*; -use crate::client_certificate_type::ClientCertificateType; -use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen}; -use crate::prf::*; - -#[derive(Clone)] -pub struct CipherSuiteAes128Ccm { - ccm: Option, - client_certificate_type: ClientCertificateType, - id: CipherSuiteId, - psk: bool, - crypto_ccm_tag_len: CryptoCcmTagLen, -} - -impl CipherSuiteAes128Ccm { - const PRF_MAC_LEN: usize = 0; - const PRF_KEY_LEN: usize = 16; - const PRF_IV_LEN: usize = 4; - - pub fn new( - client_certificate_type: ClientCertificateType, - id: CipherSuiteId, - psk: bool, - crypto_ccm_tag_len: CryptoCcmTagLen, - ) -> Self { - CipherSuiteAes128Ccm { - ccm: None, - client_certificate_type, - id, - psk, - crypto_ccm_tag_len, - } - } -} - -impl CipherSuite for CipherSuiteAes128Ccm { - fn to_string(&self) -> String { - format!("{}", self.id) - } - - fn id(&self) -> CipherSuiteId { - self.id - } - - fn certificate_type(&self) -> ClientCertificateType { - self.client_certificate_type - } - - fn hash_func(&self) -> CipherSuiteHash { - CipherSuiteHash::Sha256 - } - - fn is_psk(&self) -> bool { - self.psk - } - - fn is_initialized(&self) -> bool { - self.ccm.is_some() - } - - fn init( - &mut self, - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - is_client: bool, - ) -> Result<()> { - let keys = prf_encryption_keys( - master_secret, - client_random, - server_random, - CipherSuiteAes128Ccm::PRF_MAC_LEN, - CipherSuiteAes128Ccm::PRF_KEY_LEN, - CipherSuiteAes128Ccm::PRF_IV_LEN, - self.hash_func(), - )?; - - if is_client { - self.ccm = Some(CryptoCcm::new( - &self.crypto_ccm_tag_len, - &keys.client_write_key, - &keys.client_write_iv, - &keys.server_write_key, - &keys.server_write_iv, - )); - } else { - self.ccm = Some(CryptoCcm::new( - &self.crypto_ccm_tag_len, - &keys.server_write_key, - &keys.server_write_iv, - &keys.client_write_key, - &keys.client_write_iv, - )); - } - - Ok(()) - } - - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - if let Some(ccm) = &self.ccm { - ccm.encrypt(pkt_rlh, raw) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to encrypt".to_owned(), - )) - } - } - - fn decrypt(&self, input: &[u8]) -> Result> { - if let Some(ccm) = &self.ccm { - ccm.decrypt(input) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to decrypt".to_owned(), - )) - } - } -} diff --git a/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs deleted file mode 100644 index fe7fd9a26..000000000 --- a/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::*; -use crate::crypto::crypto_gcm::*; -use crate::prf::*; - -#[derive(Clone)] -pub struct CipherSuiteAes128GcmSha256 { - gcm: Option, - rsa: bool, -} - -impl CipherSuiteAes128GcmSha256 { - const PRF_MAC_LEN: usize = 0; - const PRF_KEY_LEN: usize = 16; - const PRF_IV_LEN: usize = 4; - - pub fn new(rsa: bool) -> Self { - CipherSuiteAes128GcmSha256 { gcm: None, rsa } - } -} - -impl CipherSuite for CipherSuiteAes128GcmSha256 { - fn to_string(&self) -> String { - if self.rsa { - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256".to_owned() - } else { - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".to_owned() - } - } - - fn id(&self) -> CipherSuiteId { - if self.rsa { - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 - } else { - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 - } - } - - fn certificate_type(&self) -> ClientCertificateType { - if self.rsa { - ClientCertificateType::RsaSign - } else { - ClientCertificateType::EcdsaSign - } - } - - fn hash_func(&self) -> CipherSuiteHash { - CipherSuiteHash::Sha256 - } - - fn is_psk(&self) -> bool { - false - } - - fn is_initialized(&self) -> bool { - self.gcm.is_some() - } - - fn init( - &mut self, - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - is_client: bool, - ) -> Result<()> { - let keys = prf_encryption_keys( - master_secret, - client_random, - server_random, - CipherSuiteAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteAes128GcmSha256::PRF_IV_LEN, - self.hash_func(), - )?; - - if is_client { - self.gcm = Some(CryptoGcm::new( - &keys.client_write_key, - &keys.client_write_iv, - &keys.server_write_key, - &keys.server_write_iv, - )); - } else { - self.gcm = Some(CryptoGcm::new( - &keys.server_write_key, - &keys.server_write_iv, - &keys.client_write_key, - &keys.client_write_iv, - )); - } - - Ok(()) - } - - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - if let Some(cg) = &self.gcm { - cg.encrypt(pkt_rlh, raw) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to encrypt".to_owned(), - )) - } - } - - fn decrypt(&self, input: &[u8]) -> Result> { - if let Some(cg) = &self.gcm { - cg.decrypt(input) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to decrypt".to_owned(), - )) - } - } -} diff --git a/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs deleted file mode 100644 index e358279b7..000000000 --- a/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::*; -use crate::crypto::crypto_cbc::*; -use crate::prf::*; - -#[derive(Clone)] -pub struct CipherSuiteAes256CbcSha { - cbc: Option, - rsa: bool, -} - -impl CipherSuiteAes256CbcSha { - const PRF_MAC_LEN: usize = 20; - const PRF_KEY_LEN: usize = 32; - const PRF_IV_LEN: usize = 16; - - pub fn new(rsa: bool) -> Self { - CipherSuiteAes256CbcSha { cbc: None, rsa } - } -} - -impl CipherSuite for CipherSuiteAes256CbcSha { - fn to_string(&self) -> String { - if self.rsa { - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA".to_owned() - } else { - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA".to_owned() - } - } - - fn id(&self) -> CipherSuiteId { - if self.rsa { - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha - } else { - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha - } - } - - fn certificate_type(&self) -> ClientCertificateType { - if self.rsa { - ClientCertificateType::RsaSign - } else { - ClientCertificateType::EcdsaSign - } - } - - fn hash_func(&self) -> CipherSuiteHash { - CipherSuiteHash::Sha256 - } - - fn is_psk(&self) -> bool { - false - } - - fn is_initialized(&self) -> bool { - self.cbc.is_some() - } - - fn init( - &mut self, - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - is_client: bool, - ) -> Result<()> { - let keys = prf_encryption_keys( - master_secret, - client_random, - server_random, - CipherSuiteAes256CbcSha::PRF_MAC_LEN, - CipherSuiteAes256CbcSha::PRF_KEY_LEN, - CipherSuiteAes256CbcSha::PRF_IV_LEN, - self.hash_func(), - )?; - - if is_client { - self.cbc = Some(CryptoCbc::new( - &keys.client_write_key, - &keys.client_mac_key, - &keys.server_write_key, - &keys.server_mac_key, - )?); - } else { - self.cbc = Some(CryptoCbc::new( - &keys.server_write_key, - &keys.server_mac_key, - &keys.client_write_key, - &keys.client_mac_key, - )?); - } - - Ok(()) - } - - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - if let Some(cg) = &self.cbc { - cg.encrypt(pkt_rlh, raw) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to encrypt".to_owned(), - )) - } - } - - fn decrypt(&self, input: &[u8]) -> Result> { - if let Some(cg) = &self.cbc { - cg.decrypt(input) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to decrypt".to_owned(), - )) - } - } -} diff --git a/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs b/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs deleted file mode 100644 index c5bf2dfd1..000000000 --- a/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; -use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; -use crate::crypto::crypto_ccm::CryptoCcmTagLen; - -pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm { - CipherSuiteAes128Ccm::new( - ClientCertificateType::EcdsaSign, - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, - false, - CryptoCcmTagLen::CryptoCcmTagLength, - ) -} diff --git a/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs b/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs deleted file mode 100644 index aa9a92ce0..000000000 --- a/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; -use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; -use crate::crypto::crypto_ccm::CryptoCcmTagLen; - -pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { - CipherSuiteAes128Ccm::new( - ClientCertificateType::EcdsaSign, - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, - false, - CryptoCcmTagLen::CryptoCcm8TagLength, - ) -} diff --git a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs b/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs deleted file mode 100644 index 6f506e0ef..000000000 --- a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; -use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; -use crate::crypto::crypto_ccm::CryptoCcmTagLen; - -pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm { - CipherSuiteAes128Ccm::new( - ClientCertificateType::Unsupported, - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, - true, - CryptoCcmTagLen::CryptoCcmTagLength, - ) -} diff --git a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs b/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs deleted file mode 100644 index 64b9f1a50..000000000 --- a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; -use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; -use crate::crypto::crypto_ccm::CryptoCcmTagLen; - -pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { - CipherSuiteAes128Ccm::new( - ClientCertificateType::Unsupported, - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, - true, - CryptoCcmTagLen::CryptoCcm8TagLength, - ) -} diff --git a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs deleted file mode 100644 index 2204ace29..000000000 --- a/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::*; -use crate::crypto::crypto_gcm::*; -use crate::prf::*; - -#[derive(Clone, Default)] -pub struct CipherSuiteTlsPskWithAes128GcmSha256 { - gcm: Option, -} - -impl CipherSuiteTlsPskWithAes128GcmSha256 { - const PRF_MAC_LEN: usize = 0; - const PRF_KEY_LEN: usize = 16; - const PRF_IV_LEN: usize = 4; -} - -impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 { - fn to_string(&self) -> String { - "TLS_PSK_WITH_AES_128_GCM_SHA256".to_owned() - } - - fn id(&self) -> CipherSuiteId { - CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 - } - - fn certificate_type(&self) -> ClientCertificateType { - ClientCertificateType::Unsupported - } - - fn hash_func(&self) -> CipherSuiteHash { - CipherSuiteHash::Sha256 - } - - fn is_psk(&self) -> bool { - true - } - - fn is_initialized(&self) -> bool { - self.gcm.is_some() - } - - fn init( - &mut self, - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - is_client: bool, - ) -> Result<()> { - let keys = prf_encryption_keys( - master_secret, - client_random, - server_random, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, - self.hash_func(), - )?; - - if is_client { - self.gcm = Some(CryptoGcm::new( - &keys.client_write_key, - &keys.client_write_iv, - &keys.server_write_key, - &keys.server_write_iv, - )); - } else { - self.gcm = Some(CryptoGcm::new( - &keys.server_write_key, - &keys.server_write_iv, - &keys.client_write_key, - &keys.client_write_iv, - )); - } - - Ok(()) - } - - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - if let Some(cg) = &self.gcm { - cg.encrypt(pkt_rlh, raw) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to encrypt".to_owned(), - )) - } - } - - fn decrypt(&self, input: &[u8]) -> Result> { - if let Some(cg) = &self.gcm { - cg.decrypt(input) - } else { - Err(Error::Other( - "CipherSuite has not been initialized, unable to decrypt".to_owned(), - )) - } - } -} diff --git a/dtls/src/cipher_suite/mod.rs b/dtls/src/cipher_suite/mod.rs deleted file mode 100644 index 195492c21..000000000 --- a/dtls/src/cipher_suite/mod.rs +++ /dev/null @@ -1,227 +0,0 @@ -pub mod cipher_suite_aes_128_ccm; -pub mod cipher_suite_aes_128_gcm_sha256; -pub mod cipher_suite_aes_256_cbc_sha; -pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm; -pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8; -pub mod cipher_suite_tls_psk_with_aes_128_ccm; -pub mod cipher_suite_tls_psk_with_aes_128_ccm8; -pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256; - -use std::fmt; -use std::marker::{Send, Sync}; - -use cipher_suite_aes_128_gcm_sha256::*; -use cipher_suite_aes_256_cbc_sha::*; -use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm::*; -use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8::*; -use cipher_suite_tls_psk_with_aes_128_ccm::*; -use cipher_suite_tls_psk_with_aes_128_ccm8::*; -use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*; - -use super::client_certificate_type::*; -use super::error::*; -use super::record_layer::record_layer_header::*; - -// CipherSuiteID is an ID for our supported CipherSuites -// Supported Cipher Suites -#[allow(non_camel_case_types)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum CipherSuiteId { - // AES-128-CCM - Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac, - Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae, - - // AES-128-GCM-SHA256 - Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b, - Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f, - - // AES-256-CBC-SHA - Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a, - Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014, - - Tls_Psk_With_Aes_128_Ccm = 0xc0a4, - Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8, - Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8, - - Unsupported, -} - -impl fmt::Display for CipherSuiteId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { - write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM") - } - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => { - write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8") - } - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => { - write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") - } - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => { - write!(f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") - } - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => { - write!(f, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") - } - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { - write!(f, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") - } - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => write!(f, "TLS_PSK_WITH_AES_128_CCM"), - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => write!(f, "TLS_PSK_WITH_AES_128_CCM_8"), - CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => { - write!(f, "TLS_PSK_WITH_AES_128_GCM_SHA256") - } - _ => write!(f, "Unsupported CipherSuiteID"), - } - } -} - -impl From for CipherSuiteId { - fn from(val: u16) -> Self { - match val { - // AES-128-CCM - 0xc0ac => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, - 0xc0ae => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, - - // AES-128-GCM-SHA256 - 0xc02b => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - 0xc02f => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256, - - // AES-256-CBC-SHA - 0xc00a => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, - 0xc014 => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha, - - 0xc0a4 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, - 0xc0a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, - 0x00a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256, - - _ => CipherSuiteId::Unsupported, - } - } -} - -#[derive(Copy, Clone, Debug)] -pub enum CipherSuiteHash { - Sha256, -} - -impl CipherSuiteHash { - pub(crate) fn size(&self) -> usize { - match *self { - CipherSuiteHash::Sha256 => 32, - } - } -} - -pub trait CipherSuite { - fn to_string(&self) -> String; - fn id(&self) -> CipherSuiteId; - fn certificate_type(&self) -> ClientCertificateType; - fn hash_func(&self) -> CipherSuiteHash; - fn is_psk(&self) -> bool; - fn is_initialized(&self) -> bool; - - // Generate the internal encryption state - fn init( - &mut self, - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - is_client: bool, - ) -> Result<()>; - - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; - fn decrypt(&self, input: &[u8]) -> Result>; -} - -// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml -// A cipher_suite is a specific combination of key agreement, cipher and MAC -// function. -pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result> { - match id { - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { - Ok(Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm())) - } - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => Ok(Box::new( - new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8(), - )), - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => { - Ok(Box::new(CipherSuiteAes128GcmSha256::new(false))) - } - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => { - Ok(Box::new(CipherSuiteAes128GcmSha256::new(true))) - } - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { - Ok(Box::new(CipherSuiteAes256CbcSha::new(true))) - } - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => { - Ok(Box::new(CipherSuiteAes256CbcSha::new(false))) - } - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => { - Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm())) - } - CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => { - Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8())) - } - CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => { - Ok(Box::::default()) - } - _ => Err(Error::ErrInvalidCipherSuite), - } -} - -// CipherSuites we support in order of preference -pub(crate) fn default_cipher_suites() -> Vec> { - vec![ - Box::new(CipherSuiteAes128GcmSha256::new(false)), - Box::new(CipherSuiteAes256CbcSha::new(false)), - Box::new(CipherSuiteAes128GcmSha256::new(true)), - Box::new(CipherSuiteAes256CbcSha::new(true)), - ] -} - -fn all_cipher_suites() -> Vec> { - vec![ - Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()), - Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8()), - Box::new(CipherSuiteAes128GcmSha256::new(false)), - Box::new(CipherSuiteAes128GcmSha256::new(true)), - Box::new(CipherSuiteAes256CbcSha::new(false)), - Box::new(CipherSuiteAes256CbcSha::new(true)), - Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()), - Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()), - Box::::default(), - ] -} - -fn cipher_suites_for_ids(ids: &[CipherSuiteId]) -> Result>> { - let mut cipher_suites = vec![]; - for id in ids { - cipher_suites.push(cipher_suite_for_id(*id)?); - } - Ok(cipher_suites) -} - -pub(crate) fn parse_cipher_suites( - user_selected_suites: &[CipherSuiteId], - exclude_psk: bool, - exclude_non_psk: bool, -) -> Result>> { - let cipher_suites = if !user_selected_suites.is_empty() { - cipher_suites_for_ids(user_selected_suites)? - } else { - default_cipher_suites() - }; - - let filtered_cipher_suites: Vec> = cipher_suites - .into_iter() - .filter(|c| !((exclude_psk && c.is_psk()) || (exclude_non_psk && !c.is_psk()))) - .collect(); - - if filtered_cipher_suites.is_empty() { - Err(Error::ErrNoAvailableCipherSuites) - } else { - Ok(filtered_cipher_suites) - } -} diff --git a/dtls/src/client_certificate_type.rs b/dtls/src/client_certificate_type.rs deleted file mode 100644 index d87aacb6c..000000000 --- a/dtls/src/client_certificate_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ClientCertificateType { - RsaSign = 1, - EcdsaSign = 64, - Unsupported, -} - -impl From for ClientCertificateType { - fn from(val: u8) -> Self { - match val { - 1 => ClientCertificateType::RsaSign, - 64 => ClientCertificateType::EcdsaSign, - _ => ClientCertificateType::Unsupported, - } - } -} diff --git a/dtls/src/compression_methods.rs b/dtls/src/compression_methods.rs deleted file mode 100644 index a03195ec7..000000000 --- a/dtls/src/compression_methods.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::io::{Read, Write}; - -use byteorder::{ReadBytesExt, WriteBytesExt}; - -use crate::error::Result; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum CompressionMethodId { - Null = 0, - Unsupported, -} - -impl From for CompressionMethodId { - fn from(val: u8) -> Self { - match val { - 0 => CompressionMethodId::Null, - _ => CompressionMethodId::Unsupported, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CompressionMethods { - pub ids: Vec, -} - -impl CompressionMethods { - pub fn size(&self) -> usize { - 1 + self.ids.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.ids.len() as u8)?; - - for id in &self.ids { - writer.write_u8(*id as u8)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let compression_methods_count = reader.read_u8()? as usize; - let mut ids = vec![]; - for _ in 0..compression_methods_count { - let id = reader.read_u8()?.into(); - if id != CompressionMethodId::Unsupported { - ids.push(id); - } - } - - Ok(CompressionMethods { ids }) - } -} - -pub fn default_compression_methods() -> CompressionMethods { - CompressionMethods { - ids: vec![CompressionMethodId::Null], - } -} diff --git a/dtls/src/config.rs b/dtls/src/config.rs deleted file mode 100644 index 0e1e23c6f..000000000 --- a/dtls/src/config.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::sync::Arc; - -use tokio::time::Duration; - -use crate::cipher_suite::*; -use crate::crypto::*; -use crate::error::*; -use crate::extension::extension_use_srtp::SrtpProtectionProfile; -use crate::handshaker::VerifyPeerCertificateFn; -use crate::signature_hash_algorithm::SignatureScheme; - -/// Config is used to configure a DTLS client or server. -/// After a Config is passed to a DTLS function it must not be modified. -#[derive(Clone)] -pub struct Config { - /// certificates contains certificate chain to present to the other side of the connection. - /// Server MUST set this if psk is non-nil - /// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil - pub certificates: Vec, - - /// cipher_suites is a list of supported cipher suites. - /// If cipher_suites is nil, a default list is used - pub cipher_suites: Vec, - - /// signature_schemes contains the signature and hash schemes that the peer requests to verify. - pub signature_schemes: Vec, - - /// srtp_protection_profiles are the supported protection profiles - /// Clients will send this via use_srtp and assert that the server properly responds - /// Servers will assert that clients send one of these profiles and will respond as needed - pub srtp_protection_profiles: Vec, - - /// client_auth determines the server's policy for - /// TLS Client Authentication. The default is NoClientCert. - pub client_auth: ClientAuthType, - - /// extended_master_secret determines if the "Extended Master Secret" extension - /// should be disabled, requested, or required (default requested). - pub extended_master_secret: ExtendedMasterSecretType, - - /// flight_interval controls how often we send outbound handshake messages - /// defaults to time.Second - pub flight_interval: Duration, - - /// psk sets the pre-shared key used by this DTLS connection - /// If psk is non-nil only psk cipher_suites will be used - pub psk: Option, - pub psk_identity_hint: Option>, - - /// insecure_skip_verify controls whether a client verifies the - /// server's certificate chain and host name. - /// If insecure_skip_verify is true, TLS accepts any certificate - /// presented by the server and any host name in that certificate. - /// In this mode, TLS is susceptible to man-in-the-middle attacks. - /// This should be used only for testing. - pub insecure_skip_verify: bool, - - /// insecure_hashes allows the use of hashing algorithms that are known - /// to be vulnerable. - pub insecure_hashes: bool, - - /// insecure_verification allows the use of verification algorithms that are - /// known to be vulnerable or deprecated - pub insecure_verification: bool, - /// VerifyPeerCertificate, if not nil, is called after normal - /// certificate verification by either a client or server. It - /// receives the certificate provided by the peer and also a flag - /// that tells if normal verification has succeeded. If it returns a - /// non-nil error, the handshake is aborted and that error results. - /// - /// If normal verification fails then the handshake will abort before - /// considering this callback. If normal verification is disabled by - /// setting insecure_skip_verify, or (for a server) when client_auth is - /// RequestClientCert or RequireAnyClientCert, then this callback will - /// be considered but the verifiedChains will always be nil. - pub verify_peer_certificate: Option, - - /// roots_cas defines the set of root certificate authorities - /// that one peer uses when verifying the other peer's certificates. - /// If RootCAs is nil, TLS uses the host's root CA set. - /// Used by Client to verify server's certificate - pub roots_cas: rustls::RootCertStore, - - /// client_cas defines the set of root certificate authorities - /// that servers use if required to verify a client certificate - /// by the policy in client_auth. - /// Used by Server to verify client's certificate - pub client_cas: rustls::RootCertStore, - - /// server_name is used to verify the hostname on the returned - /// certificates unless insecure_skip_verify is given. - pub server_name: String, - - /// mtu is the length at which handshake messages will be fragmented to - /// fit within the maximum transmission unit (default is 1200 bytes) - pub mtu: usize, - - /// replay_protection_window is the size of the replay attack protection window. - /// Duplication of the sequence number is checked in this window size. - /// Packet with sequence number older than this value compared to the latest - /// accepted packet will be discarded. (default is 64) - pub replay_protection_window: usize, -} - -impl Default for Config { - fn default() -> Self { - Config { - certificates: vec![], - cipher_suites: vec![], - signature_schemes: vec![], - srtp_protection_profiles: vec![], - client_auth: ClientAuthType::default(), - extended_master_secret: ExtendedMasterSecretType::default(), - flight_interval: Duration::default(), - psk: None, - psk_identity_hint: None, - insecure_skip_verify: false, - insecure_hashes: false, - insecure_verification: false, - verify_peer_certificate: None, - roots_cas: rustls::RootCertStore::empty(), - client_cas: rustls::RootCertStore::empty(), - server_name: String::default(), - mtu: 0, - replay_protection_window: 0, - } - } -} - -pub(crate) const DEFAULT_MTU: usize = 1200; // bytes - -// PSKCallback is called once we have the remote's psk_identity_hint. -// If the remote provided none it will be nil -pub(crate) type PskCallback = Arc Result>) + Send + Sync>; - -// ClientAuthType declares the policy the server will follow for -// TLS Client Authentication. -#[derive(Default, Copy, Clone, PartialEq, Eq)] -pub enum ClientAuthType { - #[default] - NoClientCert = 0, - RequestClientCert = 1, - RequireAnyClientCert = 2, - VerifyClientCertIfGiven = 3, - RequireAndVerifyClientCert = 4, -} - -// ExtendedMasterSecretType declares the policy the client and server -// will follow for the Extended Master Secret extension -#[derive(Default, PartialEq, Eq, Copy, Clone)] -pub enum ExtendedMasterSecretType { - #[default] - Request = 0, - Require = 1, - Disable = 2, -} - -pub(crate) fn validate_config(is_client: bool, config: &Config) -> Result<()> { - if is_client && config.psk.is_some() && config.psk_identity_hint.is_none() { - return Err(Error::ErrPskAndIdentityMustBeSetForClient); - } - - if !is_client && config.psk.is_none() && config.certificates.is_empty() { - return Err(Error::ErrServerMustHaveCertificate); - } - - if !config.certificates.is_empty() && config.psk.is_some() { - return Err(Error::ErrPskAndCertificate); - } - - if config.psk_identity_hint.is_some() && config.psk.is_none() { - return Err(Error::ErrIdentityNoPsk); - } - - for cert in &config.certificates { - match cert.private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => {} - CryptoPrivateKeyKind::Ecdsa256(_) => {} - _ => return Err(Error::ErrInvalidPrivateKey), - } - } - - parse_cipher_suites( - &config.cipher_suites, - config.psk.is_none(), - config.psk.is_some(), - )?; - - Ok(()) -} diff --git a/dtls/src/conn/conn_test.rs b/dtls/src/conn/conn_test.rs deleted file mode 100644 index 926b058f9..000000000 --- a/dtls/src/conn/conn_test.rs +++ /dev/null @@ -1,2458 +0,0 @@ -use std::time::SystemTime; - -use rand::Rng; -use rustls::pki_types::CertificateDer; -use util::conn::conn_pipe::*; -use util::KeyingMaterialExporter; - -use super::*; -use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::*; -use crate::cipher_suite::*; -use crate::compression_methods::*; -use crate::crypto::*; -use crate::curve::*; -use crate::error::*; -use crate::extension::extension_supported_elliptic_curves::*; -use crate::extension::extension_supported_point_formats::*; -use crate::extension::extension_supported_signature_algorithms::*; -use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; -use crate::extension::*; -use crate::handshake::handshake_message_certificate::*; -use crate::handshake::handshake_message_client_hello::*; -use crate::handshake::handshake_message_hello_verify_request::*; -use crate::handshake::handshake_message_server_hello::*; -use crate::handshake::handshake_message_server_hello_done::*; -use crate::handshake::handshake_message_server_key_exchange::*; -use crate::handshake::handshake_random::*; -use crate::signature_hash_algorithm::*; - -const ERR_TEST_PSK_INVALID_IDENTITY: &str = "TestPSK: Server got invalid identity"; -const ERR_PSK_REJECTED: &str = "PSK Rejected"; -const ERR_NOT_EXPECTED_CHAIN: &str = "not expected chain"; -const ERR_EXPECTED_CHAIN: &str = "expected chain"; -const ERR_WRONG_CERT: &str = "wrong cert"; - -async fn build_pipe() -> Result<(DTLSConn, DTLSConn)> { - let (ua, ub) = pipe(); - - pipe_conn(Arc::new(ua), Arc::new(ub)).await -} - -async fn pipe_conn( - ca: Arc, - cb: Arc, -) -> Result<(DTLSConn, DTLSConn)> { - let (c_tx, mut c_rx) = mpsc::channel(1); - - // Setup client - tokio::spawn(async move { - let client = create_test_client( - ca, - Config { - srtp_protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - ..Default::default() - }, - true, - ) - .await; - - let _ = c_tx.send(client).await; - }); - - // Setup server - let sever = create_test_server( - cb, - Config { - srtp_protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - ..Default::default() - }, - true, - ) - .await?; - - // Receive client - let client = match c_rx.recv().await.unwrap() { - Ok(client) => client, - Err(err) => return Err(err), - }; - - Ok((client, sever)) -} - -fn psk_callback_client(hint: &[u8]) -> Result> { - trace!( - "Server's hint: {}", - String::from_utf8(hint.to_vec()).unwrap() - ); - Ok(vec![0xAB, 0xC1, 0x23]) -} - -fn psk_callback_server(hint: &[u8]) -> Result> { - trace!( - "Client's hint: {}", - String::from_utf8(hint.to_vec()).unwrap() - ); - Ok(vec![0xAB, 0xC1, 0x23]) -} - -fn psk_callback_hint_fail(_hint: &[u8]) -> Result> { - Err(Error::Other(ERR_PSK_REJECTED.to_owned())) -} - -async fn create_test_client( - ca: Arc, - mut cfg: Config, - generate_certificate: bool, -) -> Result { - if generate_certificate { - let client_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - cfg.certificates = vec![client_cert]; - } - - cfg.insecure_skip_verify = true; - DTLSConn::new(ca, cfg, true, None).await -} - -async fn create_test_server( - cb: Arc, - mut cfg: Config, - generate_certificate: bool, -) -> Result { - if generate_certificate { - let server_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - cfg.certificates = vec![server_cert]; - } - - DTLSConn::new(cb, cfg, false, None).await -} - -#[tokio::test] -async fn test_routine_leak_on_close() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (ca, cb) = build_pipe().await?; - - let buf_a = vec![0xFA; 100]; - let n_a = ca.write(&buf_a, Some(Duration::from_secs(5))).await?; - assert_eq!(n_a, 100); - - let mut buf_b = vec![0; 1024]; - let n_b = cb.read(&mut buf_b, Some(Duration::from_secs(5))).await?; - assert_eq!(n_a, 100); - assert_eq!(&buf_a[..], &buf_b[0..n_b]); - - cb.close().await?; - ca.close().await?; - - { - drop(ca); - drop(cb); - } - - tokio::time::sleep(Duration::from_millis(1)).await; - - Ok(()) -} - -#[tokio::test] -async fn test_sequence_number_overflow_on_application_data() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (ca, cb) = build_pipe().await?; - - { - let mut lsn = ca.state.local_sequence_number.lock().await; - lsn[1] = MAX_SEQUENCE_NUMBER; - } - - let buf_a = vec![0xFA; 100]; - let n_a = ca.write(&buf_a, Some(Duration::from_secs(5))).await?; - assert_eq!(n_a, 100); - - let mut buf_b = vec![0; 1024]; - let n_b = cb.read(&mut buf_b, Some(Duration::from_secs(5))).await?; - assert_eq!(n_a, 100); - assert_eq!(&buf_a[..], &buf_b[0..n_b]); - - let result = ca.write(&buf_a, Some(Duration::from_secs(5))).await; - if let Err(err) = result { - assert_eq!( - err.to_string(), - Error::ErrSequenceNumberOverflow.to_string() - ); - } else { - panic!("Expected error but it is OK"); - } - - cb.close().await?; - - if let Err(err) = ca.close().await { - assert_eq!( - err.to_string(), - Error::ErrSequenceNumberOverflow.to_string() - ); - } else { - panic!("Expected error but it is OK"); - } - - { - drop(ca); - drop(cb); - } - - tokio::time::sleep(Duration::from_millis(1)).await; - - Ok(()) -} - -#[tokio::test] -async fn test_sequence_number_overflow_on_handshake() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (ca, cb) = build_pipe().await?; - - { - let mut lsn = ca.state.local_sequence_number.lock().await; - lsn[0] = MAX_SEQUENCE_NUMBER + 1; - } - - // Try to send handshake packet. - if let Err(err) = ca - .write_packets(vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - random: HandshakeRandom::default(), - cookie: vec![0; 64], - - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256], - compression_methods: default_compression_methods(), - extensions: vec![], - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]) - .await - { - assert_eq!( - err.to_string(), - Error::ErrSequenceNumberOverflow.to_string() - ); - } else { - panic!("Expected error but it is OK"); - } - - cb.close().await?; - ca.close().await?; - - { - drop(ca); - drop(cb); - } - - tokio::time::sleep(Duration::from_millis(1)).await; - - Ok(()) -} - -#[tokio::test] -async fn test_handshake_with_alert() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let cases = vec![ - ( - "CipherSuiteNoIntersection", - Config { - // Server - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - ..Default::default() - }, - Config { - // Client - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256], - ..Default::default() - }, - Error::ErrCipherSuiteNoIntersection, - Error::ErrAlertFatalOrClose, //errClient: &errAlert{&alert{alertLevelFatal, alertInsufficientSecurity}}, - ), - ( - "SignatureSchemesNoIntersection", - Config { - // Server - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - signature_schemes: vec![SignatureScheme::EcdsaWithP256AndSha256], - ..Default::default() - }, - Config { - // Client - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - signature_schemes: vec![SignatureScheme::EcdsaWithP521AndSha512], - ..Default::default() - }, - Error::ErrAlertFatalOrClose, //errServer: &errAlert{&alert{alertLevelFatal, alertInsufficientSecurity}}, - Error::ErrNoAvailableSignatureSchemes, //NoAvailableSignatureSchemes, - ), - ]; - - for (name, config_server, config_client, err_server, err_client) in cases { - let (client_err_tx, mut client_err_rx) = mpsc::channel(1); - - let (ca, cb) = pipe(); - tokio::spawn(async move { - let result = create_test_client(Arc::new(ca), config_client, true).await; - let _ = client_err_tx.send(result).await; - }); - - let result_server = create_test_server(Arc::new(cb), config_server, true).await; - if let Err(err) = result_server { - assert_eq!( - err.to_string(), - err_server.to_string(), - "{name} Server error exp({err_server}) failed({err})" - ); - } else { - panic!("{name} expected error but create_test_server return OK"); - } - - let result_client = client_err_rx.recv().await; - if let Some(result_client) = result_client { - if let Err(err) = result_client { - assert_eq!( - err.to_string(), - err_client.to_string(), - "{name} Client error exp({err_client}) failed({err})" - ); - } else { - panic!("{name} expected error but create_test_client return OK"); - } - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_export_keying_material() -> Result<()> { - let export_label = "EXTRACTOR-dtls_srtp"; - let expected_server_key = vec![0x61, 0x09, 0x9d, 0x7d, 0xcb, 0x08, 0x52, 0x2c, 0xe7, 0x7b]; - let expected_client_key = vec![0x87, 0xf0, 0x40, 0x02, 0xf6, 0x1c, 0xf1, 0xfe, 0x8c, 0x77]; - - let (_decrypted_tx, decrypted_rx) = mpsc::channel(1); - let (_handshake_tx, handshake_rx) = mpsc::channel(1); - let (packet_tx, _packet_rx) = mpsc::channel(1); - let (handle_queue_tx, _handle_queue_rx) = mpsc::channel(1); - let (ca, _cb) = pipe(); - - let mut c = DTLSConn { - conn: Arc::new(ca), - state: State { - local_random: HandshakeRandom { - gmt_unix_time: SystemTime::UNIX_EPOCH - .checked_add(Duration::new(500, 0)) - .unwrap(), - ..Default::default() - }, - remote_random: HandshakeRandom { - gmt_unix_time: SystemTime::UNIX_EPOCH - .checked_add(Duration::new(1000, 0)) - .unwrap(), - ..Default::default() - }, - local_sequence_number: Arc::new(Mutex::new(vec![0, 0])), - cipher_suite: Arc::new(Mutex::new(Some(Box::new(CipherSuiteAes128GcmSha256::new( - false, - ))))), - ..Default::default() - }, - cache: HandshakeCache::new(), - decrypted_rx: Mutex::new(decrypted_rx), - handshake_completed_successfully: Arc::new(AtomicBool::new(false)), - connection_closed_by_user: false, - closed: AtomicBool::new(false), - current_flight: Box::new(Flight0 {}) as Box, - flights: None, - cfg: HandshakeConfig::default(), - retransmit: false, - handshake_rx, - - packet_tx: Arc::new(packet_tx), - handle_queue_tx, - handshake_done_tx: None, - - reader_close_tx: Mutex::new(None), - }; - - c.set_local_epoch(0); - let state = c.connection_state().await; - if let Err(err) = state.export_keying_material(export_label, &[], 0).await { - assert!( - err.to_string() - .contains(&Error::ErrHandshakeInProgress.to_string()), - "ExportKeyingMaterial when epoch == 0: expected '{}' actual '{}'", - Error::ErrHandshakeInProgress, - err, - ); - } else { - panic!("expect error but export_keying_material returns OK"); - } - - c.set_local_epoch(1); - let state = c.connection_state().await; - if let Err(err) = state.export_keying_material(export_label, &[0x00], 0).await { - assert!( - err.to_string() - .contains(&Error::ErrContextUnsupported.to_string()), - "ExportKeyingMaterial with context: expected '{}' actual '{}'", - Error::ErrContextUnsupported, - err - ); - } else { - panic!("expect error but export_keying_material returns OK"); - } - - for k in INVALID_KEYING_LABELS.iter() { - let state = c.connection_state().await; - if let Err(err) = state.export_keying_material(k, &[], 0).await { - assert!( - err.to_string() - .contains(&Error::ErrReservedExportKeyingMaterial.to_string()), - "ExportKeyingMaterial reserved label: expected '{}' actual '{}'", - Error::ErrReservedExportKeyingMaterial, - err, - ); - } else { - panic!("expect error but export_keying_material returns OK"); - } - } - - let state = c.connection_state().await; - let keying_material = state.export_keying_material(export_label, &[], 10).await?; - assert_eq!( - &keying_material, &expected_server_key, - "ExportKeyingMaterial client export: expected ({:?}) actual ({:?})", - &expected_server_key, &keying_material, - ); - - c.state.is_client = true; - let state = c.connection_state().await; - let keying_material = state.export_keying_material(export_label, &[], 10).await?; - assert_eq!( - &keying_material, &expected_client_key, - "ExportKeyingMaterial client export: expected ({:?}) actual ({:?})", - &expected_client_key, &keying_material, - ); - - Ok(()) -} - -#[tokio::test] -async fn test_psk() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let tests = vec![ - ( - "Server identity specified", - Some("Test Identity".as_bytes().to_vec()), - ), - ("Server identity nil", None), - ]; - - for (name, server_identity) in tests { - let client_identity = "Client Identity".as_bytes(); - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - - let (ca, cb) = pipe(); - tokio::spawn(async move { - let conf = Config { - psk: Some(Arc::new(psk_callback_client)), - psk_identity_hint: Some(client_identity.to_vec()), - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - ..Default::default() - }; - - let result = create_test_client(Arc::new(ca), conf, false).await; - let _ = client_res_tx.send(result).await; - }); - - let config = Config { - psk: Some(Arc::new(psk_callback_server)), - psk_identity_hint: server_identity, - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - ..Default::default() - }; - - let server = create_test_server(Arc::new(cb), config, false).await?; - - let actual_psk_identity_hint = &server.connection_state().await.identity_hint; - assert_eq!( - actual_psk_identity_hint, client_identity, - "TestPSK: Server ClientPSKIdentity Mismatch '{name}': expected({client_identity:?}) actual({actual_psk_identity_hint:?})", - ); - - if let Some(result) = client_res_rx.recv().await { - if let Ok(client) = result { - client.close().await?; - } else { - panic!("{name}: Expected create_test_client successfully, but got error",); - } - } - - let _ = server.close().await; - } - - Ok(()) -} - -#[tokio::test] -async fn test_psk_hint_fail() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - - let (ca, cb) = pipe(); - tokio::spawn(async move { - let conf = Config { - psk: Some(Arc::new(psk_callback_hint_fail)), - psk_identity_hint: Some(vec![]), - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - ..Default::default() - }; - - let result = create_test_client(Arc::new(ca), conf, false).await; - let _ = client_res_tx.send(result).await; - }); - - let config = Config { - psk: Some(Arc::new(psk_callback_hint_fail)), - psk_identity_hint: Some(vec![]), - cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], - ..Default::default() - }; - - if let Err(server_err) = create_test_server(Arc::new(cb), config, false).await { - assert_eq!( - server_err.to_string(), - Error::ErrAlertFatalOrClose.to_string(), - "TestPSK: Server error exp({}) failed({})", - Error::ErrAlertFatalOrClose, - server_err, - ); - } else { - panic!("Expected server error, but got OK"); - } - - let result = client_res_rx.recv().await; - if let Some(client) = result { - if let Err(client_err) = client { - assert!( - client_err.to_string().contains(ERR_PSK_REJECTED), - "TestPSK: Client error exp({ERR_PSK_REJECTED}) failed({client_err})", - ); - } else { - panic!("Expected client error, but got OK"); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_client_timeout() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - - let (ca, _cb) = pipe(); - tokio::spawn(async move { - let conf = Config::default(); - let result = tokio::time::timeout( - Duration::from_millis(100), - create_test_client(Arc::new(ca), conf, true), - ) - .await; - let _ = client_res_tx.send(result).await; - }); - - // no server! - let result = client_res_rx.recv().await; - if let Some(client_timeout_result) = result { - assert!(client_timeout_result.is_err(), "Expected Error but got Ok"); - } - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_srtp_configuration() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - #[allow(clippy::type_complexity)] - let tests: Vec<( - &str, - Vec, - Vec, - SrtpProtectionProfile, - Option, - Option, - )> = vec![ - ( - "No SRTP in use", - vec![], - vec![], - SrtpProtectionProfile::Unsupported, - None, - None, - ), - ( - "SRTP both ends", - vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - None, - None, - ), - ( - "SRTP client only", - vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - vec![], - SrtpProtectionProfile::Unsupported, - Some(Error::ErrAlertFatalOrClose), - Some(Error::ErrServerNoMatchingSrtpProfile), - ), - ( - "SRTP server only", - vec![], - vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - SrtpProtectionProfile::Unsupported, - None, - None, - ), - ( - "Multiple Suites", - vec![ - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, - ], - vec![ - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, - ], - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - None, - None, - ), - ( - "Multiple Suites, Client Chooses", - vec![ - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, - ], - vec![ - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - ], - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - None, - None, - ), - ]; - - for (name, client_srtp, server_srtp, expected_profile, want_client_err, want_server_err) in - tests - { - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - tokio::spawn(async move { - let conf = Config { - srtp_protection_profiles: client_srtp, - ..Default::default() - }; - - let result = create_test_client(Arc::new(ca), conf, true).await; - let _ = client_res_tx.send(result).await; - }); - - let config = Config { - srtp_protection_profiles: server_srtp, - ..Default::default() - }; - - let result = create_test_server(Arc::new(cb), config, true).await; - if let Some(expected_err) = want_server_err { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "{name} TestPSK: Server error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else { - match result { - Ok(server) => { - let actual_server_srtp = server.selected_srtpprotection_profile(); - assert_eq!(actual_server_srtp, expected_profile, - "test_srtp_configuration: Server SRTPProtectionProfile Mismatch '{name}': expected({expected_profile:?}) actual({actual_server_srtp:?})"); - } - Err(err) => { - panic!("{name} expected no error: {err}"); - } - }; - } - - let client_result = client_res_rx.recv().await; - if let Some(result) = client_result { - if let Some(expected_err) = want_client_err { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "TestPSK: Client error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else if let Ok(client) = result { - let actual_client_srtp = client.selected_srtpprotection_profile(); - assert_eq!(actual_client_srtp, expected_profile, - "test_srtp_configuration: Client SRTPProtectionProfile Mismatch '{name}': expected({expected_profile:?}) actual({actual_client_srtp:?})"); - } else { - panic!("{name} expected no error"); - } - } else { - panic!("{name} expected client, but got none"); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_client_certificate() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let server_name = "localhost".to_owned(); - - let srv_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - let mut srv_ca_pool = rustls::RootCertStore::empty(); - srv_ca_pool - .add(srv_cert.certificate[0].to_owned()) - .map_err(|_err| Error::Other("add srv_cert error".to_owned()))?; - - let cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - let mut ca_pool = rustls::RootCertStore::empty(); - ca_pool - .add(cert.certificate[0].to_owned()) - .map_err(|_err| Error::Other("add cert error".to_owned()))?; - - let tests = vec![ - ( - "NoClientCert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - false, - ), - ( - "NoClientCert_cert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequireAnyClientCert, - ..Default::default() - }, - false, - ), - ( - "RequestClientCert_cert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequestClientCert, - ..Default::default() - }, - false, - ), - ( - "RequestClientCert_no_cert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequestClientCert, - ..Default::default() - }, - false, - ), - ( - "RequireAnyClientCert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequireAnyClientCert, - ..Default::default() - }, - false, - ), - ( - "RequireAnyClientCert_error", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequireAnyClientCert, - ..Default::default() - }, - true, - ), - ( - "VerifyClientCertIfGiven_no_cert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::VerifyClientCertIfGiven, - client_cas: ca_pool.clone(), - ..Default::default() - }, - false, - ), - ( - "VerifyClientCertIfGiven_cert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::VerifyClientCertIfGiven, - client_cas: ca_pool.clone(), - ..Default::default() - }, - false, - ), - ( - "VerifyClientCertIfGiven_error", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::VerifyClientCertIfGiven, - ..Default::default() - }, - true, - ), - ( - "RequireAndVerifyClientCert", - Config { - roots_cas: srv_ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![srv_cert.clone()], - client_auth: ClientAuthType::RequireAndVerifyClientCert, - client_cas: ca_pool.clone(), - ..Default::default() - }, - false, - ), - ]; - - for (name, client_cfg, server_cfg, want_err) in tests { - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - let client_cfg_clone = client_cfg.clone(); - tokio::spawn(async move { - let result = DTLSConn::new(Arc::new(ca), client_cfg_clone, true, None).await; - let _ = client_res_tx.send(result).await; - }); - - let result = DTLSConn::new(Arc::new(cb), server_cfg.clone(), false, None).await; - let client_result = client_res_rx.recv().await; - - if want_err { - if result.is_err() { - continue; - } - panic!("{name} Error expected"); - } - - assert!( - result.is_ok(), - "{} Server failed({:?})", - name, - result.err().unwrap() - ); - assert!(client_result.is_some(), "{name}, expected client conn"); - - let res = client_result.unwrap(); - assert!( - res.is_ok(), - "{} Client failed({:?})", - name, - res.err().unwrap() - ); - - let server = result.unwrap(); - let client = res.unwrap(); - - let actual_client_cert = &server.connection_state().await.peer_certificates; - if server_cfg.client_auth == ClientAuthType::RequireAnyClientCert - || server_cfg.client_auth == ClientAuthType::RequireAndVerifyClientCert - { - assert!( - !actual_client_cert.is_empty(), - "{name} Client did not provide a certificate", - ); - //if actual_client_cert.len() != len(tt.clientCfg.Certificates[0].Certificate) || !bytes.Equal(tt.clientCfg.Certificates[0].Certificate[0], actual_client_cert[0]) { - assert_eq!( - actual_client_cert[0], - client_cfg.certificates[0].certificate[0].as_ref(), - "{name} Client certificate was not communicated correctly", - ); - } - - if server_cfg.client_auth == ClientAuthType::NoClientCert { - assert!( - actual_client_cert.is_empty(), - "{name} Client certificate wasn't expected", - ); - } - - let actual_server_cert = &client.connection_state().await.peer_certificates; - assert!( - !actual_server_cert.is_empty(), - "{name} Server did not provide a certificate", - ); - - /*if len(actual_server_cert) != len(tt.serverCfg.Certificates[0].Certificate) - || !bytes.Equal( - tt.serverCfg.Certificates[0].Certificate[0], - actual_server_cert[0], - )*/ - assert_eq!( - actual_server_cert[0].len(), - server_cfg.certificates[0].certificate[0].as_ref().len(), - "{name} Server certificate was not communicated correctly", - ); - assert_eq!( - actual_server_cert[0], - server_cfg.certificates[0].certificate[0].as_ref(), - "{name} Server certificate was not communicated correctly", - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_extended_master_secret() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let tests = vec![ - ( - "Request_Request_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - None, - None, - ), - ( - "Request_Require_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - None, - None, - ), - ( - "Request_Disable_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - None, - None, - ), - ( - "Require_Request_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - None, - None, - ), - ( - "Require_Require_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - None, - None, - ), - ( - "Require_Disable_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - Some(Error::ErrClientRequiredButNoServerEms), - Some(Error::ErrAlertFatalOrClose), - ), - ( - "Disable_Request_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Request, - ..Default::default() - }, - None, - None, - ), - ( - "Disable_Require_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Require, - ..Default::default() - }, - Some(Error::ErrAlertFatalOrClose), - Some(Error::ErrServerRequiredButNoClientEms), - ), - ( - "Disable_Disable_ExtendedMasterSecret", - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - Config { - extended_master_secret: ExtendedMasterSecretType::Disable, - ..Default::default() - }, - None, - None, - ), - ]; - - for (name, client_cfg, server_cfg, expected_client_err, expected_server_err) in tests { - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - let client_cfg_clone = client_cfg.clone(); - tokio::spawn(async move { - let result = create_test_client(Arc::new(ca), client_cfg_clone, true).await; - let _ = client_res_tx.send(result).await; - }); - - let result = create_test_server(Arc::new(cb), server_cfg.clone(), true).await; - let client_result = client_res_rx.recv().await; - assert!(client_result.is_some(), "{name}, expected client conn"); - let res = client_result.unwrap(); - - if let Some(client_err) = expected_client_err { - if let Err(err) = res { - assert_eq!( - err.to_string(), - client_err.to_string(), - "Client error expected: \"{client_err}\" but got \"{err}\"", - ); - } else { - panic!("{name} expected err, but got ok"); - } - } else { - assert!(res.is_ok(), "{name} expected ok, but got err"); - } - - if let Some(server_err) = expected_server_err { - if let Err(err) = result { - assert_eq!( - err.to_string(), - server_err.to_string(), - "Server error expected: \"{server_err}\" but got \"{err}\"", - ); - } else { - panic!("{name} expected err, but got ok"); - } - } else { - assert!(result.is_ok(), "{name} expected ok, but got err"); - } - } - - Ok(()) -} - -fn fn_not_expected_chain(_cert: &[Vec], chain: &[CertificateDer<'static>]) -> Result<()> { - if !chain.is_empty() { - return Err(Error::Other(ERR_NOT_EXPECTED_CHAIN.to_owned())); - } - Ok(()) -} - -fn fn_expected_chain(_cert: &[Vec], chain: &[CertificateDer<'static>]) -> Result<()> { - if chain.is_empty() { - return Err(Error::Other(ERR_EXPECTED_CHAIN.to_owned())); - } - Ok(()) -} - -fn fn_wrong_cert(_cert: &[Vec], _chain: &[CertificateDer<'static>]) -> Result<()> { - Err(Error::Other(ERR_WRONG_CERT.to_owned())) -} - -#[tokio::test] -async fn test_server_certificate() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let server_name = "localhost".to_owned(); - let cert = Certificate::generate_self_signed(vec![server_name.clone()])?; - let mut ca_pool = rustls::RootCertStore::empty(); - ca_pool - .add(cert.certificate[0].clone()) - .map_err(|_err| Error::Other("add cert error".to_owned()))?; - - let tests = vec![ - ( - "no_ca", - Config { - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - true, - ), - ( - "good_ca", - Config { - roots_cas: ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - false, - ), - ( - "no_ca_skip_verify", - Config { - insecure_skip_verify: true, - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - false, - ), - ( - "good_ca_skip_verify_custom_verify_peer", - Config { - roots_cas: ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::RequireAnyClientCert, - verify_peer_certificate: Some(Arc::new(fn_not_expected_chain)), - ..Default::default() - }, - false, - ), - ( - "good_ca_verify_custom_verify_peer", - Config { - roots_cas: ca_pool.clone(), - server_name: server_name.clone(), - certificates: vec![cert.clone()], - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::RequireAndVerifyClientCert, - client_cas: ca_pool.clone(), - verify_peer_certificate: Some(Arc::new(fn_expected_chain)), - ..Default::default() - }, - false, - ), - ( - "good_ca_custom_verify_peer", - Config { - roots_cas: ca_pool.clone(), - server_name: server_name.clone(), - verify_peer_certificate: Some(Arc::new(fn_wrong_cert)), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - true, - ), - ( - "server_name", - Config { - roots_cas: ca_pool.clone(), - server_name: server_name.clone(), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - false, - ), - ( - "server_name_error", - Config { - roots_cas: ca_pool.clone(), - server_name: "barfoo".to_owned(), - ..Default::default() - }, - Config { - certificates: vec![cert.clone()], - client_auth: ClientAuthType::NoClientCert, - ..Default::default() - }, - true, - ), - ]; - - for (name, client_cfg, server_cfg, want_err) in tests { - let (res_tx, mut res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - - tokio::spawn(async move { - let result = DTLSConn::new(Arc::new(cb), server_cfg, false, None).await; - let _ = res_tx.send(result).await; - }); - - let cli_result = DTLSConn::new(Arc::new(ca), client_cfg, true, None).await; - - if !want_err && cli_result.is_err() { - panic!("{}: Client failed({})", name, cli_result.err().unwrap()); - } - if want_err && cli_result.is_ok() { - panic!("{name}: Error expected"); - } - - let _ = res_rx.recv().await; - } - Ok(()) -} - -#[tokio::test] -async fn test_cipher_suite_configuration() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let tests = vec![ - ( - "No CipherSuites specified", - vec![], - vec![], - None, - None, - None, - ), - ( - "Invalid CipherSuite", - vec![CipherSuiteId::Unsupported], - vec![CipherSuiteId::Unsupported], - Some(Error::ErrInvalidCipherSuite), - Some(Error::ErrInvalidCipherSuite), - None, - ), - ( - "Valid CipherSuites specified", - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - None, - None, - Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256), - ), - ( - "CipherSuites mismatch", - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha], - Some(Error::ErrAlertFatalOrClose), - Some(Error::ErrCipherSuiteNoIntersection), - None, - ), - ( - "Valid CipherSuites CCM specified", - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm], - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm], - None, - None, - Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm), - ), - ( - "Valid CipherSuites CCM-8 specified", - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8], - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8], - None, - None, - Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8), - ), - ( - "Server supports subset of client suites", - vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, - ], - vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha], - None, - None, - Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha), - ), - ]; - - for ( - name, - client_cipher_suites, - server_cipher_suites, - want_client_error, - want_server_error, - want_selected_cipher_suite, - ) in tests - { - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - tokio::spawn(async move { - let conf = Config { - cipher_suites: client_cipher_suites, - ..Default::default() - }; - - let result = create_test_client(Arc::new(ca), conf, true).await; - let _ = client_res_tx.send(result).await; - }); - - let config = Config { - cipher_suites: server_cipher_suites, - ..Default::default() - }; - - let result = create_test_server(Arc::new(cb), config, true).await; - if let Some(expected_err) = want_server_error { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "{name} test_cipher_suite_configuration: Server error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else { - assert!(result.is_ok(), "{name} expected ok, but got error") - } - - let client_result = client_res_rx.recv().await; - if let Some(result) = client_result { - if let Some(expected_err) = want_client_error { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "{name} test_cipher_suite_configuration: Client error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else { - assert!(result.is_ok(), "{name} expected ok, but got error"); - let client = result.unwrap(); - if let Some(want_cs) = want_selected_cipher_suite { - let cipher_suite = client.state.cipher_suite.lock().await; - assert!(cipher_suite.is_some(), "{name} expected some, but got none"); - if let Some(cs) = &*cipher_suite { - assert_eq!(cs.id(), want_cs, - "test_cipher_suite_configuration: Server Selected Bad Cipher Suite '{}': expected({}) actual({})", - name, want_cs, cs.id()); - } - } - } - } else { - panic!("{name} expected Some, but got None"); - } - } - - Ok(()) -} - -fn psk_callback(_b: &[u8]) -> Result> { - Ok(vec![0x00, 0x01, 0x02]) -} - -#[tokio::test] -async fn test_psk_configuration() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let tests = vec![ - ( - "PSK specified", - false, - false, - true, //Some(psk_callback), - true, //Some(psk_callback), - Some(vec![0x00]), - Some(vec![0x00]), - Some(Error::ErrNoAvailableCipherSuites), - Some(Error::ErrNoAvailableCipherSuites), - ), - ( - "PSK and certificate specified", - true, - true, - true, //Some(psk_callback), - true, //Some(psk_callback), - Some(vec![0x00]), - Some(vec![0x00]), - Some(Error::ErrPskAndCertificate), - Some(Error::ErrPskAndCertificate), - ), - ( - "PSK and no identity specified", - false, - false, - true, //Some(psk_callback), - true, //Some(psk_callback), - None, - None, - Some(Error::ErrPskAndIdentityMustBeSetForClient), - Some(Error::ErrNoAvailableCipherSuites), - ), - ( - "No PSK and identity specified", - false, - false, - false, - false, - Some(vec![0x00]), - Some(vec![0x00]), - Some(Error::ErrIdentityNoPsk), - Some(Error::ErrServerMustHaveCertificate), - ), - ]; - - for ( - name, - client_has_certificate, - server_has_certificate, - client_psk, - server_psk, - client_psk_identity, - server_psk_identity, - want_client_error, - want_server_error, - ) in tests - { - let (client_res_tx, mut client_res_rx) = mpsc::channel(1); - let (ca, cb) = pipe(); - tokio::spawn(async move { - let conf = Config { - psk: if client_psk { - Some(Arc::new(psk_callback)) - } else { - None - }, - psk_identity_hint: client_psk_identity, - ..Default::default() - }; - - let result = create_test_client(Arc::new(ca), conf, client_has_certificate).await; - let _ = client_res_tx.send(result).await; - }); - - let config = Config { - psk: if server_psk { - Some(Arc::new(psk_callback)) - } else { - None - }, - psk_identity_hint: server_psk_identity, - ..Default::default() - }; - - let result = create_test_server(Arc::new(cb), config, server_has_certificate).await; - if let Some(expected_err) = want_server_error { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "{name} test_psk_configuration: Server error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else { - assert!(result.is_ok(), "{name} expected ok, but got error") - } - - let client_result = client_res_rx.recv().await; - if let Some(result) = client_result { - if let Some(expected_err) = want_client_error { - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "{name} test_psk_configuration: Client error exp({expected_err}) failed({err})", - ); - } else { - panic!("{name} expected error, but got ok"); - } - } else { - assert!(result.is_ok(), "{name} expected ok, but got error"); - } - } else { - panic!("{name} expected Some, but got None"); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_server_timeout() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut cookie = vec![0u8; 20]; - rand::thread_rng().fill(cookie.as_mut_slice()); - - let random_bytes = [0u8; RANDOM_BYTES_LENGTH]; - let gmt_unix_time = SystemTime::UNIX_EPOCH - .checked_add(Duration::new(500, 0)) - .unwrap(); - let random = HandshakeRandom { - gmt_unix_time, - random_bytes, - }; - - let cipher_suites = vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, //&cipherSuiteTLSEcdheEcdsaWithAes128GcmSha256{}, - CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256, //&cipherSuiteTLSEcdheRsaWithAes128GcmSha256{}, - ]; - - let extensions = vec![ - Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { - signature_hash_algorithms: vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Rsa, - }, - ], - }), - Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::X25519, NamedCurve::P256, NamedCurve::P384], - }), - Extension::SupportedPointFormats(ExtensionSupportedPointFormats { - point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], - }), - ]; - - let record = RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - cookie, - random, - cipher_suites, - compression_methods: default_compression_methods(), - extensions, - }, - ))), - ); - - let mut packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); - record.marshal(&mut writer)?; - } - - use util::Conn; - let (ca, cb) = pipe(); - - // Client reader - let (ca_read_chan_tx, mut ca_read_chan_rx) = mpsc::channel(1000); - - let ca_rx = Arc::new(ca); - let ca_tx = Arc::clone(&ca_rx); - - tokio::spawn(async move { - let mut data = vec![0; 8192]; - loop { - if let Ok(n) = ca_rx.recv(&mut data).await { - let result = ca_read_chan_tx.send(data[..n].to_vec()).await; - if result.is_ok() { - return; - } - } else { - return; - } - } - }); - - // Start sending ClientHello packets until server responds with first packet - tokio::spawn(async move { - loop { - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() => { - let result = ca_tx.send(&packet).await; - if result.is_err() { - return; - } - } - _ = ca_read_chan_rx.recv() => return, - } - } - }); - - let config = Config { - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - flight_interval: Duration::from_millis(100), - ..Default::default() - }; - - let result = tokio::time::timeout( - Duration::from_millis(50), - create_test_server(Arc::new(cb), config, true), - ) - .await; - assert!(result.is_err(), "Expected Error but got Ok"); - - // Wait a little longer to ensure no additional messages have been sent by the server - //tokio::time::sleep(Duration::from_millis(300)).await; - - /*tokio::select! { - case msg := <-caReadChan: - t.Fatalf("Expected no additional messages from server, got: %+v", msg) - default: - }*/ - - Ok(()) -} - -#[tokio::test] -async fn test_protocol_version_validation() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut cookie = vec![0; 20]; - rand::thread_rng().fill(cookie.as_mut_slice()); - - let random_bytes = [0u8; RANDOM_BYTES_LENGTH]; - let gmt_unix_time = SystemTime::UNIX_EPOCH - .checked_add(Duration::new(500, 0)) - .unwrap(); - let random = HandshakeRandom { - gmt_unix_time, - random_bytes, - }; - - let local_keypair = NamedCurve::X25519.generate_keypair()?; - - //|"Server"| - { - let server_cases = vec![ - ( - "ClientHelloVersion", - vec![RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: ProtocolVersion { - major: 0xfe, - minor: 0xff, - }, // try to downgrade - cookie: cookie.clone(), - random: random.clone(), - cipher_suites: vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - ], - compression_methods: default_compression_methods(), - extensions: vec![], - }, - ))), - )], - ), - ( - "SecondsClientHelloVersion", - vec![ - RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - cookie: cookie.clone(), - random: random.clone(), - cipher_suites: vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - ], - compression_methods: default_compression_methods(), - extensions: vec![], - }, - ))), - ), - { - let mut handshake = Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: ProtocolVersion { - major: 0xfe, - minor: 0xff, - }, // try to downgrade - cookie: cookie.clone(), - random: random.clone(), - cipher_suites: vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - ], - compression_methods: default_compression_methods(), - extensions: vec![], - }, - )); - handshake.handshake_header.message_sequence = 1; - let mut record_layer = - RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record_layer.record_layer_header.sequence_number = 1; - - record_layer - }, - ], - ), - ]; - - use util::Conn; - for (name, records) in server_cases { - let (ca, cb) = pipe(); - - tokio::spawn(async move { - let config = Config { - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - flight_interval: Duration::from_millis(100), - ..Default::default() - }; - let timeout_result = tokio::time::timeout( - Duration::from_millis(1000), - create_test_server(Arc::new(cb), config, true), - ) - .await; - match timeout_result { - Ok(result) => { - if let Err(err) = result { - assert_eq!( - err.to_string(), - Error::ErrUnsupportedProtocolVersion.to_string(), - "{} Client error exp({}) failed({})", - name, - Error::ErrUnsupportedProtocolVersion, - err, - ); - } else { - panic!("{name} expected error, but got ok"); - } - } - Err(err) => { - panic!("server timeout {err}"); - } - }; - }); - - tokio::time::sleep(Duration::from_millis(50)).await; - - let mut resp = vec![0; 1024]; - let mut n = 0; - for record in records { - let mut packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); - record.marshal(&mut writer)?; - } - - let _ = ca.send(&packet).await; - n = ca.recv(&mut resp).await?; - } - - let mut reader = BufReader::new(&resp[..n]); - let h = RecordLayerHeader::unmarshal(&mut reader)?; - assert_eq!( - h.content_type, - ContentType::Alert, - "Peer must return alert to unsupported protocol version" - ); - } - } - - //"Client" - { - let client_cases = vec![( - "ServerHelloVersion", - vec![ - RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::HelloVerifyRequest( - HandshakeMessageHelloVerifyRequest { - version: PROTOCOL_VERSION1_2, - cookie: cookie.clone(), - }, - ))), - ), - { - let mut handshake = Handshake::new(HandshakeMessage::ServerHello( - HandshakeMessageServerHello { - version: ProtocolVersion { - major: 0xfe, - minor: 0xff, - }, // try to downgrade - random: random.clone(), - cipher_suite: CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - compression_method: default_compression_methods().ids[0], - extensions: vec![], - }, - )); - handshake.handshake_header.message_sequence = 1; - let mut record = - RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record.record_layer_header.sequence_number = 1; - record - }, - { - let mut handshake = Handshake::new(HandshakeMessage::Certificate( - HandshakeMessageCertificate { - certificate: vec![], - }, - )); - handshake.handshake_header.message_sequence = 2; - let mut record = - RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record.record_layer_header.sequence_number = 2; - record - }, - { - let mut handshake = Handshake::new(HandshakeMessage::ServerKeyExchange( - HandshakeMessageServerKeyExchange { - identity_hint: vec![], - elliptic_curve_type: EllipticCurveType::NamedCurve, - named_curve: NamedCurve::X25519, - public_key: local_keypair.public_key.clone(), - algorithm: SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - signature: vec![0; 64], - }, - )); - handshake.handshake_header.message_sequence = 3; - let mut record = - RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record.record_layer_header.sequence_number = 3; - record - }, - { - let mut handshake = Handshake::new(HandshakeMessage::ServerHelloDone( - HandshakeMessageServerHelloDone {}, - )); - handshake.handshake_header.message_sequence = 4; - let mut record = - RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record.record_layer_header.sequence_number = 4; - record - }, - ], - )]; - - use util::Conn; - for (name, records) in client_cases { - let (ca, cb) = pipe(); - - tokio::spawn(async move { - let config = Config { - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - flight_interval: Duration::from_millis(100), - ..Default::default() - }; - let timeout_result = tokio::time::timeout( - Duration::from_millis(1000), - create_test_client(Arc::new(cb), config, true), - ) - .await; - match timeout_result { - Ok(result) => { - if let Err(err) = result { - assert_eq!( - err.to_string(), - Error::ErrUnsupportedProtocolVersion.to_string(), - "{} Server error exp({}) failed({})", - name, - Error::ErrUnsupportedProtocolVersion, - err, - ); - } else { - panic!("{name} expected error, but got ok"); - } - } - Err(err) => { - panic!("server timeout {err}"); - } - }; - }); - - tokio::time::sleep(Duration::from_millis(50)).await; - - let mut resp = vec![0; 1024]; - for record in records { - let _ = ca.recv(&mut resp).await?; - - let mut packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); - record.marshal(&mut writer)?; - } - let _ = ca.send(&packet).await; - } - - let n = ca.recv(&mut resp).await?; - - let mut reader = BufReader::new(&resp[..n]); - let h = RecordLayerHeader::unmarshal(&mut reader)?; - - assert_eq!( - h.content_type, - ContentType::Alert, - "Peer must return alert to unsupported protocol version" - ); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_multiple_hello_verify_request() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut cookies = vec![ - // first clientHello contains an empty cookie - vec![], - ]; - - let mut packets = vec![]; - for i in 0..2 { - let mut cookie = vec![0; 20]; - rand::thread_rng().fill(cookie.as_mut_slice()); - cookies.push(cookie.clone()); - - let mut handshake = Handshake::new(HandshakeMessage::HelloVerifyRequest( - HandshakeMessageHelloVerifyRequest { - version: PROTOCOL_VERSION1_2, - cookie, - }, - )); - handshake.handshake_header.message_sequence = i as u16; - - let mut record = RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); - record.record_layer_header.sequence_number = i as u64; - - let mut packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); - record.marshal(&mut writer)?; - } - - packets.push(packet); - } - - let (ca, cb) = pipe(); - - tokio::spawn(async move { - let conf = Config::default(); - let _ = tokio::time::timeout( - Duration::from_millis(100), - create_test_client(Arc::new(ca), conf, true), - ) - .await; - }); - - for i in 0..cookies.len() { - let cookie = &cookies[i]; - trace!("cookie {}: {:?}", i, cookie); - - // read client hello - let mut resp = vec![0; 1024]; - let n = cb.recv(&mut resp).await?; - let mut reader = BufReader::new(&resp[..n]); - let record = RecordLayer::unmarshal(&mut reader)?; - match record.content { - Content::Handshake(h) => match h.handshake_message { - HandshakeMessage::ClientHello(client_hello) => { - assert_eq!( - &client_hello.cookie, cookie, - "Wrong cookie {}, expected: {:?}, got: {:?}", - i, &client_hello.cookie, cookie - ); - } - _ => panic!("unexpected handshake message"), - }, - _ => panic!("unexpected content"), - }; - - if packets.len() <= i { - break; - } - // write hello verify request - cb.send(&packets[i]).await?; - } - - Ok(()) -} - -async fn send_client_hello( - cookie: Vec, - ca: &Arc, - sequence_number: u64, - send_renegotiation_info: bool, -) -> Result<()> { - let mut extensions = vec![]; - if send_renegotiation_info { - extensions.push(Extension::RenegotiationInfo(ExtensionRenegotiationInfo { - renegotiated_connection: 0, - })); - } - - let mut h = Handshake::new(HandshakeMessage::ClientHello(HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - random: HandshakeRandom::default(), - cookie, - - cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], - compression_methods: default_compression_methods(), - extensions, - })); - h.handshake_header.message_sequence = sequence_number as u16; - - let mut record = RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(h)); - record.record_layer_header.sequence_number = sequence_number; - - let mut packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); - record.marshal(&mut writer)?; - } - - ca.send(&packet).await?; - - Ok(()) -} - -// Assert that a DTLS Server always responds with RenegotiationInfo if -// a ClientHello contained that extension or not -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_renegotiation_info() -> Result<()> { - let mut resp = vec![0u8; 1024]; - - let tests = vec![ - ("Include RenegotiationInfo", true), - ("No RenegotiationInfo", false), - ]; - - for (name, send_renegotiation_info) in tests { - let (ca, cb) = pipe(); - - tokio::spawn(async move { - let conf = Config::default(); - let _ = tokio::time::timeout( - Duration::from_millis(100), - create_test_server(Arc::new(cb), conf, true), - ) - .await; - }); - - tokio::time::sleep(Duration::from_millis(5)).await; - - let ca: Arc = Arc::new(ca); - send_client_hello(vec![], &ca, 0, send_renegotiation_info).await?; - - let n = ca.recv(&mut resp).await?; - let mut reader = BufReader::new(&resp[..n]); - let record = RecordLayer::unmarshal(&mut reader)?; - - let hello_verify_request = match record.content { - Content::Handshake(h) => match h.handshake_message { - HandshakeMessage::HelloVerifyRequest(hvr) => hvr, - _ => { - panic!("unexpected handshake message"); - } - }, - _ => { - panic!("unexpected content"); - } - }; - - send_client_hello( - hello_verify_request.cookie.clone(), - &ca, - 1, - send_renegotiation_info, - ) - .await?; - let n = ca.recv(&mut resp).await?; - let messages = unpack_datagram(&resp[..n])?; - - let mut reader = BufReader::new(&messages[0][..]); - let record = RecordLayer::unmarshal(&mut reader)?; - - let server_hello = match record.content { - Content::Handshake(h) => match h.handshake_message { - HandshakeMessage::ServerHello(sh) => sh, - _ => { - panic!("unexpected handshake message"); - } - }, - _ => { - panic!("unexpected content"); - } - }; - - let got_negotiation_info = server_hello - .extensions - .iter() - .any(|v| matches!(v, Extension::RenegotiationInfo(_))); - - assert!( - got_negotiation_info, - "{name}: Received ServerHello without RenegotiationInfo" - ); - - ca.close().await?; - } - - Ok(()) -} diff --git a/dtls/src/conn/mod.rs b/dtls/src/conn/mod.rs deleted file mode 100644 index f160443c2..000000000 --- a/dtls/src/conn/mod.rs +++ /dev/null @@ -1,1215 +0,0 @@ -#[cfg(test)] -mod conn_test; - -use std::io::{BufReader, BufWriter}; -use std::marker::{Send, Sync}; -use std::net::SocketAddr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use log::*; -use portable_atomic::{AtomicBool, AtomicU16}; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; -use util::replay_detector::*; -use util::Conn; - -use crate::alert::*; -use crate::application_data::*; -use crate::cipher_suite::*; -use crate::config::*; -use crate::content::*; -use crate::curve::named_curve::NamedCurve; -use crate::error::*; -use crate::extension::extension_use_srtp::*; -use crate::flight::flight0::*; -use crate::flight::flight1::*; -use crate::flight::flight5::*; -use crate::flight::flight6::*; -use crate::flight::*; -use crate::fragment_buffer::*; -use crate::handshake::handshake_cache::*; -use crate::handshake::handshake_header::HandshakeHeader; -use crate::handshake::*; -use crate::handshaker::*; -use crate::record_layer::record_layer_header::*; -use crate::record_layer::*; -use crate::signature_hash_algorithm::parse_signature_schemes; -use crate::state::*; - -pub(crate) const INITIAL_TICKER_INTERVAL: Duration = Duration::from_secs(1); -pub(crate) const COOKIE_LENGTH: usize = 20; -pub(crate) const DEFAULT_NAMED_CURVE: NamedCurve = NamedCurve::X25519; -pub(crate) const INBOUND_BUFFER_SIZE: usize = 8192; -// Default replay protection window is specified by RFC 6347 Section 4.1.2.6 -pub(crate) const DEFAULT_REPLAY_PROTECTION_WINDOW: usize = 64; - -pub static INVALID_KEYING_LABELS: &[&str] = &[ - "client finished", - "server finished", - "master secret", - "key expansion", -]; - -type PacketSendRequest = (Vec, Option>>); - -struct ConnReaderContext { - is_client: bool, - replay_protection_window: usize, - replay_detector: Vec>, - decrypted_tx: mpsc::Sender>>, - encrypted_packets: Vec>, - fragment_buffer: FragmentBuffer, - cache: HandshakeCache, - cipher_suite: Arc>>>, - remote_epoch: Arc, - handshake_tx: mpsc::Sender>, - handshake_done_rx: mpsc::Receiver<()>, - packet_tx: Arc>, -} - -// Conn represents a DTLS connection -pub struct DTLSConn { - conn: Arc, - pub(crate) cache: HandshakeCache, // caching of handshake messages for verifyData generation - decrypted_rx: Mutex>>>, // Decrypted Application Data or error, pull by calling `Read` - pub(crate) state: State, // Internal state - - handshake_completed_successfully: Arc, - connection_closed_by_user: bool, - // closeLock sync.Mutex - closed: AtomicBool, // *closer.Closer - //handshakeLoopsFinished sync.WaitGroup - - //readDeadline :deadline.Deadline, - //writeDeadline :deadline.Deadline, - - //log logging.LeveledLogger - /* - reading chan struct{} - handshakeRecv chan chan struct{} - cancelHandshaker func() - cancelHandshakeReader func() - */ - pub(crate) current_flight: Box, - pub(crate) flights: Option>, - pub(crate) cfg: HandshakeConfig, - pub(crate) retransmit: bool, - pub(crate) handshake_rx: mpsc::Receiver>, - - pub(crate) packet_tx: Arc>, - pub(crate) handle_queue_tx: mpsc::Sender>, - pub(crate) handshake_done_tx: Option>, - - reader_close_tx: Mutex>>, -} - -type UtilResult = std::result::Result; - -#[async_trait] -impl Conn for DTLSConn { - async fn connect(&self, _addr: SocketAddr) -> UtilResult<()> { - Err(util::Error::Other("Not applicable".to_owned())) - } - async fn recv(&self, buf: &mut [u8]) -> UtilResult { - self.read(buf, None).await.map_err(util::Error::from_std) - } - async fn recv_from(&self, buf: &mut [u8]) -> UtilResult<(usize, SocketAddr)> { - if let Some(raddr) = self.conn.remote_addr() { - let n = self.read(buf, None).await.map_err(util::Error::from_std)?; - Ok((n, raddr)) - } else { - Err(util::Error::Other( - "No remote address is provided by underlying Conn".to_owned(), - )) - } - } - async fn send(&self, buf: &[u8]) -> UtilResult { - self.write(buf, None).await.map_err(util::Error::from_std) - } - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> UtilResult { - Err(util::Error::Other("Not applicable".to_owned())) - } - fn local_addr(&self) -> UtilResult { - self.conn.local_addr() - } - fn remote_addr(&self) -> Option { - self.conn.remote_addr() - } - async fn close(&self) -> UtilResult<()> { - self.close().await.map_err(util::Error::from_std) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -impl DTLSConn { - pub async fn new( - conn: Arc, - mut config: Config, - is_client: bool, - initial_state: Option, - ) -> Result { - validate_config(is_client, &config)?; - - let local_cipher_suites: Vec = parse_cipher_suites( - &config.cipher_suites, - config.psk.is_none(), - config.psk.is_some(), - )? - .iter() - .map(|cs| cs.id()) - .collect(); - - let sigs: Vec = config.signature_schemes.iter().map(|x| *x as u16).collect(); - let local_signature_schemes = parse_signature_schemes(&sigs, config.insecure_hashes)?; - - let retransmit_interval = if config.flight_interval != Duration::from_secs(0) { - config.flight_interval - } else { - INITIAL_TICKER_INTERVAL - }; - - /* - loggerFactory := config.LoggerFactory - if loggerFactory == nil { - loggerFactory = logging.NewDefaultLoggerFactory() - } - - logger := loggerFactory.NewLogger("dtls") - */ - let maximum_transmission_unit = if config.mtu == 0 { - DEFAULT_MTU - } else { - config.mtu - }; - - let replay_protection_window = if config.replay_protection_window == 0 { - DEFAULT_REPLAY_PROTECTION_WINDOW - } else { - config.replay_protection_window - }; - - let mut server_name = config.server_name.clone(); - - // Use host from conn address when server_name is not provided - if is_client && server_name.is_empty() { - if let Some(remote_addr) = conn.remote_addr() { - server_name = remote_addr.ip().to_string(); - } else { - warn!("conn.remote_addr is empty, please set explicitly server_name in Config! Use default \"localhost\" as server_name now"); - "localhost".clone_into(&mut server_name); - } - } - - let cfg = HandshakeConfig { - local_psk_callback: config.psk.take(), - local_psk_identity_hint: config.psk_identity_hint.take(), - local_cipher_suites, - local_signature_schemes, - extended_master_secret: config.extended_master_secret, - local_srtp_protection_profiles: config.srtp_protection_profiles.clone(), - server_name, - client_auth: config.client_auth, - local_certificates: config.certificates.clone(), - insecure_skip_verify: config.insecure_skip_verify, - insecure_verification: config.insecure_verification, - verify_peer_certificate: config.verify_peer_certificate.take(), - client_cert_verifier: if config.client_auth as u8 - >= ClientAuthType::VerifyClientCertIfGiven as u8 - { - Some( - rustls::server::WebPkiClientVerifier::builder(Arc::new(config.client_cas)) - .allow_unauthenticated() - .build() - .unwrap_or( - rustls::server::WebPkiClientVerifier::builder(Arc::new( - gen_self_signed_root_cert(), - )) - .allow_unauthenticated() - .build() - .unwrap(), - ), - ) - } else { - None - }, - server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new( - config.roots_cas, - )) - .build() - .unwrap_or( - rustls::client::WebPkiServerVerifier::builder( - Arc::new(gen_self_signed_root_cert()), - ) - .build() - .unwrap(), - ), - retransmit_interval, - //log: logger, - initial_epoch: 0, - ..Default::default() - }; - - let (state, flight, initial_fsm_state) = if let Some(state) = initial_state { - let flight = if is_client { - Box::new(Flight5 {}) as Box - } else { - Box::new(Flight6 {}) as Box - }; - - (state, flight, HandshakeState::Finished) - } else { - let flight = if is_client { - Box::new(Flight1 {}) as Box - } else { - Box::new(Flight0 {}) as Box - }; - - ( - State { - is_client, - ..Default::default() - }, - flight, - HandshakeState::Preparing, - ) - }; - - let (decrypted_tx, decrypted_rx) = mpsc::channel(1); - let (handshake_tx, handshake_rx) = mpsc::channel(1); - let (handshake_done_tx, handshake_done_rx) = mpsc::channel(1); - let (packet_tx, mut packet_rx) = mpsc::channel(1); - let (handle_queue_tx, mut handle_queue_rx) = mpsc::channel(1); - let (reader_close_tx, mut reader_close_rx) = mpsc::channel(1); - - let packet_tx = Arc::new(packet_tx); - let packet_tx2 = Arc::clone(&packet_tx); - let next_conn_rx = Arc::clone(&conn); - let next_conn_tx = Arc::clone(&conn); - let cache = HandshakeCache::new(); - let mut cache1 = cache.clone(); - let cache2 = cache.clone(); - let handshake_completed_successfully = Arc::new(AtomicBool::new(false)); - let handshake_completed_successfully2 = Arc::clone(&handshake_completed_successfully); - - let mut c = DTLSConn { - conn: Arc::clone(&conn), - cache, - decrypted_rx: Mutex::new(decrypted_rx), - state, - handshake_completed_successfully, - connection_closed_by_user: false, - closed: AtomicBool::new(false), - - current_flight: flight, - flights: None, - cfg, - retransmit: false, - handshake_rx, - packet_tx, - handle_queue_tx, - handshake_done_tx: Some(handshake_done_tx), - reader_close_tx: Mutex::new(Some(reader_close_tx)), - }; - - let cipher_suite1 = Arc::clone(&c.state.cipher_suite); - let sequence_number = Arc::clone(&c.state.local_sequence_number); - - tokio::spawn(async move { - loop { - let rx = packet_rx.recv().await; - if let Some(r) = rx { - let (pkt, result_tx) = r; - - let result = DTLSConn::handle_outgoing_packets( - &next_conn_tx, - pkt, - &mut cache1, - is_client, - &sequence_number, - &cipher_suite1, - maximum_transmission_unit, - ) - .await; - - if let Some(tx) = result_tx { - let _ = tx.send(result).await; - } - } else { - trace!("{}: handle_outgoing_packets exit", srv_cli_str(is_client)); - break; - } - } - }); - - let local_epoch = Arc::clone(&c.state.local_epoch); - let remote_epoch = Arc::clone(&c.state.remote_epoch); - let cipher_suite2 = Arc::clone(&c.state.cipher_suite); - - tokio::spawn(async move { - let mut buf = vec![0u8; INBOUND_BUFFER_SIZE]; - let mut ctx = ConnReaderContext { - is_client, - replay_protection_window, - replay_detector: vec![], - decrypted_tx, - encrypted_packets: vec![], - fragment_buffer: FragmentBuffer::new(), - cache: cache2, - cipher_suite: cipher_suite2, - remote_epoch, - handshake_tx, - handshake_done_rx, - packet_tx: packet_tx2, - }; - - //trace!("before enter read_and_buffer: {}] ", srv_cli_str(is_client)); - loop { - tokio::select! { - _ = reader_close_rx.recv() => { - trace!( - "{}: read_and_buffer exit", - srv_cli_str(ctx.is_client), - ); - break; - } - result = DTLSConn::read_and_buffer( - &mut ctx, - &next_conn_rx, - &mut handle_queue_rx, - &mut buf, - &local_epoch, - &handshake_completed_successfully2, - ) => { - if let Err(err) = result { - trace!( - "{}: read_and_buffer return err: {}", - srv_cli_str(is_client), - err - ); - if Error::ErrAlertFatalOrClose == err { - trace!( - "{}: read_and_buffer exit with {}", - srv_cli_str(ctx.is_client), - err - ); - - break; - } - } - } - } - } - }); - - // Do handshake - c.handshake(initial_fsm_state).await?; - - trace!("Handshake Completed"); - - Ok(c) - } - - // Read reads data from the connection. - pub async fn read(&self, p: &mut [u8], duration: Option) -> Result { - if !self.is_handshake_completed_successfully() { - return Err(Error::ErrHandshakeInProgress); - } - - let rx = { - let mut decrypted_rx = self.decrypted_rx.lock().await; - if let Some(d) = duration { - let timer = tokio::time::sleep(d); - tokio::pin!(timer); - - tokio::select! { - r = decrypted_rx.recv() => r, - _ = timer.as_mut() => return Err(Error::ErrDeadlineExceeded), - } - } else { - decrypted_rx.recv().await - } - }; - - if let Some(out) = rx { - match out { - Ok(val) => { - let n = val.len(); - if p.len() < n { - return Err(Error::ErrBufferTooSmall); - } - p[..n].copy_from_slice(&val); - Ok(n) - } - Err(err) => Err(err), - } - } else { - Err(Error::ErrAlertFatalOrClose) - } - } - - // Write writes len(p) bytes from p to the DTLS connection - pub async fn write(&self, p: &[u8], duration: Option) -> Result { - if self.is_connection_closed() { - return Err(Error::ErrConnClosed); - } - - if !self.is_handshake_completed_successfully() { - return Err(Error::ErrHandshakeInProgress); - } - - let pkts = vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - self.get_local_epoch(), - Content::ApplicationData(ApplicationData { data: p.to_vec() }), - ), - should_encrypt: true, - reset_local_sequence_number: false, - }]; - - if let Some(d) = duration { - let timer = tokio::time::sleep(d); - tokio::pin!(timer); - - tokio::select! { - result = self.write_packets(pkts) => { - result?; - } - _ = timer.as_mut() => return Err(Error::ErrDeadlineExceeded), - } - } else { - self.write_packets(pkts).await?; - } - - Ok(p.len()) - } - - // Close closes the connection. - pub async fn close(&self) -> Result<()> { - if !self.closed.load(Ordering::SeqCst) { - self.closed.store(true, Ordering::SeqCst); - - // Discard error from notify() to return non-error on the first user call of Close() - // even if the underlying connection is already closed. - self.notify(AlertLevel::Warning, AlertDescription::CloseNotify) - .await?; - - { - let mut reader_close_tx = self.reader_close_tx.lock().await; - reader_close_tx.take(); - } - self.conn.close().await?; - } - - Ok(()) - } - - /// connection_state returns basic DTLS details about the connection. - /// Note that this replaced the `Export` function of v1. - pub async fn connection_state(&self) -> State { - self.state.clone().await - } - - /// selected_srtpprotection_profile returns the selected SRTPProtectionProfile - pub fn selected_srtpprotection_profile(&self) -> SrtpProtectionProfile { - self.state.srtp_protection_profile - } - - pub(crate) async fn notify(&self, level: AlertLevel, desc: AlertDescription) -> Result<()> { - self.write_packets(vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - self.get_local_epoch(), - Content::Alert(Alert { - alert_level: level, - alert_description: desc, - }), - ), - should_encrypt: self.is_handshake_completed_successfully(), - reset_local_sequence_number: false, - }]) - .await - } - - pub(crate) async fn write_packets(&self, pkts: Vec) -> Result<()> { - let (tx, mut rx) = mpsc::channel(1); - - self.packet_tx.send((pkts, Some(tx))).await?; - - if let Some(result) = rx.recv().await { - result - } else { - Ok(()) - } - } - - async fn handle_outgoing_packets( - next_conn: &Arc, - mut pkts: Vec, - cache: &mut HandshakeCache, - is_client: bool, - local_sequence_number: &Arc>>, - cipher_suite: &Arc>>>, - maximum_transmission_unit: usize, - ) -> Result<()> { - let mut raw_packets = vec![]; - for p in &mut pkts { - if let Content::Handshake(h) = &p.record.content { - let mut handshake_raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(handshake_raw.as_mut()); - p.record.marshal(&mut writer)?; - } - trace!( - "Send [handshake:{}] -> {} (epoch: {}, seq: {})", - srv_cli_str(is_client), - h.handshake_header.handshake_type.to_string(), - p.record.record_layer_header.epoch, - h.handshake_header.message_sequence - ); - cache - .push( - handshake_raw[RECORD_LAYER_HEADER_SIZE..].to_vec(), - p.record.record_layer_header.epoch, - h.handshake_header.message_sequence, - h.handshake_header.handshake_type, - is_client, - ) - .await; - - let raw_handshake_packets = DTLSConn::process_handshake_packet( - local_sequence_number, - cipher_suite, - maximum_transmission_unit, - p, - h, - ) - .await?; - raw_packets.extend_from_slice(&raw_handshake_packets); - } else { - /*if let Content::Alert(a) = &p.record.content { - if a.alert_description == AlertDescription::CloseNotify { - closed = true; - } - }*/ - - let raw_packet = - DTLSConn::process_packet(local_sequence_number, cipher_suite, p).await?; - raw_packets.push(raw_packet); - } - } - - if !raw_packets.is_empty() { - let compacted_raw_packets = - compact_raw_packets(&raw_packets, maximum_transmission_unit); - - for compacted_raw_packets in &compacted_raw_packets { - next_conn.send(compacted_raw_packets).await?; - } - } - - Ok(()) - } - - async fn process_packet( - local_sequence_number: &Arc>>, - cipher_suite: &Arc>>>, - p: &mut Packet, - ) -> Result> { - let epoch = p.record.record_layer_header.epoch as usize; - let seq = { - let mut lsn = local_sequence_number.lock().await; - while lsn.len() <= epoch { - lsn.push(0); - } - - lsn[epoch] += 1; - lsn[epoch] - 1 - }; - //trace!("{}: seq = {}", srv_cli_str(is_client), seq); - - if seq > MAX_SEQUENCE_NUMBER { - // RFC 6347 Section 4.1.0 - // The implementation must either abandon an association or rehandshake - // prior to allowing the sequence number to wrap. - return Err(Error::ErrSequenceNumberOverflow); - } - p.record.record_layer_header.sequence_number = seq; - - let mut raw_packet = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw_packet.as_mut()); - p.record.marshal(&mut writer)?; - } - - if p.should_encrypt { - let cipher_suite = cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - raw_packet = cipher_suite.encrypt(&p.record.record_layer_header, &raw_packet)?; - } - } - - Ok(raw_packet) - } - - async fn process_handshake_packet( - local_sequence_number: &Arc>>, - cipher_suite: &Arc>>>, - maximum_transmission_unit: usize, - p: &Packet, - h: &Handshake, - ) -> Result>> { - let mut raw_packets = vec![]; - - let handshake_fragments = DTLSConn::fragment_handshake(maximum_transmission_unit, h)?; - - let epoch = p.record.record_layer_header.epoch as usize; - - let mut lsn = local_sequence_number.lock().await; - while lsn.len() <= epoch { - lsn.push(0); - } - - for handshake_fragment in &handshake_fragments { - let seq = { - lsn[epoch] += 1; - lsn[epoch] - 1 - }; - //trace!("seq = {}", seq); - if seq > MAX_SEQUENCE_NUMBER { - return Err(Error::ErrSequenceNumberOverflow); - } - - let record_layer_header = RecordLayerHeader { - protocol_version: p.record.record_layer_header.protocol_version, - content_type: p.record.record_layer_header.content_type, - content_len: handshake_fragment.len() as u16, - epoch: p.record.record_layer_header.epoch, - sequence_number: seq, - }; - - let mut record_layer_header_bytes = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(record_layer_header_bytes.as_mut()); - record_layer_header.marshal(&mut writer)?; - } - - //p.record.record_layer_header = record_layer_header; - - let mut raw_packet = vec![]; - raw_packet.extend_from_slice(&record_layer_header_bytes); - raw_packet.extend_from_slice(handshake_fragment); - if p.should_encrypt { - let cipher_suite = cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - raw_packet = cipher_suite.encrypt(&record_layer_header, &raw_packet)?; - } - } - - raw_packets.push(raw_packet); - } - - Ok(raw_packets) - } - - fn fragment_handshake(maximum_transmission_unit: usize, h: &Handshake) -> Result>> { - let mut content = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(content.as_mut()); - h.handshake_message.marshal(&mut writer)?; - } - - let mut fragmented_handshakes = vec![]; - - let mut content_fragments = split_bytes(&content, maximum_transmission_unit); - if content_fragments.is_empty() { - content_fragments = vec![vec![]]; - } - - let mut offset = 0; - for content_fragment in &content_fragments { - let content_fragment_len = content_fragment.len(); - - let handshake_header_fragment = HandshakeHeader { - handshake_type: h.handshake_header.handshake_type, - length: h.handshake_header.length, - message_sequence: h.handshake_header.message_sequence, - fragment_offset: offset as u32, - fragment_length: content_fragment_len as u32, - }; - - offset += content_fragment_len; - - let mut handshake_header_fragment_raw = vec![]; - { - let mut writer = - BufWriter::<&mut Vec>::new(handshake_header_fragment_raw.as_mut()); - handshake_header_fragment.marshal(&mut writer)?; - } - - let mut fragmented_handshake = vec![]; - fragmented_handshake.extend_from_slice(&handshake_header_fragment_raw); - fragmented_handshake.extend_from_slice(content_fragment); - - fragmented_handshakes.push(fragmented_handshake); - } - - Ok(fragmented_handshakes) - } - - pub(crate) fn set_handshake_completed_successfully(&mut self) { - self.handshake_completed_successfully - .store(true, Ordering::SeqCst); - } - - pub(crate) fn is_handshake_completed_successfully(&self) -> bool { - self.handshake_completed_successfully.load(Ordering::SeqCst) - } - - async fn read_and_buffer( - ctx: &mut ConnReaderContext, - next_conn: &Arc, - handle_queue_rx: &mut mpsc::Receiver>, - buf: &mut [u8], - local_epoch: &Arc, - handshake_completed_successfully: &Arc, - ) -> Result<()> { - let n = next_conn.recv(buf).await?; - let pkts = unpack_datagram(&buf[..n])?; - let mut has_handshake = false; - for pkt in pkts { - let (hs, alert, mut err) = DTLSConn::handle_incoming_packet(ctx, pkt, true).await; - if let Some(alert) = alert { - let alert_err = ctx - .packet_tx - .send(( - vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - local_epoch.load(Ordering::SeqCst), - Content::Alert(Alert { - alert_level: alert.alert_level, - alert_description: alert.alert_description, - }), - ), - should_encrypt: handshake_completed_successfully.load(Ordering::SeqCst), - reset_local_sequence_number: false, - }], - None, - )) - .await; - - if let Err(alert_err) = alert_err { - if err.is_none() { - err = Some(Error::Other(alert_err.to_string())); - } - } - - if alert.alert_level == AlertLevel::Fatal - || alert.alert_description == AlertDescription::CloseNotify - { - return Err(Error::ErrAlertFatalOrClose); - } - } - - if let Some(err) = err { - return Err(err); - } - - if hs { - has_handshake = true - } - } - - if has_handshake { - let (done_tx, mut done_rx) = mpsc::channel(1); - - tokio::select! { - _ = ctx.handshake_tx.send(done_tx) => { - let mut wait_done_rx = true; - while wait_done_rx{ - tokio::select!{ - _ = done_rx.recv() => { - // If the other party may retransmit the flight, - // we should respond even if it not a new message. - wait_done_rx = false; - } - done = handle_queue_rx.recv() => { - //trace!("recv handle_queue: {} ", srv_cli_str(ctx.is_client)); - - let pkts = ctx.encrypted_packets.drain(..).collect(); - DTLSConn::handle_queued_packets(ctx, local_epoch, handshake_completed_successfully, pkts).await?; - - drop(done); - } - } - } - } - _ = ctx.handshake_done_rx.recv() => {} - } - } - - Ok(()) - } - - async fn handle_queued_packets( - ctx: &mut ConnReaderContext, - local_epoch: &Arc, - handshake_completed_successfully: &Arc, - pkts: Vec>, - ) -> Result<()> { - for p in pkts { - let (_, alert, mut err) = DTLSConn::handle_incoming_packet(ctx, p, false).await; // don't re-enqueue - if let Some(alert) = alert { - let alert_err = ctx - .packet_tx - .send(( - vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - local_epoch.load(Ordering::SeqCst), - Content::Alert(Alert { - alert_level: alert.alert_level, - alert_description: alert.alert_description, - }), - ), - should_encrypt: handshake_completed_successfully.load(Ordering::SeqCst), - reset_local_sequence_number: false, - }], - None, - )) - .await; - - if let Err(alert_err) = alert_err { - if err.is_none() { - err = Some(Error::Other(alert_err.to_string())); - } - } - if alert.alert_level == AlertLevel::Fatal - || alert.alert_description == AlertDescription::CloseNotify - { - return Err(Error::ErrAlertFatalOrClose); - } - } - - if let Some(err) = err { - return Err(err); - } - } - - Ok(()) - } - - async fn handle_incoming_packet( - ctx: &mut ConnReaderContext, - mut pkt: Vec, - enqueue: bool, - ) -> (bool, Option, Option) { - let mut reader = BufReader::new(pkt.as_slice()); - let h = match RecordLayerHeader::unmarshal(&mut reader) { - Ok(h) => h, - Err(err) => { - // Decode error must be silently discarded - // [RFC6347 Section-4.1.2.7] - debug!( - "{}: discarded broken packet: {}", - srv_cli_str(ctx.is_client), - err - ); - return (false, None, None); - } - }; - - // Validate epoch - let epoch = ctx.remote_epoch.load(Ordering::SeqCst); - if h.epoch > epoch { - if h.epoch > epoch + 1 { - debug!( - "{}: discarded future packet (epoch: {}, seq: {})", - srv_cli_str(ctx.is_client), - h.epoch, - h.sequence_number, - ); - return (false, None, None); - } - if enqueue { - debug!( - "{}: received packet of next epoch, queuing packet", - srv_cli_str(ctx.is_client) - ); - ctx.encrypted_packets.push(pkt); - } - return (false, None, None); - } - - // Anti-replay protection - while ctx.replay_detector.len() <= h.epoch as usize { - ctx.replay_detector - .push(Box::new(SlidingWindowDetector::new( - ctx.replay_protection_window, - MAX_SEQUENCE_NUMBER, - ))); - } - - let ok = ctx.replay_detector[h.epoch as usize].check(h.sequence_number); - if !ok { - debug!( - "{}: discarded duplicated packet (epoch: {}, seq: {})", - srv_cli_str(ctx.is_client), - h.epoch, - h.sequence_number, - ); - return (false, None, None); - } - - // Decrypt - if h.epoch != 0 { - let invalid_cipher_suite = { - let cipher_suite = ctx.cipher_suite.lock().await; - if cipher_suite.is_none() { - true - } else if let Some(cipher_suite) = &*cipher_suite { - !cipher_suite.is_initialized() - } else { - false - } - }; - if invalid_cipher_suite { - if enqueue { - debug!( - "{}: handshake not finished, queuing packet", - srv_cli_str(ctx.is_client) - ); - ctx.encrypted_packets.push(pkt); - } - return (false, None, None); - } - - let cipher_suite = ctx.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - pkt = match cipher_suite.decrypt(&pkt) { - Ok(pkt) => pkt, - Err(err) => { - debug!("{}: decrypt failed: {}", srv_cli_str(ctx.is_client), err); - - // If we get an error for PSK we need to return an error. - if cipher_suite.is_psk() { - return ( - false, - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnknownPskIdentity, - }), - None, - ); - } else { - return (false, None, None); - } - } - }; - } - } - - let is_handshake = match ctx.fragment_buffer.push(&pkt) { - Ok(is_handshake) => is_handshake, - Err(err) => { - // Decode error must be silently discarded - // [RFC6347 Section-4.1.2.7] - debug!("{}: defragment failed: {}", srv_cli_str(ctx.is_client), err); - return (false, None, None); - } - }; - if is_handshake { - ctx.replay_detector[h.epoch as usize].accept(); - while let Ok((out, epoch)) = ctx.fragment_buffer.pop() { - //log::debug!("Extension Debug: out.len()={}", out.len()); - let mut reader = BufReader::new(out.as_slice()); - let raw_handshake = match Handshake::unmarshal(&mut reader) { - Ok(rh) => { - trace!( - "Recv [handshake:{}] -> {} (epoch: {}, seq: {})", - srv_cli_str(ctx.is_client), - rh.handshake_header.handshake_type.to_string(), - h.epoch, - rh.handshake_header.message_sequence - ); - rh - } - Err(err) => { - debug!( - "{}: handshake parse failed: {}", - srv_cli_str(ctx.is_client), - err - ); - continue; - } - }; - - ctx.cache - .push( - out, - epoch, - raw_handshake.handshake_header.message_sequence, - raw_handshake.handshake_header.handshake_type, - !ctx.is_client, - ) - .await; - } - - return (true, None, None); - } - - let mut reader = BufReader::new(pkt.as_slice()); - let r = match RecordLayer::unmarshal(&mut reader) { - Ok(r) => r, - Err(err) => { - return ( - false, - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::DecodeError, - }), - Some(err), - ); - } - }; - - match r.content { - Content::Alert(mut a) => { - trace!("{}: <- {}", srv_cli_str(ctx.is_client), a.to_string()); - if a.alert_description == AlertDescription::CloseNotify { - // Respond with a close_notify [RFC5246 Section 7.2.1] - a = Alert { - alert_level: AlertLevel::Warning, - alert_description: AlertDescription::CloseNotify, - }; - } - ctx.replay_detector[h.epoch as usize].accept(); - return ( - false, - Some(a), - Some(Error::Other(format!("Error of Alert {a}"))), - ); - } - Content::ChangeCipherSpec(_) => { - let invalid_cipher_suite = { - let cipher_suite = ctx.cipher_suite.lock().await; - if cipher_suite.is_none() { - true - } else if let Some(cipher_suite) = &*cipher_suite { - !cipher_suite.is_initialized() - } else { - false - } - }; - - if invalid_cipher_suite { - if enqueue { - debug!( - "{}: CipherSuite not initialized, queuing packet", - srv_cli_str(ctx.is_client) - ); - ctx.encrypted_packets.push(pkt); - } - return (false, None, None); - } - - let new_remote_epoch = h.epoch + 1; - trace!( - "{}: <- ChangeCipherSpec (epoch: {})", - srv_cli_str(ctx.is_client), - new_remote_epoch - ); - - if epoch + 1 == new_remote_epoch { - ctx.remote_epoch.store(new_remote_epoch, Ordering::SeqCst); - ctx.replay_detector[h.epoch as usize].accept(); - } - } - Content::ApplicationData(a) => { - if h.epoch == 0 { - return ( - false, - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnexpectedMessage, - }), - Some(Error::ErrApplicationDataEpochZero), - ); - } - - ctx.replay_detector[h.epoch as usize].accept(); - - let _ = ctx.decrypted_tx.send(Ok(a.data)).await; - //TODO - /*select { - case self.decrypted < - content.data: - case < -c.closed.Done(): - }*/ - } - _ => { - return ( - false, - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnexpectedMessage, - }), - Some(Error::ErrUnhandledContextType), - ); - } - }; - - (false, None, None) - } - - fn is_connection_closed(&self) -> bool { - self.closed.load(Ordering::SeqCst) - } - - pub(crate) fn set_local_epoch(&mut self, epoch: u16) { - self.state.local_epoch.store(epoch, Ordering::SeqCst); - } - - pub(crate) fn get_local_epoch(&self) -> u16 { - self.state.local_epoch.load(Ordering::SeqCst) - } -} - -fn compact_raw_packets(raw_packets: &[Vec], maximum_transmission_unit: usize) -> Vec> { - let mut combined_raw_packets = vec![]; - let mut current_combined_raw_packet = vec![]; - - for raw_packet in raw_packets { - if !current_combined_raw_packet.is_empty() - && current_combined_raw_packet.len() + raw_packet.len() >= maximum_transmission_unit - { - combined_raw_packets.push(current_combined_raw_packet); - current_combined_raw_packet = vec![]; - } - current_combined_raw_packet.extend_from_slice(raw_packet); - } - - combined_raw_packets.push(current_combined_raw_packet); - - combined_raw_packets -} - -fn split_bytes(bytes: &[u8], split_len: usize) -> Vec> { - let mut splits = vec![]; - let num_bytes = bytes.len(); - for i in (0..num_bytes).step_by(split_len) { - let mut j = i + split_len; - if j > num_bytes { - j = num_bytes; - } - - splits.push(bytes[i..j].to_vec()); - } - - splits -} diff --git a/dtls/src/content.rs b/dtls/src/content.rs deleted file mode 100644 index f1b0f623f..000000000 --- a/dtls/src/content.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::io::{Read, Write}; - -use super::alert::*; -use super::application_data::*; -use super::change_cipher_spec::*; -use super::handshake::*; -use crate::error::*; - -// https://tools.ietf.org/html/rfc4346#section-6.2.1 -#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] -pub enum ContentType { - ChangeCipherSpec = 20, - Alert = 21, - Handshake = 22, - ApplicationData = 23, - #[default] - Invalid, -} - -impl From for ContentType { - fn from(val: u8) -> Self { - match val { - 20 => ContentType::ChangeCipherSpec, - 21 => ContentType::Alert, - 22 => ContentType::Handshake, - 23 => ContentType::ApplicationData, - _ => ContentType::Invalid, - } - } -} - -#[derive(PartialEq, Debug, Clone)] -pub enum Content { - ChangeCipherSpec(ChangeCipherSpec), - Alert(Alert), - Handshake(Handshake), - ApplicationData(ApplicationData), -} - -impl Content { - pub fn content_type(&self) -> ContentType { - match self { - Content::ChangeCipherSpec(c) => c.content_type(), - Content::Alert(c) => c.content_type(), - Content::Handshake(c) => c.content_type(), - Content::ApplicationData(c) => c.content_type(), - } - } - - pub fn size(&self) -> usize { - match self { - Content::ChangeCipherSpec(c) => c.size(), - Content::Alert(c) => c.size(), - Content::Handshake(c) => c.size(), - Content::ApplicationData(c) => c.size(), - } - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - match self { - Content::ChangeCipherSpec(c) => c.marshal(writer), - Content::Alert(c) => c.marshal(writer), - Content::Handshake(c) => c.marshal(writer), - Content::ApplicationData(c) => c.marshal(writer), - } - } - - pub fn unmarshal(content_type: ContentType, reader: &mut R) -> Result { - match content_type { - ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec( - ChangeCipherSpec::unmarshal(reader)?, - )), - ContentType::Alert => Ok(Content::Alert(Alert::unmarshal(reader)?)), - ContentType::Handshake => Ok(Content::Handshake(Handshake::unmarshal(reader)?)), - ContentType::ApplicationData => Ok(Content::ApplicationData( - ApplicationData::unmarshal(reader)?, - )), - _ => Err(Error::ErrInvalidContentType), - } - } -} diff --git a/dtls/src/crypto/crypto_cbc.rs b/dtls/src/crypto/crypto_cbc.rs deleted file mode 100644 index 74b1681ff..000000000 --- a/dtls/src/crypto/crypto_cbc.rs +++ /dev/null @@ -1,129 +0,0 @@ -// AES-CBC (Cipher Block Chaining) -// First historic block cipher for AES. -// CBC mode is insecure and must not be used. It’s been progressively deprecated and -// removed from SSL libraries. -// Introduced with TLS 1.0 year 2002. Superseded by GCM in TLS 1.2 year 2008. -// Removed in TLS 1.3 year 2018. -// RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268 - -// https://github.com/RustCrypto/block-ciphers - -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use p256::elliptic_curve::subtle::ConstantTimeEq; -use rand::Rng; -use std::io::Cursor; -use std::ops::Not; - -use super::padding::DtlsPadding; -use crate::content::*; -use crate::error::*; -use crate::prf::*; -use crate::record_layer::record_layer_header::*; -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; - -// State needed to handle encrypted input/output -#[derive(Clone)] -pub struct CryptoCbc { - local_key: Vec, - remote_key: Vec, - write_mac: Vec, - read_mac: Vec, -} - -impl CryptoCbc { - const BLOCK_SIZE: usize = 16; - const MAC_SIZE: usize = 20; - - pub fn new( - local_key: &[u8], - local_mac: &[u8], - remote_key: &[u8], - remote_mac: &[u8], - ) -> Result { - Ok(CryptoCbc { - local_key: local_key.to_vec(), - write_mac: local_mac.to_vec(), - - remote_key: remote_key.to_vec(), - read_mac: remote_mac.to_vec(), - }) - } - - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec(); - let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - - // Generate + Append MAC - let h = pkt_rlh; - - let mac = prf_mac( - h.epoch, - h.sequence_number, - h.content_type, - h.protocol_version, - &payload, - &self.write_mac, - )?; - payload.extend_from_slice(&mac); - - let mut iv: Vec = vec![0; Self::BLOCK_SIZE]; - rand::thread_rng().fill(iv.as_mut_slice()); - - let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?; - let encrypted = write_cbc.encrypt_padded_vec_mut::(&payload); - - // Prepend unencrypte header with encrypted payload - let mut r = vec![]; - r.extend_from_slice(raw); - r.extend_from_slice(&iv); - r.extend_from_slice(&encrypted); - - let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; - r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] - .copy_from_slice(&r_len.to_be_bytes()); - - Ok(r) - } - - pub fn decrypt(&self, r: &[u8]) -> Result> { - let mut reader = Cursor::new(r); - let h = RecordLayerHeader::unmarshal(&mut reader)?; - if h.content_type == ContentType::ChangeCipherSpec { - // Nothing to encrypt with ChangeCipherSpec - return Ok(r.to_vec()); - } - - let body = &r[RECORD_LAYER_HEADER_SIZE..]; - let iv = &body[0..Self::BLOCK_SIZE]; - let body = &body[Self::BLOCK_SIZE..]; - //TODO: add body.len() check - - let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?; - - let decrypted = read_cbc - .decrypt_padded_vec_mut::(body) - .map_err(|_| Error::ErrInvalidPacketLength)?; - - let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..]; - let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE]; - let mac = prf_mac( - h.epoch, - h.sequence_number, - h.content_type, - h.protocol_version, - decrypted, - &self.read_mac, - )?; - - if recv_mac.ct_eq(&mac).not().into() { - return Err(Error::ErrInvalidMac); - } - - let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + decrypted.len()); - d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); - d.extend_from_slice(decrypted); - - Ok(d) - } -} diff --git a/dtls/src/crypto/crypto_ccm.rs b/dtls/src/crypto/crypto_ccm.rs deleted file mode 100644 index a932e5439..000000000 --- a/dtls/src/crypto/crypto_ccm.rs +++ /dev/null @@ -1,188 +0,0 @@ -// AES-CCM (Counter with CBC-MAC) -// Alternative to GCM mode. -// Available in OpenSSL as of TLS 1.3 (2018), but disabled by default. -// Two AES computations per block, thus expected to be somewhat slower than AES-GCM. -// RFC 6655 year 2012 https://tools.ietf.org/html/rfc6655 -// Much lower adoption, probably because it came after GCM and offer no significant benefit. - -// https://github.com/RustCrypto/AEADs -// https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm? - -use std::io::Cursor; - -use aes::Aes128; -use ccm::aead::generic_array::GenericArray; -use ccm::aead::AeadInPlace; -use ccm::consts::{U12, U16, U8}; -use ccm::Ccm; -use ccm::KeyInit; -use rand::Rng; - -use super::*; -use crate::content::*; -use crate::error::*; -use crate::record_layer::record_layer_header::*; - -const CRYPTO_CCM_8_TAG_LENGTH: usize = 8; -const CRYPTO_CCM_TAG_LENGTH: usize = 16; -const CRYPTO_CCM_NONCE_LENGTH: usize = 12; - -type AesCcm8 = Ccm; -type AesCcm = Ccm; - -#[derive(Clone)] -pub enum CryptoCcmTagLen { - CryptoCcm8TagLength, - CryptoCcmTagLength, -} - -enum CryptoCcmType { - CryptoCcm8(AesCcm8), - CryptoCcm(AesCcm), -} - -// State needed to handle encrypted input/output -pub struct CryptoCcm { - local_ccm: CryptoCcmType, - remote_ccm: CryptoCcmType, - local_write_iv: Vec, - remote_write_iv: Vec, - // used by clone() - local_write_key: Vec, - remote_write_key: Vec, -} - -impl Clone for CryptoCcm { - fn clone(&self) -> Self { - match self.local_ccm { - CryptoCcmType::CryptoCcm(_) => Self::new( - &CryptoCcmTagLen::CryptoCcmTagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - CryptoCcmType::CryptoCcm8(_) => Self::new( - &CryptoCcmTagLen::CryptoCcm8TagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - } - } -} - -impl CryptoCcm { - pub fn new( - tag_len: &CryptoCcmTagLen, - local_key: &[u8], - local_write_iv: &[u8], - remote_key: &[u8], - remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), - }; - - let key = GenericArray::from_slice(remote_key); - let remote_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), - }; - - CryptoCcm { - local_ccm, - local_write_key: local_key.to_vec(), - local_write_iv: local_write_iv.to_vec(), - remote_ccm, - remote_write_key: remote_key.to_vec(), - remote_write_iv: remote_write_iv.to_vec(), - } - } - - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; - let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - - let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH]; - nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::thread_rng().fill(&mut nonce[4..]); - let nonce = GenericArray::from_slice(&nonce); - - let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); - - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(payload); - - match &self.local_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - ccm.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - } - - let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); - - r.extend_from_slice(raw); - r.extend_from_slice(&nonce[4..]); - r.extend_from_slice(&buffer); - - // Update recordLayer size to include explicit nonce - let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; - r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] - .copy_from_slice(&r_len.to_be_bytes()); - - Ok(r) - } - - pub fn decrypt(&self, r: &[u8]) -> Result> { - let mut reader = Cursor::new(r); - let h = RecordLayerHeader::unmarshal(&mut reader)?; - if h.content_type == ContentType::ChangeCipherSpec { - // Nothing to encrypt with ChangeCipherSpec - return Ok(r.to_vec()); - } - - if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) { - return Err(Error::ErrNotEnoughRoomForNonce); - } - - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..4]); - nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); - let nonce = GenericArray::from_slice(&nonce); - - let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; - - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); - - match &self.remote_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH); - ccm.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH); - ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - } - - let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); - d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); - d.extend_from_slice(&buffer); - - Ok(d) - } -} diff --git a/dtls/src/crypto/crypto_gcm.rs b/dtls/src/crypto/crypto_gcm.rs deleted file mode 100644 index 9928ccad0..000000000 --- a/dtls/src/crypto/crypto_gcm.rs +++ /dev/null @@ -1,119 +0,0 @@ -// AES-GCM (Galois Counter Mode) -// The most widely used block cipher worldwide. -// Mandatory as of TLS 1.2 (2008) and used by default by most clients. -// RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288 - -// https://github.com/RustCrypto/AEADs -// https://docs.rs/aes-gcm/0.8.0/aes_gcm/ - -use std::io::Cursor; - -use aes_gcm::aead::generic_array::GenericArray; -use aes_gcm::aead::AeadInPlace; -use aes_gcm::{Aes128Gcm, KeyInit}; -use rand::Rng; - -use super::*; -use crate::content::*; -use crate::error::*; -use crate::record_layer::record_layer_header::*; // what about Aes256Gcm? - -const CRYPTO_GCM_TAG_LENGTH: usize = 16; -const CRYPTO_GCM_NONCE_LENGTH: usize = 12; - -// State needed to handle encrypted input/output -#[derive(Clone)] -pub struct CryptoGcm { - local_gcm: Aes128Gcm, - remote_gcm: Aes128Gcm, - local_write_iv: Vec, - remote_write_iv: Vec, -} - -impl CryptoGcm { - pub fn new( - local_key: &[u8], - local_write_iv: &[u8], - remote_key: &[u8], - remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_gcm = Aes128Gcm::new(key); - - let key = GenericArray::from_slice(remote_key); - let remote_gcm = Aes128Gcm::new(key); - - CryptoGcm { - local_gcm, - local_write_iv: local_write_iv.to_vec(), - remote_gcm, - remote_write_iv: remote_write_iv.to_vec(), - } - } - - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; - let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - - let mut nonce = vec![0u8; CRYPTO_GCM_NONCE_LENGTH]; - nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::thread_rng().fill(&mut nonce[4..]); - let nonce = GenericArray::from_slice(&nonce); - - let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); - - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(payload); - - self.local_gcm - .encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - - let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); - r.extend_from_slice(raw); - r.extend_from_slice(&nonce[4..]); - r.extend_from_slice(&buffer); - - // Update recordLayer size to include explicit nonce - let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; - r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] - .copy_from_slice(&r_len.to_be_bytes()); - - Ok(r) - } - - pub fn decrypt(&self, r: &[u8]) -> Result> { - let mut reader = Cursor::new(r); - let h = RecordLayerHeader::unmarshal(&mut reader)?; - if h.content_type == ContentType::ChangeCipherSpec { - // Nothing to encrypt with ChangeCipherSpec - return Ok(r.to_vec()); - } - - if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) { - return Err(Error::ErrNotEnoughRoomForNonce); - } - - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..4]); - nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); - let nonce = GenericArray::from_slice(&nonce); - - let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; - - let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_GCM_TAG_LENGTH); - - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); - - self.remote_gcm - .decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - - let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); - d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); - d.extend_from_slice(&buffer); - - Ok(d) - } -} diff --git a/dtls/src/crypto/crypto_test.rs b/dtls/src/crypto/crypto_test.rs deleted file mode 100644 index f06988bc6..000000000 --- a/dtls/src/crypto/crypto_test.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::io::Cursor; - -use x509_parser::pem::Pem; - -use super::crypto_ccm::*; -use super::*; -use crate::content::ContentType; -use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE}; - -const RAW_PRIVATE_KEY: &str = " ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP -6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3 -bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1 -ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB -ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL -rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0 -N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra -ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3 -AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0 -xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD -8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl -pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22 -HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S -Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87 -mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin -mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71 -hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5 -lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH -BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz -uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ -srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq -eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O -UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr -nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L -JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/ ------END RSA PRIVATE KEY----- -"; - -#[test] -fn test_generate_key_signature() -> Result<()> { - let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes()); - let pem = match Pem::read(reader) { - Ok((pem, _)) => pem, - Err(_) => return Err(Error::Other("Pem::read error".to_owned())), - }; - //let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?; - - let client_random = vec![ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, - 0x1e, 0x1f, - ]; - let server_random = vec![ - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, - 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, - 0x8e, 0x8f, - ]; - let public_key = vec![ - 0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, - 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, - 0x80, 0xb6, 0x15, - ]; - let expected_signature = vec![ - 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, - 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, - 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, - 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, - 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, - 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, - 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, - 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, - 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, - 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, - 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, - 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, - 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, - 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, - 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, - 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, - 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, - 0x86, - ]; - - let signature = generate_key_signature( - &client_random, - &server_random, - &public_key, - NamedCurve::X25519, - &CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_der(&pem.contents) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der: pem.contents.clone(), - }, //hashAlgorithmSHA256, - )?; - - assert_eq!( - signature, expected_signature, - "Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} " - ); - - Ok(()) -} - -#[test] -fn test_ccm_encryption_and_decryption() -> Result<()> { - let key = vec![ - 0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, 0x63, - 0x54, - ]; - let iv = vec![0x0e, 0xb2, 0x09, 0x06]; - - let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv); - - let rlh = RecordLayerHeader { - content_type: ContentType::ApplicationData, - protocol_version: ProtocolVersion { - major: 0xfe, - minor: 0xff, - }, - epoch: 0, - sequence_number: 18, - content_len: 3, - }; - - let raw = vec![ - 0x17, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x03, 0xff, 0xaa, - 0xbb, - ]; - - let cipher_text = ccm.encrypt(&rlh, &raw)?; - - assert_eq!( - &cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE], - [0, 27], - "RecordLayer size updating failed \nexp: {:?} \nactual {:?} ", - [0, 27], - &cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] - ); - - let plain_text = ccm.decrypt(&cipher_text)?; - - assert_eq!( - raw[RECORD_LAYER_HEADER_SIZE..], - plain_text[RECORD_LAYER_HEADER_SIZE..], - "Decryption failed \nexp: {:?} \nactual {:?} ", - &raw[RECORD_LAYER_HEADER_SIZE..], - &plain_text[RECORD_LAYER_HEADER_SIZE..] - ); - - Ok(()) -} - -#[test] -fn test_certificate_verify() -> Result<()> { - let plain_text: Vec = vec![ - 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, - 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, - 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, - 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, - 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, - 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, - 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, - 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, - 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, - 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, - 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, - 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, - 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, - 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, - 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, - 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, - 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, - 0x86, - ]; - - //test ECDSA256 - let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - let cert_verify_ecdsa256 = - generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?; - verify_certificate_verify( - &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - &cert_verify_ecdsa256, - &certificate_ecdsa256 - .certificate - .iter() - .map(|x| x.as_ref().to_owned()) - .collect::>>(), - false, - )?; - - //test ED25519 - let certificate_ed25519 = Certificate::generate_self_signed_with_alg( - vec!["localhost".to_owned()], - &rcgen::PKCS_ED25519, - )?; - let cert_verify_ed25519 = - generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?; - verify_certificate_verify( - &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ed25519, - }, - &cert_verify_ed25519, - &certificate_ed25519 - .certificate - .iter() - .map(|x| x.as_ref().to_owned()) - .collect::>>(), - false, - )?; - - Ok(()) -} diff --git a/dtls/src/crypto/mod.rs b/dtls/src/crypto/mod.rs deleted file mode 100644 index 92bba0977..000000000 --- a/dtls/src/crypto/mod.rs +++ /dev/null @@ -1,528 +0,0 @@ -#[cfg(test)] -mod crypto_test; - -pub mod crypto_cbc; -pub mod crypto_ccm; -pub mod crypto_gcm; -pub mod padding; - -use std::convert::TryFrom; -use std::sync::Arc; - -use der_parser::oid; -use der_parser::oid::Oid; - -use rustls::client::danger::ServerCertVerifier; -use rustls::pki_types::{CertificateDer, ServerName}; -use rustls::server::danger::ClientCertVerifier; - -use rcgen::{generate_simple_self_signed, CertifiedKey, KeyPair}; -use ring::rand::SystemRandom; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; - -use crate::curve::named_curve::*; -use crate::error::*; -use crate::record_layer::record_layer_header::*; -use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm}; - -/// A X.509 certificate(s) used to authenticate a DTLS connection. -#[derive(Clone, PartialEq, Debug)] -pub struct Certificate { - /// DER-encoded certificates. - pub certificate: Vec>, - /// Private key. - pub private_key: CryptoPrivateKey, -} - -impl Certificate { - /// Generate a self-signed certificate. - /// - /// See [`rcgen::generate_simple_self_signed`]. - pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { - let CertifiedKey { cert, key_pair } = - generate_simple_self_signed(subject_alt_names).unwrap(); - Ok(Certificate { - certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&key_pair)?, - }) - } - - /// Generate a self-signed certificate with the given algorithm. - /// - /// See [`rcgen::Certificate::from_params`]. - pub fn generate_self_signed_with_alg( - subject_alt_names: impl Into>, - alg: &'static rcgen::SignatureAlgorithm, - ) -> Result { - let params = rcgen::CertificateParams::new(subject_alt_names).unwrap(); - let key_pair = rcgen::KeyPair::generate_for(alg).unwrap(); - let cert = params.self_signed(&key_pair).unwrap(); - - Ok(Certificate { - certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&key_pair)?, - }) - } - - /// Parses a certificate from the ASCII PEM format. - #[cfg(feature = "pem")] - pub fn from_pem(pem_str: &str) -> Result { - let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; - if pems.len() < 2 { - return Err(Error::InvalidPEM(format!( - "expected at least two PEM blocks, got {}", - pems.len() - ))); - } - if pems[0].tag() != "PRIVATE_KEY" { - return Err(Error::InvalidPEM(format!( - "invalid tag (expected: 'PRIVATE_KEY', got: '{}')", - pems[0].tag() - ))); - } - - let keypair = KeyPair::try_from(pems[0].contents()) - .map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?; - - let mut rustls_certs = Vec::new(); - for p in pems.drain(1..) { - if p.tag() != "CERTIFICATE" { - return Err(Error::InvalidPEM(format!( - "invalid tag (expected: 'CERTIFICATE', got: '{}')", - p.tag() - ))); - } - rustls_certs.push(CertificateDer::from(p.contents().to_vec())); - } - - Ok(Certificate { - certificate: rustls_certs, - private_key: CryptoPrivateKey::try_from(&keypair)?, - }) - } - - /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. - #[cfg(feature = "pem")] - pub fn serialize_pem(&self) -> String { - let mut data = vec![pem::Pem::new( - "PRIVATE_KEY".to_string(), - self.private_key.serialized_der.clone(), - )]; - for rustls_cert in &self.certificate { - data.push(pem::Pem::new( - "CERTIFICATE".to_string(), - rustls_cert.as_ref(), - )); - } - pem::encode_many(&data) - } -} - -pub(crate) fn value_key_message( - client_random: &[u8], - server_random: &[u8], - public_key: &[u8], - named_curve: NamedCurve, -) -> Vec { - let mut server_ecdh_params = vec![0u8; 4]; - server_ecdh_params[0] = 3; // named curve - server_ecdh_params[1..3].copy_from_slice(&(named_curve as u16).to_be_bytes()); - server_ecdh_params[3] = public_key.len() as u8; - - let mut plaintext = vec![]; - plaintext.extend_from_slice(client_random); - plaintext.extend_from_slice(server_random); - plaintext.extend_from_slice(&server_ecdh_params); - plaintext.extend_from_slice(public_key); - - plaintext -} - -/// Either ED25519, ECDSA or RSA keypair. -#[derive(Debug)] -pub enum CryptoPrivateKeyKind { - Ed25519(Ed25519KeyPair), - Ecdsa256(EcdsaKeyPair), - Rsa256(ring::rsa::KeyPair), -} - -/// Private key. -#[derive(Debug)] -pub struct CryptoPrivateKey { - /// Keypair. - pub kind: CryptoPrivateKeyKind, - /// DER-encoded keypair. - pub serialized_der: Vec, -} - -impl PartialEq for CryptoPrivateKey { - fn eq(&self, other: &Self) -> bool { - if self.serialized_der != other.serialized_der { - return false; - } - - matches!( - (&self.kind, &other.kind), - ( - CryptoPrivateKeyKind::Rsa256(_), - CryptoPrivateKeyKind::Rsa256(_) - ) | ( - CryptoPrivateKeyKind::Ecdsa256(_), - CryptoPrivateKeyKind::Ecdsa256(_) - ) | ( - CryptoPrivateKeyKind::Ed25519(_), - CryptoPrivateKeyKind::Ed25519(_) - ) - ) - } -} - -impl Clone for CryptoPrivateKey { - fn clone(&self) -> Self { - match self.kind { - CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &self.serialized_der, - &SystemRandom::new(), - ) - .unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - } - } -} - -impl TryFrom<&KeyPair> for CryptoPrivateKey { - type Error = Error; - - fn try_from(key_pair: &KeyPair) -> Result { - Self::from_key_pair(key_pair) - } -} - -impl CryptoPrivateKey { - pub fn from_key_pair(key_pair: &KeyPair) -> Result { - let serialized_der = key_pair.serialize_der(); - if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) - } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - &SystemRandom::new(), - ) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) - } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) - } else { - Err(Error::Other("Unsupported key_pair".to_owned())) - } - } -} - -// If the client provided a "signature_algorithms" extension, then all -// certificates provided by the server MUST be signed by a -// hash/signature algorithm pair that appears in that extension -// -// https://tools.ietf.org/html/rfc5246#section-7.4.2 -pub(crate) fn generate_key_signature( - client_random: &[u8], - server_random: &[u8], - public_key: &[u8], - named_curve: NamedCurve, - private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/ -) -> Result> { - let msg = value_key_message(client_random, server_random, public_key, named_curve); - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, &msg) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - let mut signature = vec![0; kp.public().modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - &msg, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - }; - - Ok(signature) -} - -// add OID_ED25519 which is not defined in x509_parser -pub const OID_ED25519: Oid<'static> = oid!(1.3.101 .112); -pub const OID_ECDSA: Oid<'static> = oid!(1.2.840 .10045 .2 .1); - -fn verify_signature( - message: &[u8], - hash_algorithm: &SignatureHashAlgorithm, - remote_key_signature: &[u8], - raw_certificates: &[Vec], - insecure_verification: bool, -) -> Result<()> { - if raw_certificates.is_empty() { - return Err(Error::ErrLengthMismatch); - } - - let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0]) - .map_err(|e| Error::Other(e.to_string()))?; - - let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature { - SignatureAlgorithm::Ed25519 => &ring::signature::ED25519, - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => { - &ring::signature::ECDSA_P256_SHA256_ASN1 - } - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::ECDSA_P384_SHA384_ASN1 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => { - &ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY - } - SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA256 - } - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::RSA_PKCS1_2048_8192_SHA384 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA512 - } - } - _ => return Err(Error::ErrKeySignatureVerifyUnimplemented), - }; - - log::trace!("Picked an algorithm {:?}", verify_alg); - - let public_key = ring::signature::UnparsedPublicKey::new( - verify_alg, - certificate - .tbs_certificate - .subject_pki - .subject_public_key - .data, - ); - - public_key - .verify(message, remote_key_signature) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) -} - -pub(crate) fn verify_key_signature( - message: &[u8], - hash_algorithm: &SignatureHashAlgorithm, - remote_key_signature: &[u8], - raw_certificates: &[Vec], - insecure_verification: bool, -) -> Result<()> { - verify_signature( - message, - hash_algorithm, - remote_key_signature, - raw_certificates, - insecure_verification, - ) -} - -// If the server has sent a CertificateRequest message, the client MUST send the Certificate -// message. The ClientKeyExchange message is now sent, and the content -// of that message will depend on the public key algorithm selected -// between the ClientHello and the ServerHello. If the client has sent -// a certificate with signing ability, a digitally-signed -// CertificateVerify message is sent to explicitly verify possession of -// the private key in the certificate. -// https://tools.ietf.org/html/rfc5246#section-7.3 -pub(crate) fn generate_certificate_verify( - handshake_bodies: &[u8], - private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/ -) -> Result> { - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, handshake_bodies) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - let mut signature = vec![0; kp.public().modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - handshake_bodies, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - }; - - Ok(signature) -} - -pub(crate) fn verify_certificate_verify( - handshake_bodies: &[u8], - hash_algorithm: &SignatureHashAlgorithm, - remote_key_signature: &[u8], - raw_certificates: &[Vec], - insecure_verification: bool, -) -> Result<()> { - verify_signature( - handshake_bodies, - hash_algorithm, - remote_key_signature, - raw_certificates, - insecure_verification, - ) -} - -pub(crate) fn load_certs(raw_certificates: &[Vec]) -> Result>> { - if raw_certificates.is_empty() { - return Err(Error::ErrLengthMismatch); - } - - let mut certs = vec![]; - for raw_cert in raw_certificates { - let cert = CertificateDer::from(raw_cert.to_vec()); - certs.push(cert); - } - - Ok(certs) -} - -pub(crate) fn verify_client_cert( - raw_certificates: &[Vec], - cert_verifier: &Arc, -) -> Result>> { - let chains = load_certs(raw_certificates)?; - - let (end_entity, intermediates) = chains - .split_first() - .ok_or(Error::ErrClientCertificateRequired)?; - - match cert_verifier.verify_client_cert( - end_entity, - intermediates, - rustls::pki_types::UnixTime::now(), - ) { - Ok(_) => {} - Err(err) => return Err(Error::Other(err.to_string())), - }; - - Ok(chains) -} - -pub(crate) fn verify_server_cert( - raw_certificates: &[Vec], - cert_verifier: &Arc, - server_name: &str, -) -> Result>> { - let chains = load_certs(raw_certificates)?; - let server_name = match ServerName::try_from(server_name) { - Ok(server_name) => server_name, - Err(err) => return Err(Error::Other(err.to_string())), - }; - - let (end_entity, intermediates) = chains - .split_first() - .ok_or(Error::ErrServerMustHaveCertificate)?; - match cert_verifier.verify_server_cert( - end_entity, - intermediates, - &server_name, - &[], - rustls::pki_types::UnixTime::now(), - ) { - Ok(_) => {} - Err(err) => return Err(Error::Other(err.to_string())), - }; - - Ok(chains) -} - -pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: usize) -> Vec { - let mut additional_data = vec![0u8; 13]; - // SequenceNumber MUST be set first - // we only want uint48, clobbering an extra 2 (using uint64, rust doesn't have uint48) - additional_data[..8].copy_from_slice(&h.sequence_number.to_be_bytes()); - additional_data[..2].copy_from_slice(&h.epoch.to_be_bytes()); - additional_data[8] = h.content_type as u8; - additional_data[9] = h.protocol_version.major; - additional_data[10] = h.protocol_version.minor; - additional_data[11..].copy_from_slice(&(payload_len as u16).to_be_bytes()); - - additional_data -} - -#[cfg(test)] -mod test { - #[cfg(feature = "pem")] - use super::*; - - #[cfg(feature = "pem")] - #[test] - fn test_certificate_serialize_pem_and_from_pem() -> crate::error::Result<()> { - let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?; - - let pem = cert.serialize_pem(); - let loaded_cert = Certificate::from_pem(&pem)?; - - assert_eq!(loaded_cert, cert); - - Ok(()) - } -} diff --git a/dtls/src/crypto/padding.rs b/dtls/src/crypto/padding.rs deleted file mode 100644 index 7c5c82d74..000000000 --- a/dtls/src/crypto/padding.rs +++ /dev/null @@ -1,122 +0,0 @@ -use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError}; -use core::panic; - -pub enum DtlsPadding {} -/// Reference: RFC5246, 6.2.3.2 -impl RawPadding for DtlsPadding { - const TYPE: PadType = PadType::Reversible; - - fn raw_pad(block: &mut [u8], pos: usize) { - if pos >= block.len() { - panic!("`pos` is bigger or equal to block size"); - } - - let padding_length = block.len() - pos - 1; - if padding_length > 255 { - panic!("block size is too big for DTLS"); - } - - set(&mut block[pos..], padding_length as u8); - } - - fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> { - let padding_length = data.last().copied().unwrap_or(1) as usize; - if padding_length + 1 > data.len() { - return Err(UnpadError); - } - - let padding_begin = data.len() - padding_length - 1; - - if data[padding_begin..data.len() - 1] - .iter() - .any(|&byte| byte as usize != padding_length) - { - return Err(UnpadError); - } - - Ok(&data[0..padding_begin]) - } -} - -/// Sets all bytes in `dst` equal to `value` -#[inline(always)] -fn set(dst: &mut [u8], value: u8) { - // SAFETY: we overwrite valid memory behind `dst` - // note: loop is not used here because it produces - // unnecessary branch which tests for zero-length slices - unsafe { - core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len()); - } -} - -#[cfg(test)] -pub mod tests { - use rand::Rng; - - use super::*; - - #[test] - fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> { - for original_length in 0..128 { - for padding_length in 0..(256 - original_length) { - let mut block = vec![0; original_length + padding_length + 1]; - rand::thread_rng().fill(&mut block[0..original_length]); - let original = block[0..original_length].to_vec(); - DtlsPadding::raw_pad(&mut block, original_length); - - for byte in block[original_length..].iter() { - assert_eq!(*byte as usize, padding_length); - } - assert_eq!(block[0..original_length], original); - } - } - - Ok(()) - } - - #[test] - #[should_panic] - fn full_block_is_padding_error() { - for original_length in 0..256 { - let mut block = vec![0; original_length]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - #[should_panic] - fn padding_length_bigger_than_255_is_a_pad_error() { - let padding_length = 256; - for original_length in 0..128 { - let mut block = vec![0; original_length + padding_length + 1]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - fn empty_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[]); - assert!(r.is_err()); - } - - #[test] - fn padding_too_big_for_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[1]); - assert!(r.is_err()); - } - - #[test] - fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() { - for padding_length in 0..16 { - for invalid_byte in 0..padding_length { - let mut block = vec![0; padding_length + 1]; - DtlsPadding::raw_pad(&mut block, 0); - - assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..])); - block[invalid_byte] = (padding_length - 1) as u8; - let r = DtlsPadding::raw_unpad(&block); - assert!(r.is_err()); - } - } - } -} diff --git a/dtls/src/curve/mod.rs b/dtls/src/curve/mod.rs deleted file mode 100644 index dd48d5ff2..000000000 --- a/dtls/src/curve/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod named_curve; - -// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10 -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum EllipticCurveType { - NamedCurve = 0x03, - Unsupported, -} - -impl From for EllipticCurveType { - fn from(val: u8) -> Self { - match val { - 0x03 => EllipticCurveType::NamedCurve, - _ => EllipticCurveType::Unsupported, - } - } -} diff --git a/dtls/src/curve/named_curve.rs b/dtls/src/curve/named_curve.rs deleted file mode 100644 index 6d7d97f8e..000000000 --- a/dtls/src/curve/named_curve.rs +++ /dev/null @@ -1,83 +0,0 @@ -use rand_core::OsRng; // requires 'getrandom' feature - -use crate::error::*; - -// https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 -#[repr(u16)] -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum NamedCurve { - Unsupported = 0x0000, - P256 = 0x0017, - P384 = 0x0018, - X25519 = 0x001d, -} - -impl From for NamedCurve { - fn from(val: u16) -> Self { - match val { - 0x0017 => NamedCurve::P256, - 0x0018 => NamedCurve::P384, - 0x001d => NamedCurve::X25519, - _ => NamedCurve::Unsupported, - } - } -} - -pub(crate) enum NamedCurvePrivateKey { - EphemeralSecretP256(p256::ecdh::EphemeralSecret), - EphemeralSecretP384(p384::ecdh::EphemeralSecret), - StaticSecretX25519(x25519_dalek::StaticSecret), -} - -pub struct NamedCurveKeypair { - pub(crate) curve: NamedCurve, - pub(crate) public_key: Vec, - pub(crate) private_key: NamedCurvePrivateKey, -} - -fn elliptic_curve_keypair(curve: NamedCurve) -> Result { - let (public_key, private_key) = match curve { - NamedCurve::P256 => { - let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p256::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP256(secret_key), - ) - } - NamedCurve::P384 => { - let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p384::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP384(secret_key), - ) - } - NamedCurve::X25519 => { - let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng); - let public_key = x25519_dalek::PublicKey::from(&secret_key); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::StaticSecretX25519(secret_key), - ) - } - _ => return Err(Error::ErrInvalidNamedCurve), - }; - - Ok(NamedCurveKeypair { - curve, - public_key, - private_key, - }) -} - -impl NamedCurve { - pub fn generate_keypair(&self) -> Result { - match *self { - NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519), - NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256), - NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384), - _ => Err(Error::ErrInvalidNamedCurve), - } - } -} diff --git a/dtls/src/error.rs b/dtls/src/error.rs deleted file mode 100644 index a90d9fa8b..000000000 --- a/dtls/src/error.rs +++ /dev/null @@ -1,223 +0,0 @@ -use std::io; -use std::string::FromUtf8Error; - -use thiserror::Error; -use tokio::sync::mpsc::error::SendError as MpscSendError; -use util::KeyingMaterialExporterError; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("conn is closed")] - ErrConnClosed, - #[error("read/write timeout")] - ErrDeadlineExceeded, - #[error("buffer is too small")] - ErrBufferTooSmall, - #[error("context is not supported for export_keying_material")] - ErrContextUnsupported, - #[error("packet is too short")] - ErrDtlspacketInvalidLength, - #[error("handshake is in progress")] - ErrHandshakeInProgress, - #[error("invalid content type")] - ErrInvalidContentType, - #[error("invalid mac")] - ErrInvalidMac, - #[error("packet length and declared length do not match")] - ErrInvalidPacketLength, - #[error("export_keying_material can not be used with a reserved label")] - ErrReservedExportKeyingMaterial, - #[error("client sent certificate verify but we have no certificate to verify")] - ErrCertificateVerifyNoCertificate, - #[error("client+server do not support any shared cipher suites")] - ErrCipherSuiteNoIntersection, - #[error("server hello can not be created without a cipher suite")] - ErrCipherSuiteUnset, - #[error("client sent certificate but did not verify it")] - ErrClientCertificateNotVerified, - #[error("server required client verification, but got none")] - ErrClientCertificateRequired, - #[error("server responded with SRTP Profile we do not support")] - ErrClientNoMatchingSrtpProfile, - #[error("client required Extended Master Secret extension, but server does not support it")] - ErrClientRequiredButNoServerEms, - #[error("server hello can not be created without a compression method")] - ErrCompressionMethodUnset, - #[error("client+server cookie does not match")] - ErrCookieMismatch, - #[error("cookie must not be longer then 255 bytes")] - ErrCookieTooLong, - #[error("PSK Identity Hint provided but PSK is nil")] - ErrIdentityNoPsk, - #[error("no certificate provided")] - ErrInvalidCertificate, - #[error("cipher spec invalid")] - ErrInvalidCipherSpec, - #[error("invalid or unknown cipher suite")] - ErrInvalidCipherSuite, - #[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")] - ErrInvalidClientKeyExchange, - #[error("invalid or unknown compression method")] - ErrInvalidCompressionMethod, - #[error("ECDSA signature contained zero or negative values")] - ErrInvalidEcdsasignature, - #[error("invalid or unknown elliptic curve type")] - ErrInvalidEllipticCurveType, - #[error("invalid extension type")] - ErrInvalidExtensionType, - #[error("invalid hash algorithm")] - ErrInvalidHashAlgorithm, - #[error("invalid named curve")] - ErrInvalidNamedCurve, - #[error("invalid private key type")] - ErrInvalidPrivateKey, - #[error("named curve and private key type does not match")] - ErrNamedCurveAndPrivateKeyMismatch, - #[error("invalid server name format")] - ErrInvalidSniFormat, - #[error("invalid signature algorithm")] - ErrInvalidSignatureAlgorithm, - #[error("expected and actual key signature do not match")] - ErrKeySignatureMismatch, - #[error("Conn can not be created with a nil nextConn")] - ErrNilNextConn, - #[error("connection can not be created, no CipherSuites satisfy this Config")] - ErrNoAvailableCipherSuites, - #[error("connection can not be created, no SignatureScheme satisfy this Config")] - ErrNoAvailableSignatureSchemes, - #[error("no certificates configured")] - ErrNoCertificates, - #[error("no config provided")] - ErrNoConfigProvided, - #[error("client requested zero or more elliptic curves that are not supported by the server")] - ErrNoSupportedEllipticCurves, - #[error("unsupported protocol version")] - ErrUnsupportedProtocolVersion, - #[error("Certificate and PSK provided")] - ErrPskAndCertificate, - #[error("PSK and PSK Identity Hint must both be set for client")] - ErrPskAndIdentityMustBeSetForClient, - #[error("SRTP support was requested but server did not respond with use_srtp extension")] - ErrRequestedButNoSrtpExtension, - #[error("Certificate is mandatory for server")] - ErrServerMustHaveCertificate, - #[error("client requested SRTP but we have no matching profiles")] - ErrServerNoMatchingSrtpProfile, - #[error( - "server requires the Extended Master Secret extension, but the client does not support it" - )] - ErrServerRequiredButNoClientEms, - #[error("expected and actual verify data does not match")] - ErrVerifyDataMismatch, - #[error("handshake message unset, unable to marshal")] - ErrHandshakeMessageUnset, - #[error("invalid flight number")] - ErrInvalidFlight, - #[error("unable to generate key signature, unimplemented")] - ErrKeySignatureGenerateUnimplemented, - #[error("unable to verify key signature, unimplemented")] - ErrKeySignatureVerifyUnimplemented, - #[error("data length and declared length do not match")] - ErrLengthMismatch, - #[error("buffer not long enough to contain nonce")] - ErrNotEnoughRoomForNonce, - #[error("feature has not been implemented yet")] - ErrNotImplemented, - #[error("sequence number overflow")] - ErrSequenceNumberOverflow, - #[error("unable to marshal fragmented handshakes")] - ErrUnableToMarshalFragmented, - #[error("invalid state machine transition")] - ErrInvalidFsmTransition, - #[error("ApplicationData with epoch of 0")] - ErrApplicationDataEpochZero, - #[error("unhandled contentType")] - ErrUnhandledContextType, - #[error("context canceled")] - ErrContextCanceled, - #[error("empty fragment")] - ErrEmptyFragment, - #[error("Alert is Fatal or Close Notify")] - ErrAlertFatalOrClose, - - #[error( - "Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}" - )] - ErrFragmentBufferOverflow { new_size: usize, max_size: usize }, - - #[error("{0}")] - Io(#[source] IoError), - #[error("{0}")] - Util(#[from] util::Error), - #[error("utf8: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("{0}")] - Sec1(#[source] sec1::Error), - #[error("{0}")] - Aes(#[from] aes::cipher::InvalidLength), - #[error("{0}")] - P256(#[source] P256Error), - #[error("{0}")] - RcGen(#[from] rcgen::Error), - #[error("mpsc send: {0}")] - MpscSend(String), - #[error("keying material: {0}")] - KeyingMaterial(#[from] KeyingMaterialExporterError), - - /// Error parsing a given PEM string. - #[error("invalid PEM: {0}")] - InvalidPEM(String), - - #[allow(non_camel_case_types)] - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -impl From for Error { - fn from(e: sec1::Error) -> Self { - Error::Sec1(e) - } -} - -#[derive(Debug, Error)] -#[error("{0}")] -pub struct P256Error(#[source] p256::elliptic_curve::Error); - -impl PartialEq for P256Error { - fn eq(&self, _: &Self) -> bool { - false - } -} - -impl From for Error { - fn from(e: p256::elliptic_curve::Error) -> Self { - Error::P256(P256Error(e)) - } -} - -// Because Tokio SendError is parameterized, we sadly lose the backtrace. -impl From> for Error { - fn from(e: MpscSendError) -> Self { - Error::MpscSend(e.to_string()) - } -} diff --git a/dtls/src/extension/extension_server_name.rs b/dtls/src/extension/extension_server_name.rs deleted file mode 100644 index dbe018e9f..000000000 --- a/dtls/src/extension/extension_server_name.rs +++ /dev/null @@ -1,56 +0,0 @@ -#[cfg(test)] -mod extension_server_name_test; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::*; - -const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionServerName { - pub(crate) server_name: String, -} - -impl ExtensionServerName { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::ServerName - } - - pub fn size(&self) -> usize { - //TODO: check how to do cryptobyte? - 2 + 2 + 1 + 2 + self.server_name.as_bytes().len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - //TODO: check how to do cryptobyte? - writer.write_u16::(2 + 1 + 2 + self.server_name.len() as u16)?; - writer.write_u16::(1 + 2 + self.server_name.len() as u16)?; - writer.write_u8(EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME)?; - writer.write_u16::(self.server_name.len() as u16)?; - writer.write_all(self.server_name.as_bytes())?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - //TODO: check how to do cryptobyte? - let _ = reader.read_u16::()? as usize; - let _ = reader.read_u16::()? as usize; - - let name_type = reader.read_u8()?; - if name_type != EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME { - return Err(Error::ErrInvalidSniFormat); - } - - let buf_len = reader.read_u16::()? as usize; - let mut buf: Vec = vec![0u8; buf_len]; - reader.read_exact(&mut buf)?; - - let server_name = String::from_utf8(buf)?; - - Ok(ExtensionServerName { server_name }) - } -} diff --git a/dtls/src/extension/extension_server_name/extension_server_name_test.rs b/dtls/src/extension/extension_server_name/extension_server_name_test.rs deleted file mode 100644 index 4cfb7a1a1..000000000 --- a/dtls/src/extension/extension_server_name/extension_server_name_test.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_server_name() -> Result<()> { - let extension = ExtensionServerName { - server_name: "test.domain".to_owned(), - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - extension.marshal(&mut writer)?; - } - - let mut reader = BufReader::new(raw.as_slice()); - let new_extension = ExtensionServerName::unmarshal(&mut reader)?; - - assert_eq!( - new_extension, extension, - "extensionServerName marshal: got {new_extension:?} expected {extension:?}", - ); - - Ok(()) -} diff --git a/dtls/src/extension/extension_supported_elliptic_curves.rs b/dtls/src/extension/extension_supported_elliptic_curves.rs deleted file mode 100644 index 88caf6f76..000000000 --- a/dtls/src/extension/extension_supported_elliptic_curves.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[cfg(test)] -mod extension_supported_elliptic_curves_test; - -use super::*; -use crate::curve::named_curve::*; - -const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6; - -// https://tools.ietf.org/html/rfc8422#section-5.1.1 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionSupportedEllipticCurves { - pub elliptic_curves: Vec, -} - -impl ExtensionSupportedEllipticCurves { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::SupportedEllipticCurves - } - - pub fn size(&self) -> usize { - 2 + 2 + self.elliptic_curves.len() * 2 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::(2 + 2 * self.elliptic_curves.len() as u16)?; - writer.write_u16::(2 * self.elliptic_curves.len() as u16)?; - for v in &self.elliptic_curves { - writer.write_u16::(*v as u16)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let _ = reader.read_u16::()?; - - let group_count = reader.read_u16::()? as usize / 2; - let mut elliptic_curves = vec![]; - for _ in 0..group_count { - let elliptic_curve = reader.read_u16::()?.into(); - elliptic_curves.push(elliptic_curve); - } - - Ok(ExtensionSupportedEllipticCurves { elliptic_curves }) - } -} diff --git a/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs b/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs deleted file mode 100644 index 2bcf70c17..000000000 --- a/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_supported_groups() -> Result<()> { - let raw_supported_groups = vec![0x0, 0x4, 0x0, 0x2, 0x0, 0x1d]; // 0x0, 0xa, - let parsed_supported_groups = ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::X25519], - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - parsed_supported_groups.marshal(&mut writer)?; - } - - assert_eq!( - raw, raw_supported_groups, - "extensionSupportedGroups marshal: got {raw:?}, want {raw_supported_groups:?}" - ); - - let mut reader = BufReader::new(raw.as_slice()); - let new_supported_groups = ExtensionSupportedEllipticCurves::unmarshal(&mut reader)?; - - assert_eq!( - new_supported_groups, parsed_supported_groups, - "extensionSupportedGroups unmarshal: got {new_supported_groups:?}, want {parsed_supported_groups:?}" - ); - - Ok(()) -} diff --git a/dtls/src/extension/extension_supported_point_formats.rs b/dtls/src/extension/extension_supported_point_formats.rs deleted file mode 100644 index 17e2448af..000000000 --- a/dtls/src/extension/extension_supported_point_formats.rs +++ /dev/null @@ -1,49 +0,0 @@ -#[cfg(test)] -mod extension_supported_point_formats_test; - -use super::*; - -const EXTENSION_SUPPORTED_POINT_FORMATS_SIZE: usize = 5; - -pub type EllipticCurvePointFormat = u8; - -pub const ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED: EllipticCurvePointFormat = 0; - -// https://tools.ietf.org/html/rfc4492#section-5.1.2 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionSupportedPointFormats { - pub(crate) point_formats: Vec, -} - -impl ExtensionSupportedPointFormats { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::SupportedPointFormats - } - - pub fn size(&self) -> usize { - 2 + 1 + self.point_formats.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::(1 + self.point_formats.len() as u16)?; - writer.write_u8(self.point_formats.len() as u8)?; - for v in &self.point_formats { - writer.write_u8(*v)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let _ = reader.read_u16::()?; - - let point_format_count = reader.read_u8()? as usize; - let mut point_formats = vec![]; - for _ in 0..point_format_count { - let point_format = reader.read_u8()?; - point_formats.push(point_format); - } - - Ok(ExtensionSupportedPointFormats { point_formats }) - } -} diff --git a/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs b/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs deleted file mode 100644 index e624a98a1..000000000 --- a/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_supported_point_formats() -> Result<()> { - let raw_extension_supported_point_formats = vec![0x00, 0x02, 0x01, 0x00]; // 0x00, 0x0b, - let parsed_extension_supported_point_formats = ExtensionSupportedPointFormats { - point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - parsed_extension_supported_point_formats.marshal(&mut writer)?; - } - - assert_eq!( - raw, raw_extension_supported_point_formats, - "extensionSupportedPointFormats marshal: got {raw:?}, want {raw_extension_supported_point_formats:?}" - ); - - let mut reader = BufReader::new(raw.as_slice()); - let new_extension_supported_point_formats = - ExtensionSupportedPointFormats::unmarshal(&mut reader)?; - - assert_eq!( - new_extension_supported_point_formats, parsed_extension_supported_point_formats, - "extensionSupportedPointFormats unmarshal: got {new_extension_supported_point_formats:?}, want {parsed_extension_supported_point_formats:?}" - ); - - Ok(()) -} diff --git a/dtls/src/extension/extension_supported_signature_algorithms.rs b/dtls/src/extension/extension_supported_signature_algorithms.rs deleted file mode 100644 index e15210b2e..000000000 --- a/dtls/src/extension/extension_supported_signature_algorithms.rs +++ /dev/null @@ -1,50 +0,0 @@ -#[cfg(test)] -mod extension_supported_signature_algorithms_test; - -use super::*; -use crate::signature_hash_algorithm::*; - -const EXTENSION_SUPPORTED_SIGNATURE_ALGORITHMS_HEADER_SIZE: usize = 6; - -// https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionSupportedSignatureAlgorithms { - pub(crate) signature_hash_algorithms: Vec, -} - -impl ExtensionSupportedSignatureAlgorithms { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::SupportedSignatureAlgorithms - } - - pub fn size(&self) -> usize { - 2 + 2 + self.signature_hash_algorithms.len() * 2 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::(2 + 2 * self.signature_hash_algorithms.len() as u16)?; - writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; - for v in &self.signature_hash_algorithms { - writer.write_u8(v.hash as u8)?; - writer.write_u8(v.signature as u8)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let _ = reader.read_u16::()?; - - let algorithm_count = reader.read_u16::()? as usize / 2; - let mut signature_hash_algorithms = vec![]; - for _ in 0..algorithm_count { - let hash = reader.read_u8()?.into(); - let signature = reader.read_u8()?.into(); - signature_hash_algorithms.push(SignatureHashAlgorithm { hash, signature }); - } - - Ok(ExtensionSupportedSignatureAlgorithms { - signature_hash_algorithms, - }) - } -} diff --git a/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs b/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs deleted file mode 100644 index e531fee03..000000000 --- a/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_supported_signature_algorithms() -> Result<()> { - let raw_extension_supported_signature_algorithms = - vec![0x00, 0x08, 0x00, 0x06, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03]; //0x00, 0x0d, - let parsed_extension_supported_signature_algorithms = ExtensionSupportedSignatureAlgorithms { - signature_hash_algorithms: vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Ecdsa, - }, - ], - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - parsed_extension_supported_signature_algorithms.marshal(&mut writer)?; - } - - assert_eq!( - raw, raw_extension_supported_signature_algorithms, - "extensionSupportedSignatureAlgorithms marshal: got {raw:?}, want {raw_extension_supported_signature_algorithms:?}" - ); - - let mut reader = BufReader::new(raw.as_slice()); - let new_extension_supported_signature_algorithms = - ExtensionSupportedSignatureAlgorithms::unmarshal(&mut reader)?; - - assert_eq!( - new_extension_supported_signature_algorithms, - parsed_extension_supported_signature_algorithms, - "extensionSupportedSignatureAlgorithms unmarshal: got {new_extension_supported_signature_algorithms:?}, want {parsed_extension_supported_signature_algorithms:?}" - ); - - Ok(()) -} diff --git a/dtls/src/extension/extension_use_extended_master_secret.rs b/dtls/src/extension/extension_use_extended_master_secret.rs deleted file mode 100644 index 9d62c4eaa..000000000 --- a/dtls/src/extension/extension_use_extended_master_secret.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[cfg(test)] -mod extension_use_extended_master_secret_test; - -use super::*; - -const EXTENSION_USE_EXTENDED_MASTER_SECRET_HEADER_SIZE: usize = 4; - -// https://tools.ietf.org/html/rfc8422 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionUseExtendedMasterSecret { - pub(crate) supported: bool, -} - -impl ExtensionUseExtendedMasterSecret { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::UseExtendedMasterSecret - } - - pub fn size(&self) -> usize { - 2 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - // length - writer.write_u16::(0)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let _ = reader.read_u16::()?; - - Ok(ExtensionUseExtendedMasterSecret { supported: true }) - } -} diff --git a/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs b/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs deleted file mode 100644 index d2b0dd424..000000000 --- a/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_use_extended_master_secret() -> Result<()> { - let raw_extension_use_extended_master_secret = vec![0x00, 0x00]; - let parsed_extension_use_extended_master_secret = - ExtensionUseExtendedMasterSecret { supported: true }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - parsed_extension_use_extended_master_secret.marshal(&mut writer)?; - } - - assert_eq!( - raw, raw_extension_use_extended_master_secret, - "extension_use_extended_master_secret marshal: got {raw:?}, want {raw_extension_use_extended_master_secret:?}" - ); - - let mut reader = BufReader::new(raw.as_slice()); - let new_extension_use_extended_master_secret = - ExtensionUseExtendedMasterSecret::unmarshal(&mut reader)?; - - assert_eq!( - new_extension_use_extended_master_secret, parsed_extension_use_extended_master_secret, - "extension_use_extended_master_secret unmarshal: got {new_extension_use_extended_master_secret:?}, want {parsed_extension_use_extended_master_secret:?}" - ); - - Ok(()) -} diff --git a/dtls/src/extension/extension_use_srtp.rs b/dtls/src/extension/extension_use_srtp.rs deleted file mode 100644 index b8620d7ea..000000000 --- a/dtls/src/extension/extension_use_srtp.rs +++ /dev/null @@ -1,80 +0,0 @@ -#[cfg(test)] -mod extension_use_srtp_test; - -use super::*; - -// SRTPProtectionProfile defines the parameters and options that are in effect for the SRTP processing -// https://tools.ietf.org/html/rfc5764#section-4.1.2 -#[allow(non_camel_case_types)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum SrtpProtectionProfile { - Srtp_Aes128_Cm_Hmac_Sha1_80 = 0x0001, - Srtp_Aes128_Cm_Hmac_Sha1_32 = 0x0002, - Srtp_Aead_Aes_128_Gcm = 0x0007, - Srtp_Aead_Aes_256_Gcm = 0x0008, - Unsupported, -} - -impl From for SrtpProtectionProfile { - fn from(val: u16) -> Self { - match val { - 0x0001 => SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - 0x0002 => SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, - 0x0007 => SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, - 0x0008 => SrtpProtectionProfile::Srtp_Aead_Aes_256_Gcm, - _ => SrtpProtectionProfile::Unsupported, - } - } -} - -const EXTENSION_USE_SRTPHEADER_SIZE: usize = 6; - -// https://tools.ietf.org/html/rfc8422 -#[allow(non_camel_case_types)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionUseSrtp { - pub(crate) protection_profiles: Vec, -} - -impl ExtensionUseSrtp { - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::UseSrtp - } - - pub fn size(&self) -> usize { - 2 + 2 + self.protection_profiles.len() * 2 + 1 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::( - 2 + /* MKI Length */ 1 + 2 * self.protection_profiles.len() as u16, - )?; - writer.write_u16::(2 * self.protection_profiles.len() as u16)?; - for v in &self.protection_profiles { - writer.write_u16::(*v as u16)?; - } - - /* MKI Length */ - writer.write_u8(0x00)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let _ = reader.read_u16::()?; - - let profile_count = reader.read_u16::()? as usize / 2; - let mut protection_profiles = vec![]; - for _ in 0..profile_count { - let protection_profile = reader.read_u16::()?.into(); - protection_profiles.push(protection_profile); - } - - /* MKI Length */ - let _ = reader.read_u8()?; - - Ok(ExtensionUseSrtp { - protection_profiles, - }) - } -} diff --git a/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs b/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs deleted file mode 100644 index c58ba56a9..000000000 --- a/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_extension_use_srtp() -> Result<()> { - let raw_use_srtp = vec![0x00, 0x05, 0x00, 0x02, 0x00, 0x01, 0x00]; //0x00, 0x0e, - let parsed_use_srtp = ExtensionUseSrtp { - protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - parsed_use_srtp.marshal(&mut writer)?; - } - - assert_eq!( - raw, raw_use_srtp, - "extensionUseSRTP marshal: got {raw:?}, want {raw_use_srtp:?}" - ); - - let mut reader = BufReader::new(raw.as_slice()); - let new_use_srtp = ExtensionUseSrtp::unmarshal(&mut reader)?; - - assert_eq!( - new_use_srtp, parsed_use_srtp, - "extensionUseSRTP unmarshal: got {new_use_srtp:?}, want {parsed_use_srtp:?}" - ); - - Ok(()) -} diff --git a/dtls/src/extension/mod.rs b/dtls/src/extension/mod.rs deleted file mode 100644 index 203003810..000000000 --- a/dtls/src/extension/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -pub mod extension_server_name; -pub mod extension_supported_elliptic_curves; -pub mod extension_supported_point_formats; -pub mod extension_supported_signature_algorithms; -pub mod extension_use_extended_master_secret; -pub mod extension_use_srtp; -pub mod renegotiation_info; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use extension_server_name::*; -use extension_supported_elliptic_curves::*; -use extension_supported_point_formats::*; -use extension_supported_signature_algorithms::*; -use extension_use_extended_master_secret::*; -use extension_use_srtp::*; - -use crate::error::*; -use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; - -// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ExtensionValue { - ServerName = 0, - SupportedEllipticCurves = 10, - SupportedPointFormats = 11, - SupportedSignatureAlgorithms = 13, - UseSrtp = 14, - UseExtendedMasterSecret = 23, - RenegotiationInfo = 65281, - Unsupported, -} - -impl From for ExtensionValue { - fn from(val: u16) -> Self { - match val { - 0 => ExtensionValue::ServerName, - 10 => ExtensionValue::SupportedEllipticCurves, - 11 => ExtensionValue::SupportedPointFormats, - 13 => ExtensionValue::SupportedSignatureAlgorithms, - 14 => ExtensionValue::UseSrtp, - 23 => ExtensionValue::UseExtendedMasterSecret, - 65281 => ExtensionValue::RenegotiationInfo, - _ => ExtensionValue::Unsupported, - } - } -} - -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Extension { - ServerName(ExtensionServerName), - SupportedEllipticCurves(ExtensionSupportedEllipticCurves), - SupportedPointFormats(ExtensionSupportedPointFormats), - SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms), - UseSrtp(ExtensionUseSrtp), - UseExtendedMasterSecret(ExtensionUseExtendedMasterSecret), - RenegotiationInfo(ExtensionRenegotiationInfo), -} - -impl Extension { - pub fn extension_value(&self) -> ExtensionValue { - match self { - Extension::ServerName(ext) => ext.extension_value(), - Extension::SupportedEllipticCurves(ext) => ext.extension_value(), - Extension::SupportedPointFormats(ext) => ext.extension_value(), - Extension::SupportedSignatureAlgorithms(ext) => ext.extension_value(), - Extension::UseSrtp(ext) => ext.extension_value(), - Extension::UseExtendedMasterSecret(ext) => ext.extension_value(), - Extension::RenegotiationInfo(ext) => ext.extension_value(), - } - } - - pub fn size(&self) -> usize { - let mut len = 2; - - len += match self { - Extension::ServerName(ext) => ext.size(), - Extension::SupportedEllipticCurves(ext) => ext.size(), - Extension::SupportedPointFormats(ext) => ext.size(), - Extension::SupportedSignatureAlgorithms(ext) => ext.size(), - Extension::UseSrtp(ext) => ext.size(), - Extension::UseExtendedMasterSecret(ext) => ext.size(), - Extension::RenegotiationInfo(ext) => ext.size(), - }; - - len - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::(self.extension_value() as u16)?; - match self { - Extension::ServerName(ext) => ext.marshal(writer), - Extension::SupportedEllipticCurves(ext) => ext.marshal(writer), - Extension::SupportedPointFormats(ext) => ext.marshal(writer), - Extension::SupportedSignatureAlgorithms(ext) => ext.marshal(writer), - Extension::UseSrtp(ext) => ext.marshal(writer), - Extension::UseExtendedMasterSecret(ext) => ext.marshal(writer), - Extension::RenegotiationInfo(ext) => ext.marshal(writer), - } - } - - pub fn unmarshal(reader: &mut R) -> Result { - let extension_value: ExtensionValue = reader.read_u16::()?.into(); - match extension_value { - ExtensionValue::ServerName => Ok(Extension::ServerName( - ExtensionServerName::unmarshal(reader)?, - )), - ExtensionValue::SupportedEllipticCurves => Ok(Extension::SupportedEllipticCurves( - ExtensionSupportedEllipticCurves::unmarshal(reader)?, - )), - ExtensionValue::SupportedPointFormats => Ok(Extension::SupportedPointFormats( - ExtensionSupportedPointFormats::unmarshal(reader)?, - )), - ExtensionValue::SupportedSignatureAlgorithms => { - Ok(Extension::SupportedSignatureAlgorithms( - ExtensionSupportedSignatureAlgorithms::unmarshal(reader)?, - )) - } - ExtensionValue::UseSrtp => Ok(Extension::UseSrtp(ExtensionUseSrtp::unmarshal(reader)?)), - ExtensionValue::UseExtendedMasterSecret => Ok(Extension::UseExtendedMasterSecret( - ExtensionUseExtendedMasterSecret::unmarshal(reader)?, - )), - ExtensionValue::RenegotiationInfo => Ok(Extension::RenegotiationInfo( - ExtensionRenegotiationInfo::unmarshal(reader)?, - )), - _ => Err(Error::ErrInvalidExtensionType), - } - } -} diff --git a/dtls/src/extension/renegotiation_info.rs b/dtls/src/extension/renegotiation_info.rs deleted file mode 100644 index ab3d29ab8..000000000 --- a/dtls/src/extension/renegotiation_info.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[cfg(test)] -mod renegotiation_info_test; - -use super::*; -use crate::error::Error::ErrInvalidPacketLength; - -const RENEGOTIATION_INFO_HEADER_SIZE: usize = 5; - -/// RenegotiationInfo allows a Client/Server to -/// communicate their renegotiation support -/// https://tools.ietf.org/html/rfc5746 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExtensionRenegotiationInfo { - pub(crate) renegotiated_connection: u8, -} - -impl ExtensionRenegotiationInfo { - // TypeValue returns the extension TypeValue - pub fn extension_value(&self) -> ExtensionValue { - ExtensionValue::RenegotiationInfo - } - - pub fn size(&self) -> usize { - 3 - } - - /// marshal encodes the extension - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u16::(1)?; //length - writer.write_u8(self.renegotiated_connection)?; - - Ok(writer.flush()?) - } - - /// Unmarshal populates the extension from encoded data - pub fn unmarshal(reader: &mut R) -> Result { - let l = reader.read_u16::()?; //length - if l != 1 { - return Err(ErrInvalidPacketLength); - } - - let renegotiated_connection = reader.read_u8()?; - - Ok(ExtensionRenegotiationInfo { - renegotiated_connection, - }) - } -} diff --git a/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs b/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs deleted file mode 100644 index 5266a94c3..000000000 --- a/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_renegotiation_info() -> Result<()> { - let extension = ExtensionRenegotiationInfo { - renegotiated_connection: 0, - }; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - extension.marshal(&mut writer)?; - } - - let mut reader = BufReader::new(raw.as_slice()); - let new_extension = ExtensionRenegotiationInfo::unmarshal(&mut reader)?; - - assert_eq!( - new_extension.renegotiated_connection, - extension.renegotiated_connection - ); - - Ok(()) -} diff --git a/dtls/src/flight/flight0.rs b/dtls/src/flight/flight0.rs deleted file mode 100644 index 834d7c098..000000000 --- a/dtls/src/flight/flight0.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::fmt; -use std::sync::atomic::Ordering; - -use async_trait::async_trait; -use rand::Rng; - -use super::flight2::*; -use super::*; -use crate::config::*; -use crate::conn::*; -use crate::error::Error; -use crate::extension::*; -use crate::handshake::*; -use crate::record_layer::record_layer_header::*; -use crate::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight0; - -impl fmt::Display for Flight0 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 0") - } -} - -#[async_trait] -impl Flight for Flight0 { - async fn parse( - &self, - _tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let (seq, msgs) = match cache - .full_pull_map( - 0, - &[HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }], - ) - .await - { - Ok((seq, msgs)) => (seq, msgs), - Err(_) => return Err((None, None)), - }; - - state.handshake_recv_sequence = seq; - - if let Some(message) = msgs.get(&HandshakeType::ClientHello) { - // Validate type - let client_hello = match message { - HandshakeMessage::ClientHello(client_hello) => client_hello, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if client_hello.version != PROTOCOL_VERSION1_2 { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::ProtocolVersion, - }), - Some(Error::ErrUnsupportedProtocolVersion), - )); - } - - state.remote_random = client_hello.random.clone(); - - if let Ok(id) = - find_matching_cipher_suite(&client_hello.cipher_suites, &cfg.local_cipher_suites) - { - if let Ok(cipher_suite) = cipher_suite_for_id(id) { - log::debug!( - "[handshake:{}] use cipher suite: {}", - srv_cli_str(state.is_client), - cipher_suite.to_string() - ); - let mut cs = state.cipher_suite.lock().await; - *cs = Some(cipher_suite); - } - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrCipherSuiteNoIntersection), - )); - } - - for extension in &client_hello.extensions { - match extension { - Extension::SupportedEllipticCurves(e) => { - if e.elliptic_curves.is_empty() { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrNoSupportedEllipticCurves), - )); - } - state.named_curve = e.elliptic_curves[0]; - } - Extension::UseSrtp(e) => { - if let Ok(profile) = find_matching_srtp_profile( - &e.protection_profiles, - &cfg.local_srtp_protection_profiles, - ) { - state.srtp_protection_profile = profile; - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrServerNoMatchingSrtpProfile), - )); - } - } - Extension::UseExtendedMasterSecret(_) => { - if cfg.extended_master_secret != ExtendedMasterSecretType::Disable { - state.extended_master_secret = true; - } - } - Extension::ServerName(e) => { - state.server_name.clone_from(&e.server_name); // remote server name - } - _ => {} - } - } - - if cfg.extended_master_secret == ExtendedMasterSecretType::Require - && !state.extended_master_secret - { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrServerRequiredButNoClientEms), - )); - } - - if state.local_keypair.is_none() { - state.local_keypair = match state.named_curve.generate_keypair() { - Ok(local_keypar) => Some(local_keypar), - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::IllegalParameter, - }), - Some(err), - )) - } - }; - } - - Ok(Box::new(Flight2 {})) - } else { - Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - } - - async fn generate( - &self, - state: &mut State, - _cache: &HandshakeCache, - _cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - // Initialize - state.cookie = vec![0; COOKIE_LENGTH]; - rand::thread_rng().fill(state.cookie.as_mut_slice()); - - //TODO: figure out difference between golang's atom store and rust atom store - let zero_epoch = 0; - state.local_epoch.store(zero_epoch, Ordering::SeqCst); - state.remote_epoch.store(zero_epoch, Ordering::SeqCst); - - state.named_curve = DEFAULT_NAMED_CURVE; - state.local_random.populate(); - - Ok(vec![]) - } -} diff --git a/dtls/src/flight/flight1.rs b/dtls/src/flight/flight1.rs deleted file mode 100644 index 0cb37e62f..000000000 --- a/dtls/src/flight/flight1.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::fmt; -use std::sync::atomic::Ordering; - -use async_trait::async_trait; - -use super::flight3::*; -use super::*; -use crate::compression_methods::*; -use crate::config::*; -use crate::conn::*; -use crate::content::*; -use crate::curve::named_curve::*; -use crate::error::Error; -use crate::extension::extension_server_name::*; -use crate::extension::extension_supported_elliptic_curves::*; -use crate::extension::extension_supported_point_formats::*; -use crate::extension::extension_supported_signature_algorithms::*; -use crate::extension::extension_use_extended_master_secret::*; -use crate::extension::extension_use_srtp::*; -use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; -use crate::extension::*; -use crate::handshake::handshake_message_client_hello::*; -use crate::handshake::*; -use crate::record_layer::record_layer_header::*; -use crate::record_layer::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight1; - -impl fmt::Display for Flight1 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 1") - } -} - -#[async_trait] -impl Flight for Flight1 { - async fn parse( - &self, - tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - // HelloVerifyRequest can be skipped by the server, - // so allow ServerHello during flight1 also - let (seq, msgs) = match cache - .full_pull_map( - state.handshake_recv_sequence, - &[ - HandshakeCachePullRule { - typ: HandshakeType::HelloVerifyRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }, - ], - ) - .await - { - // No valid message received. Keep reading - Ok((seq, msgs)) => (seq, msgs), - Err(_) => return Err((None, None)), - }; - - if msgs.contains_key(&HandshakeType::ServerHello) { - // Flight1 and flight2 were skipped. - // Parse as flight3. - let flight3 = Flight3 {}; - return flight3.parse(tx, state, cache, cfg).await; - } - - if let Some(message) = msgs.get(&HandshakeType::HelloVerifyRequest) { - // DTLS 1.2 clients must not assume that the server will use the protocol version - // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 - let h = match message { - HandshakeMessage::HelloVerifyRequest(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if h.version != PROTOCOL_VERSION1_0 && h.version != PROTOCOL_VERSION1_2 { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::ProtocolVersion, - }), - Some(Error::ErrUnsupportedProtocolVersion), - )); - } - - state.cookie.clone_from(&h.cookie); - state.handshake_recv_sequence = seq; - Ok(Box::new(Flight3 {})) - } else { - Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - } - - async fn generate( - &self, - state: &mut State, - _cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let zero_epoch = 0; - state.local_epoch.store(zero_epoch, Ordering::SeqCst); - state.remote_epoch.store(zero_epoch, Ordering::SeqCst); - - state.named_curve = DEFAULT_NAMED_CURVE; - state.cookie = vec![]; - state.local_random.populate(); - - let mut extensions = vec![ - Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { - signature_hash_algorithms: cfg.local_signature_schemes.clone(), - }), - Extension::RenegotiationInfo(ExtensionRenegotiationInfo { - renegotiated_connection: 0, - }), - ]; - - if cfg.local_psk_callback.is_none() { - extensions.extend_from_slice(&[ - Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], - }), - Extension::SupportedPointFormats(ExtensionSupportedPointFormats { - point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], - }), - ]); - } - - if !cfg.local_srtp_protection_profiles.is_empty() { - extensions.push(Extension::UseSrtp(ExtensionUseSrtp { - protection_profiles: cfg.local_srtp_protection_profiles.clone(), - })); - } - - if cfg.extended_master_secret == ExtendedMasterSecretType::Request - || cfg.extended_master_secret == ExtendedMasterSecretType::Require - { - extensions.push(Extension::UseExtendedMasterSecret( - ExtensionUseExtendedMasterSecret { supported: true }, - )); - } - - if !cfg.server_name.is_empty() { - extensions.push(Extension::ServerName(ExtensionServerName { - server_name: cfg.server_name.clone(), - })); - } - - Ok(vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - random: state.local_random.clone(), - cookie: state.cookie.clone(), - - cipher_suites: cfg.local_cipher_suites.clone(), - compression_methods: default_compression_methods(), - extensions, - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]) - } -} diff --git a/dtls/src/flight/flight2.rs b/dtls/src/flight/flight2.rs deleted file mode 100644 index 9703c9aaa..000000000 --- a/dtls/src/flight/flight2.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::fmt; - -use async_trait::async_trait; - -use super::flight0::*; -use super::flight4::*; -use super::*; -use crate::content::*; -use crate::error::Error; -use crate::handshake::handshake_message_hello_verify_request::*; -use crate::handshake::*; -use crate::record_layer::record_layer_header::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight2; - -impl fmt::Display for Flight2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 2") - } -} - -#[async_trait] -impl Flight for Flight2 { - fn has_retransmit(&self) -> bool { - false - } - - async fn parse( - &self, - tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let (seq, msgs) = match cache - .full_pull_map( - state.handshake_recv_sequence, - &[HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }], - ) - .await - { - // No valid message received. Keep reading - Ok((seq, msgs)) => (seq, msgs), - - // Client may retransmit the first ClientHello when HelloVerifyRequest is dropped. - // Parse as flight 0 in this case. - Err(_) => return Flight0 {}.parse(tx, state, cache, cfg).await, - }; - - state.handshake_recv_sequence = seq; - - if let Some(message) = msgs.get(&HandshakeType::ClientHello) { - // Validate type - let client_hello = match message { - HandshakeMessage::ClientHello(client_hello) => client_hello, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if client_hello.version != PROTOCOL_VERSION1_2 { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::ProtocolVersion, - }), - Some(Error::ErrUnsupportedProtocolVersion), - )); - } - - if client_hello.cookie.is_empty() { - return Err((None, None)); - } - - if state.cookie != client_hello.cookie { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::AccessDenied, - }), - Some(Error::ErrCookieMismatch), - )); - } - - Ok(Box::new(Flight4 {})) - } else { - Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - } - - async fn generate( - &self, - state: &mut State, - _cache: &HandshakeCache, - _cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - state.handshake_send_sequence = 0; - Ok(vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::HelloVerifyRequest( - HandshakeMessageHelloVerifyRequest { - version: PROTOCOL_VERSION1_2, - cookie: state.cookie.clone(), - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]) - } -} diff --git a/dtls/src/flight/flight3.rs b/dtls/src/flight/flight3.rs deleted file mode 100644 index d84e3bdd0..000000000 --- a/dtls/src/flight/flight3.rs +++ /dev/null @@ -1,470 +0,0 @@ -use std::fmt; - -use async_trait::async_trait; -use log::*; - -use super::flight5::*; -use super::*; -use crate::cipher_suite::cipher_suite_for_id; -use crate::compression_methods::*; -use crate::config::*; -use crate::content::*; -use crate::curve::named_curve::*; -use crate::error::Error; -use crate::extension::extension_server_name::*; -use crate::extension::extension_supported_elliptic_curves::*; -use crate::extension::extension_supported_point_formats::*; -use crate::extension::extension_supported_signature_algorithms::*; -use crate::extension::extension_use_extended_master_secret::*; -use crate::extension::extension_use_srtp::*; -use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; -use crate::extension::*; -use crate::handshake::handshake_message_client_hello::*; -use crate::handshake::handshake_message_server_key_exchange::*; -use crate::handshake::*; -use crate::prf::{prf_pre_master_secret, prf_psk_pre_master_secret}; -use crate::record_layer::record_layer_header::*; -use crate::record_layer::*; -use crate::{find_matching_cipher_suite, find_matching_srtp_profile}; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight3; - -impl fmt::Display for Flight3 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 3") - } -} - -#[async_trait] -impl Flight for Flight3 { - async fn parse( - &self, - _tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - // Clients may receive multiple HelloVerifyRequest messages with different cookies. - // Clients SHOULD handle this by sending a new ClientHello with a cookie in response - // to the new HelloVerifyRequest. RFC 6347 Section 4.2.1 - if let Ok((seq, msgs)) = cache - .full_pull_map( - state.handshake_recv_sequence, - &[HandshakeCachePullRule { - typ: HandshakeType::HelloVerifyRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }], - ) - .await - { - if let Some(message) = msgs.get(&HandshakeType::HelloVerifyRequest) { - // DTLS 1.2 clients must not assume that the server will use the protocol version - // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 - let h = match message { - HandshakeMessage::HelloVerifyRequest(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - // DTLS 1.2 clients must not assume that the server will use the protocol version - // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 - if h.version != PROTOCOL_VERSION1_0 && h.version != PROTOCOL_VERSION1_2 { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::ProtocolVersion, - }), - Some(Error::ErrUnsupportedProtocolVersion), - )); - } - - state.cookie.clone_from(&h.cookie); - state.handshake_recv_sequence = seq; - return Ok(Box::new(Flight3 {}) as Box); - } - } - - let result = if cfg.local_psk_callback.is_some() { - cache - .full_pull_map( - state.handshake_recv_sequence, - &[ - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - ], - ) - .await - } else { - cache - .full_pull_map( - state.handshake_recv_sequence, - &[ - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: true, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - ], - ) - .await - }; - - let (seq, msgs) = match result { - Ok((seq, msgs)) => (seq, msgs), - Err(_) => return Err((None, None)), - }; - - state.handshake_recv_sequence = seq; - - if let Some(message) = msgs.get(&HandshakeType::ServerHello) { - let h = match message { - HandshakeMessage::ServerHello(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if h.version != PROTOCOL_VERSION1_2 { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::ProtocolVersion, - }), - Some(Error::ErrUnsupportedProtocolVersion), - )); - } - - for extension in &h.extensions { - match extension { - Extension::UseSrtp(e) => { - let profile = match find_matching_srtp_profile( - &e.protection_profiles, - &cfg.local_srtp_protection_profiles, - ) { - Ok(profile) => profile, - Err(_) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::IllegalParameter, - }), - Some(Error::ErrClientNoMatchingSrtpProfile), - )) - } - }; - state.srtp_protection_profile = profile; - } - Extension::UseExtendedMasterSecret(_) => { - if cfg.extended_master_secret != ExtendedMasterSecretType::Disable { - state.extended_master_secret = true; - } - } - _ => {} - }; - } - - if cfg.extended_master_secret == ExtendedMasterSecretType::Require - && !state.extended_master_secret - { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrClientRequiredButNoServerEms), - )); - } - if !cfg.local_srtp_protection_profiles.is_empty() - && state.srtp_protection_profile == SrtpProtectionProfile::Unsupported - { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrRequestedButNoSrtpExtension), - )); - } - if find_matching_cipher_suite(&[h.cipher_suite], &cfg.local_cipher_suites).is_err() { - debug!( - "[handshake:{}] use cipher suite: {}", - srv_cli_str(state.is_client), - h.cipher_suite - ); - - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrCipherSuiteNoIntersection), - )); - } - - let cipher_suite = match cipher_suite_for_id(h.cipher_suite) { - Ok(cipher_suite) => cipher_suite, - Err(_) => { - debug!( - "[handshake:{}] use cipher suite: {}", - srv_cli_str(state.is_client), - h.cipher_suite - ); - - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrInvalidCipherSuite), - )); - } - }; - - trace!( - "[handshake:{}] use cipher suite: {}", - srv_cli_str(state.is_client), - cipher_suite.to_string() - ); - { - let mut cs = state.cipher_suite.lock().await; - *cs = Some(cipher_suite); - } - state.remote_random = h.random.clone(); - } - - if let Some(message) = msgs.get(&HandshakeType::Certificate) { - let h = match message { - HandshakeMessage::Certificate(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - state.peer_certificates.clone_from(&h.certificate); - } - - if let Some(message) = msgs.get(&HandshakeType::ServerKeyExchange) { - let h = match message { - HandshakeMessage::ServerKeyExchange(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if let Err((alert, err)) = handle_server_key_exchange(state, cfg, h) { - return Err((alert, err)); - } - } - - if let Some(message) = msgs.get(&HandshakeType::CertificateRequest) { - match message { - HandshakeMessage::CertificateRequest(_) => {} - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - state.remote_requested_certificate = true; - } - - Ok(Box::new(Flight5 {}) as Box) - } - - async fn generate( - &self, - state: &mut State, - _cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let mut extensions = vec![ - Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { - signature_hash_algorithms: cfg.local_signature_schemes.clone(), - }), - Extension::RenegotiationInfo(ExtensionRenegotiationInfo { - renegotiated_connection: 0, - }), - ]; - - if cfg.local_psk_callback.is_none() { - extensions.extend_from_slice(&[ - Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], - }), - Extension::SupportedPointFormats(ExtensionSupportedPointFormats { - point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], - }), - ]); - } - - if !cfg.local_srtp_protection_profiles.is_empty() { - extensions.push(Extension::UseSrtp(ExtensionUseSrtp { - protection_profiles: cfg.local_srtp_protection_profiles.clone(), - })); - } - - if cfg.extended_master_secret == ExtendedMasterSecretType::Request - || cfg.extended_master_secret == ExtendedMasterSecretType::Require - { - extensions.push(Extension::UseExtendedMasterSecret( - ExtensionUseExtendedMasterSecret { supported: true }, - )); - } - - if !cfg.server_name.is_empty() { - extensions.push(Extension::ServerName(ExtensionServerName { - server_name: cfg.server_name.clone(), - })); - } - - Ok(vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( - HandshakeMessageClientHello { - version: PROTOCOL_VERSION1_2, - random: state.local_random.clone(), - cookie: state.cookie.clone(), - - cipher_suites: cfg.local_cipher_suites.clone(), - compression_methods: default_compression_methods(), - extensions, - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]) - } -} - -pub(crate) fn handle_server_key_exchange( - state: &mut State, - cfg: &HandshakeConfig, - h: &HandshakeMessageServerKeyExchange, -) -> Result<(), (Option, Option)> { - if let Some(local_psk_callback) = &cfg.local_psk_callback { - let psk = match local_psk_callback(&h.identity_hint) { - Ok(psk) => psk, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.identity_hint.clone_from(&h.identity_hint); - state.pre_master_secret = prf_psk_pre_master_secret(&psk); - } else { - let local_keypair = match h.named_curve.generate_keypair() { - Ok(local_keypair) => local_keypair, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.pre_master_secret = match prf_pre_master_secret( - &h.public_key, - &local_keypair.private_key, - local_keypair.curve, - ) { - Ok(pre_master_secret) => pre_master_secret, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.local_keypair = Some(local_keypair); - } - - Ok(()) -} diff --git a/dtls/src/flight/flight4.rs b/dtls/src/flight/flight4.rs deleted file mode 100644 index 11a77ffe1..000000000 --- a/dtls/src/flight/flight4.rs +++ /dev/null @@ -1,852 +0,0 @@ -use std::fmt; -use std::io::BufWriter; - -use async_trait::async_trait; -use log::*; - -use super::flight6::*; -use super::*; -use crate::cipher_suite::*; -use crate::client_certificate_type::*; -use crate::compression_methods::*; -use crate::config::*; -use crate::content::*; -use crate::crypto::*; -use crate::curve::named_curve::*; -use crate::curve::*; -use crate::error::Error; -use crate::extension::extension_supported_elliptic_curves::*; -use crate::extension::extension_supported_point_formats::*; -use crate::extension::extension_use_extended_master_secret::*; -use crate::extension::extension_use_srtp::*; -use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; -use crate::extension::*; -use crate::handshake::handshake_message_certificate::*; -use crate::handshake::handshake_message_certificate_request::*; -use crate::handshake::handshake_message_server_hello::*; -use crate::handshake::handshake_message_server_hello_done::*; -use crate::handshake::handshake_message_server_key_exchange::*; -use crate::handshake::*; -use crate::prf::*; -use crate::record_layer::record_layer_header::*; -use crate::record_layer::*; -use crate::signature_hash_algorithm::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight4; - -impl fmt::Display for Flight4 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 4") - } -} - -#[async_trait] -impl Flight for Flight4 { - async fn parse( - &self, - tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let (seq, msgs) = match cache - .full_pull_map( - state.handshake_recv_sequence, - &[ - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: true, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateVerify, - epoch: cfg.initial_epoch, - is_client: true, - optional: true, - }, - ], - ) - .await - { - Ok((seq, msgs)) => (seq, msgs), - Err(_) => return Err((None, None)), - }; - - let client_key_exchange = if let Some(HandshakeMessage::ClientKeyExchange(h)) = - msgs.get(&HandshakeType::ClientKeyExchange) - { - h - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )); - }; - - if let Some(message) = msgs.get(&HandshakeType::Certificate) { - let h = match message { - HandshakeMessage::Certificate(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - state.peer_certificates.clone_from(&h.certificate); - trace!( - "[handshake] PeerCertificates4 {}", - state.peer_certificates.len() - ); - } - - if let Some(message) = msgs.get(&HandshakeType::CertificateVerify) { - let h = match message { - HandshakeMessage::CertificateVerify(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - - if state.peer_certificates.is_empty() { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::NoCertificate, - }), - Some(Error::ErrCertificateVerifyNoCertificate), - )); - } - - let plain_text = cache - .pull_and_merge(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - ]) - .await; - - // Verify that the pair of hash algorithm and signature is listed. - let mut valid_signature_scheme = false; - for ss in &cfg.local_signature_schemes { - if ss.hash == h.algorithm.hash && ss.signature == h.algorithm.signature { - valid_signature_scheme = true; - break; - } - } - if !valid_signature_scheme { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrNoAvailableSignatureSchemes), - )); - } - - if let Err(err) = verify_certificate_verify( - &plain_text, - &h.algorithm, - &h.signature, - &state.peer_certificates, - cfg.insecure_verification, - ) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); - } - - let mut chains = vec![]; - let mut verified = false; - if cfg.client_auth as u8 >= ClientAuthType::VerifyClientCertIfGiven as u8 { - if let Some(client_cert_verifier) = &cfg.client_cert_verifier { - chains = - match verify_client_cert(&state.peer_certificates, client_cert_verifier) { - Ok(chains) => chains, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )) - } - }; - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(Error::ErrInvalidCertificate), - )); - } - - verified = true - } - if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate { - if let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); - } - } - state.peer_certificates_verified = verified - } else if !state.peer_certificates.is_empty() { - // A certificate was received, but we haven't seen a CertificateVerify - // keep reading until we receive one - return Err((None, None)); - } - - { - let mut cipher_suite = state.cipher_suite.lock().await; - if let Some(cipher_suite) = &mut *cipher_suite { - if !cipher_suite.is_initialized() { - let mut server_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); - let _ = state.local_random.marshal(&mut writer); - } - let mut client_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); - let _ = state.remote_random.marshal(&mut writer); - } - - let mut pre_master_secret = vec![]; - if let Some(local_psk_callback) = &cfg.local_psk_callback { - let psk = match local_psk_callback(&client_key_exchange.identity_hint) { - Ok(psk) => psk, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state - .identity_hint - .clone_from(&client_key_exchange.identity_hint); - pre_master_secret = prf_psk_pre_master_secret(&psk); - } else if let Some(local_keypair) = &state.local_keypair { - pre_master_secret = match prf_pre_master_secret( - &client_key_exchange.public_key, - &local_keypair.private_key, - local_keypair.curve, - ) { - Ok(pre_master_secret) => pre_master_secret, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::IllegalParameter, - }), - Some(err), - )) - } - }; - } - - if state.extended_master_secret { - let hf = cipher_suite.hash_func(); - let session_hash = - match cache.session_hash(hf, cfg.initial_epoch, &[]).await { - Ok(s) => s, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.master_secret = match prf_extended_master_secret( - &pre_master_secret, - &session_hash, - cipher_suite.hash_func(), - ) { - Ok(ms) => ms, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - } else { - state.master_secret = match prf_master_secret( - &pre_master_secret, - &client_random, - &server_random, - cipher_suite.hash_func(), - ) { - Ok(ms) => ms, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - } - - if let Err(err) = cipher_suite.init( - &state.master_secret, - &client_random, - &server_random, - false, - ) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - } - } - } - - // Now, encrypted packets can be handled - let (done_tx, mut done_rx) = mpsc::channel(1); - if let Err(err) = tx.send(done_tx).await { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(Error::Other(err.to_string())), - )); - } - - done_rx.recv().await; - - let (seq, msgs) = match cache - .full_pull_map( - seq, - &[HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: true, - optional: false, - }], - ) - .await - { - Ok((seq, msgs)) => (seq, msgs), - // No valid message received. Keep reading - Err(_) => return Err((None, None)), - }; - - state.handshake_recv_sequence = seq; - - if let Some(HandshakeMessage::Finished(h)) = msgs.get(&HandshakeType::Finished) { - h - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )); - }; - - match cfg.client_auth { - ClientAuthType::RequireAnyClientCert => { - trace!( - "{} peer_certificates.len() {}", - srv_cli_str(state.is_client), - state.peer_certificates.len(), - ); - if state.peer_certificates.is_empty() { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::NoCertificate, - }), - Some(Error::ErrClientCertificateRequired), - )); - } - } - ClientAuthType::VerifyClientCertIfGiven => { - if !state.peer_certificates.is_empty() && !state.peer_certificates_verified { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(Error::ErrClientCertificateNotVerified), - )); - } - } - ClientAuthType::RequireAndVerifyClientCert => { - if state.peer_certificates.is_empty() { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::NoCertificate, - }), - Some(Error::ErrClientCertificateRequired), - )); - } - if !state.peer_certificates_verified { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(Error::ErrClientCertificateNotVerified), - )); - } - } - ClientAuthType::NoClientCert | ClientAuthType::RequestClientCert => { - return Ok(Box::new(Flight6 {}) as Box); - } - } - - Ok(Box::new(Flight6 {}) as Box) - } - - async fn generate( - &self, - state: &mut State, - _cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let mut extensions = vec![Extension::RenegotiationInfo(ExtensionRenegotiationInfo { - renegotiated_connection: 0, - })]; - if (cfg.extended_master_secret == ExtendedMasterSecretType::Request - || cfg.extended_master_secret == ExtendedMasterSecretType::Require) - && state.extended_master_secret - { - extensions.push(Extension::UseExtendedMasterSecret( - ExtensionUseExtendedMasterSecret { supported: true }, - )); - } - - if state.srtp_protection_profile != SrtpProtectionProfile::Unsupported { - extensions.push(Extension::UseSrtp(ExtensionUseSrtp { - protection_profiles: vec![state.srtp_protection_profile], - })); - } - - if cfg.local_psk_callback.is_none() { - extensions.extend_from_slice(&[ - Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], - }), - Extension::SupportedPointFormats(ExtensionSupportedPointFormats { - point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], - }), - ]); - } - - let mut pkts = vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ServerHello( - HandshakeMessageServerHello { - version: PROTOCOL_VERSION1_2, - random: state.local_random.clone(), - cipher_suite: { - let cipher_suite = state.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - cipher_suite.id() - } else { - CipherSuiteId::Unsupported - } - }, - compression_method: default_compression_methods().ids[0], - extensions, - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]; - - if cfg.local_psk_callback.is_none() { - let certificate = match cfg.get_certificate(&cfg.server_name) { - Ok(cert) => cert, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::HandshakeFailure, - }), - Some(err), - )) - } - }; - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::Certificate( - HandshakeMessageCertificate { - certificate: certificate - .certificate - .iter() - .map(|x| x.as_ref().to_owned()) - .collect(), - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - - let mut server_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); - let _ = state.local_random.marshal(&mut writer); - } - let mut client_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); - let _ = state.remote_random.marshal(&mut writer); - } - - // Find compatible signature scheme - let signature_hash_algo = match select_signature_scheme( - &cfg.local_signature_schemes, - &certificate.private_key, - ) { - Ok(s) => s, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(err), - )) - } - }; - - if let Some(local_keypair) = &state.local_keypair { - let signature = match generate_key_signature( - &client_random, - &server_random, - &local_keypair.public_key, - state.named_curve, - &certificate.private_key, /*, signature_hash_algo.hash*/ - ) { - Ok(s) => s, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.local_key_signature = signature; - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ServerKeyExchange( - HandshakeMessageServerKeyExchange { - identity_hint: vec![], - elliptic_curve_type: EllipticCurveType::NamedCurve, - named_curve: state.named_curve, - public_key: local_keypair.public_key.clone(), - algorithm: SignatureHashAlgorithm { - hash: signature_hash_algo.hash, - signature: signature_hash_algo.signature, - }, - signature: state.local_key_signature.clone(), - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - } - - if cfg.client_auth as u8 > ClientAuthType::NoClientCert as u8 { - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::CertificateRequest( - HandshakeMessageCertificateRequest { - certificate_types: vec![ - ClientCertificateType::RsaSign, - ClientCertificateType::EcdsaSign, - ], - signature_hash_algorithms: cfg.local_signature_schemes.clone(), - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - } - } else if let Some(local_psk_identity_hint) = &cfg.local_psk_identity_hint { - // To help the client in selecting which identity to use, the server - // can provide a "PSK identity hint" in the ServerKeyExchange message. - // If no hint is provided, the ServerKeyExchange message is omitted. - // - // https://tools.ietf.org/html/rfc4279#section-2 - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ServerKeyExchange( - HandshakeMessageServerKeyExchange { - identity_hint: local_psk_identity_hint.clone(), - elliptic_curve_type: EllipticCurveType::Unsupported, - named_curve: NamedCurve::Unsupported, - public_key: vec![], - algorithm: SignatureHashAlgorithm { - hash: HashAlgorithm::Unsupported, - signature: SignatureAlgorithm::Unsupported, - }, - signature: vec![], - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - } - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ServerHelloDone( - HandshakeMessageServerHelloDone {}, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - - Ok(pkts) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use tokio::sync::Mutex; - - use super::*; - use crate::error::Result; - - struct MockCipherSuite {} - - impl CipherSuite for MockCipherSuite { - fn to_string(&self) -> String { - "MockCipherSuite".into() - } - fn id(&self) -> CipherSuiteId { - unimplemented!(); - } - fn certificate_type(&self) -> ClientCertificateType { - unimplemented!(); - } - fn hash_func(&self) -> CipherSuiteHash { - unimplemented!(); - } - fn is_psk(&self) -> bool { - false - } - fn is_initialized(&self) -> bool { - panic!("is_initialized called with Certificate but not CertificateVerify"); - } - - // Generate the internal encryption state - fn init( - &mut self, - _master_secret: &[u8], - _client_random: &[u8], - _server_random: &[u8], - _is_client: bool, - ) -> Result<()> { - unimplemented!(); - } - - fn encrypt(&self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { - unimplemented!(); - } - fn decrypt(&self, _input: &[u8]) -> Result> { - unimplemented!(); - } - } - - // Assert that if a client sends a certificate they must also send a `CertificateVerify` - // message. The `Flight4` must not interact with the `cipher_suite` if the `CertificateVerify` - // is missing. - #[tokio::test] - async fn test_flight4_process_certificateverify() { - let mut state = State { - cipher_suite: Arc::new(Mutex::new(Some(Box::new(MockCipherSuite {})))), - ..Default::default() - }; - - let raw_certificate = vec![ - 0x0b, 0x00, 0x01, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9b, 0x00, 0x01, - 0x98, 0x00, 0x01, 0x95, 0x30, 0x82, 0x01, 0x91, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, - 0x02, 0x01, 0x02, 0x02, 0x11, 0x01, 0x65, 0x03, 0x3f, 0x4d, 0x0b, 0x9a, 0x62, 0x91, - 0xdb, 0x4d, 0x28, 0x2c, 0x1f, 0xd6, 0x73, 0x32, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, - 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x00, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x32, - 0x30, 0x35, 0x31, 0x35, 0x31, 0x38, 0x34, 0x33, 0x35, 0x35, 0x5a, 0x17, 0x0d, 0x32, - 0x32, 0x30, 0x36, 0x31, 0x35, 0x31, 0x38, 0x34, 0x33, 0x35, 0x35, 0x5a, 0x30, 0x00, - 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, - 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xc3, - 0xb7, 0x13, 0x1a, 0x0a, 0xfc, 0xd0, 0x82, 0xf8, 0x94, 0x5e, 0xc0, 0x77, 0x07, 0x81, - 0x28, 0xc9, 0xcb, 0x08, 0x84, 0x50, 0x6b, 0xf0, 0x22, 0xe8, 0x79, 0xb9, 0x15, 0x33, - 0xc4, 0x56, 0xa1, 0xd3, 0x1b, 0x24, 0xe3, 0x61, 0xbd, 0x4d, 0x65, 0x80, 0x6b, 0x5d, - 0x96, 0x48, 0xa2, 0x44, 0x9e, 0xce, 0xe8, 0x65, 0xd6, 0x3c, 0xe0, 0x9b, 0x6b, 0xa1, - 0x36, 0x34, 0xb2, 0x39, 0xe2, 0x03, 0x00, 0xa3, 0x81, 0x92, 0x30, 0x81, 0x8f, 0x30, - 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x02, - 0xa4, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, - 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, - 0x05, 0x07, 0x03, 0x01, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, - 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, - 0x04, 0x16, 0x04, 0x14, 0xb1, 0x1a, 0xe3, 0xeb, 0x6f, 0x7c, 0xc3, 0x8f, 0xba, 0x6f, - 0x1c, 0xe8, 0xf0, 0x23, 0x08, 0x50, 0x8d, 0x3c, 0xea, 0x31, 0x30, 0x2e, 0x06, 0x03, - 0x55, 0x1d, 0x11, 0x01, 0x01, 0xff, 0x04, 0x24, 0x30, 0x22, 0x82, 0x20, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, - 0x03, 0x47, 0x00, 0x30, 0x44, 0x02, 0x20, 0x06, 0x31, 0x43, 0xac, 0x03, 0x45, 0x79, - 0x3c, 0xd7, 0x5f, 0x6e, 0x6a, 0xf8, 0x0e, 0xfd, 0x35, 0x49, 0xee, 0x1b, 0xbc, 0x47, - 0xce, 0xe3, 0x39, 0xec, 0xe4, 0x62, 0xe1, 0x30, 0x1a, 0xa1, 0x89, 0x02, 0x20, 0x35, - 0xcd, 0x7a, 0x15, 0x68, 0x09, 0x50, 0x49, 0x9e, 0x3e, 0x05, 0xd7, 0xc2, 0x69, 0x3f, - 0x9c, 0x0c, 0x98, 0x92, 0x65, 0xec, 0xae, 0x44, 0xfe, 0xe5, 0x68, 0xb8, 0x09, 0x78, - 0x7f, 0x6b, 0x77, - ]; - - let raw_client_key_exchange = vec![ - 0x10, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x20, 0x96, - 0xed, 0x0c, 0xee, 0xf3, 0x11, 0xb1, 0x9d, 0x8b, 0x1c, 0x02, 0x7f, 0x06, 0x7c, 0x57, - 0x7a, 0x14, 0xa6, 0x41, 0xde, 0x63, 0x57, 0x9e, 0xcd, 0x34, 0x54, 0xba, 0x37, 0x4d, - 0x34, 0x15, 0x18, - ]; - - let mut cache = HandshakeCache::new(); - cache - .push(raw_certificate, 0, 0, HandshakeType::Certificate, true) - .await; - cache - .push( - raw_client_key_exchange, - 0, - 1, - HandshakeType::ClientKeyExchange, - true, - ) - .await; - - let cfg = HandshakeConfig::default(); - - let (mut tx, _rx) = mpsc::channel::>(1); - - let f = Flight4 {}; - let res = f.parse(&mut tx, &mut state, &cache, &cfg).await; - assert!(res.is_err()); - } -} diff --git a/dtls/src/flight/flight5.rs b/dtls/src/flight/flight5.rs deleted file mode 100644 index 264cd9e0a..000000000 --- a/dtls/src/flight/flight5.rs +++ /dev/null @@ -1,778 +0,0 @@ -use std::fmt; -use std::io::{BufReader, BufWriter}; - -use async_trait::async_trait; - -use super::flight3::*; -use super::*; -use crate::change_cipher_spec::ChangeCipherSpec; -use crate::content::*; -use crate::crypto::*; -use crate::curve::named_curve::*; -use crate::curve::*; -use crate::error::Error; -use crate::handshake::handshake_message_certificate::*; -use crate::handshake::handshake_message_certificate_verify::*; -use crate::handshake::handshake_message_client_key_exchange::*; -use crate::handshake::handshake_message_finished::*; -use crate::handshake::handshake_message_server_key_exchange::*; -use crate::handshake::*; -use crate::prf::*; -use crate::record_layer::record_layer_header::*; -use crate::record_layer::*; -use crate::signature_hash_algorithm::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight5; - -impl fmt::Display for Flight5 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 5") - } -} - -#[async_trait] -impl Flight for Flight5 { - fn is_last_recv_flight(&self) -> bool { - true - } - - async fn parse( - &self, - _tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let (_seq, msgs) = match cache - .full_pull_map( - state.handshake_recv_sequence, - &[HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: false, - optional: false, - }], - ) - .await - { - Ok((seq, msgs)) => (seq, msgs), - Err(_) => return Err((None, None)), - }; - - let finished = - if let Some(HandshakeMessage::Finished(h)) = msgs.get(&HandshakeType::Finished) { - h - } else { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )); - }; - - let plain_text = cache - .pull_and_merge(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateVerify, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: true, - optional: false, - }, - ]) - .await; - - { - let cipher_suite = state.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - let expected_verify_data = match prf_verify_data_server( - &state.master_secret, - &plain_text, - cipher_suite.hash_func(), - ) { - Ok(d) => d, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(err), - )) - } - }; - - if expected_verify_data != finished.verify_data { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::HandshakeFailure, - }), - Some(Error::ErrVerifyDataMismatch), - )); - } - } - } - - Ok(Box::new(Flight5 {})) - } - - async fn generate( - &self, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let certificate = if !cfg.local_certificates.is_empty() { - let cert = match cfg.get_certificate(&cfg.server_name) { - Ok(cert) => cert, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::HandshakeFailure, - }), - Some(err), - )) - } - }; - Some(cert) - } else { - None - }; - - let mut pkts = vec![]; - - if state.remote_requested_certificate { - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::Certificate( - HandshakeMessageCertificate { - certificate: if let Some(cert) = &certificate { - cert.certificate - .iter() - .map(|x| x.as_ref().to_owned()) - .collect() - } else { - vec![] - }, - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - } - - let mut client_key_exchange = HandshakeMessageClientKeyExchange { - identity_hint: vec![], - public_key: vec![], - }; - if cfg.local_psk_callback.is_none() { - if let Some(local_keypair) = &state.local_keypair { - client_key_exchange - .public_key - .clone_from(&local_keypair.public_key); - } - } else if let Some(local_psk_identity_hint) = &cfg.local_psk_identity_hint { - client_key_exchange - .identity_hint - .clone_from(local_psk_identity_hint); - } - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::ClientKeyExchange( - client_key_exchange, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - - let server_key_exchange_data = cache - .pull_and_merge(&[HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }]) - .await; - - let mut server_key_exchange = HandshakeMessageServerKeyExchange { - identity_hint: vec![], - elliptic_curve_type: EllipticCurveType::Unsupported, - named_curve: NamedCurve::Unsupported, - public_key: vec![], - algorithm: SignatureHashAlgorithm { - hash: HashAlgorithm::Unsupported, - signature: SignatureAlgorithm::Unsupported, - }, - signature: vec![], - }; - - // handshakeMessageServerKeyExchange is optional for PSK - if server_key_exchange_data.is_empty() { - if let Err((alert, err)) = handle_server_key_exchange(state, cfg, &server_key_exchange) - { - return Err((alert, err)); - } - } else { - let mut reader = BufReader::new(server_key_exchange_data.as_slice()); - let raw_handshake = match Handshake::unmarshal(&mut reader) { - Ok(h) => h, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnexpectedMessage, - }), - Some(err), - )) - } - }; - - match raw_handshake.handshake_message { - HandshakeMessage::ServerKeyExchange(h) => server_key_exchange = h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::UnexpectedMessage, - }), - Some(Error::ErrInvalidContentType), - )) - } - }; - } - - // Append not-yet-sent packets - let mut merged = vec![]; - let mut seq_pred = state.handshake_send_sequence as u16; - for p in &mut pkts { - let h = match &mut p.record.content { - Content::Handshake(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(Error::ErrInvalidContentType), - )) - } - }; - h.handshake_header.message_sequence = seq_pred; - seq_pred += 1; - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - if let Err(err) = h.marshal(&mut writer) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - } - - merged.extend_from_slice(&raw); - } - - if let Err((alert, err)) = - initialize_cipher_suite(state, cache, cfg, &server_key_exchange, &merged).await - { - return Err((alert, err)); - } - - // If the client has sent a certificate with signing ability, a digitally-signed - // CertificateVerify message is sent to explicitly verify possession of the - // private key in the certificate. - if state.remote_requested_certificate && !cfg.local_certificates.is_empty() { - let mut plain_text = cache - .pull_and_merge(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - ]) - .await; - - plain_text.extend_from_slice(&merged); - - // Find compatible signature scheme - let signature_hash_algo = match select_signature_scheme( - &cfg.local_signature_schemes, - &certificate.as_ref().unwrap().private_key, - ) { - Ok(s) => s, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(err), - )) - } - }; - - let cert_verify = match generate_certificate_verify( - &plain_text, - &certificate.as_ref().unwrap().private_key, /*, signature_hash_algo.hash*/ - ) { - Ok(cert) => cert, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - state.local_certificates_verify = cert_verify; - - let mut p = Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::Handshake(Handshake::new(HandshakeMessage::CertificateVerify( - HandshakeMessageCertificateVerify { - algorithm: signature_hash_algo, - signature: state.local_certificates_verify.clone(), - }, - ))), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }; - - let h = match &mut p.record.content { - Content::Handshake(h) => h, - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(Error::ErrInvalidContentType), - )) - } - }; - h.handshake_header.message_sequence = seq_pred; - - // seqPred++ // this is the last use of seqPred - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - if let Err(err) = h.marshal(&mut writer) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - } - merged.extend_from_slice(&raw); - - pkts.push(p); - } - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::ChangeCipherSpec(ChangeCipherSpec {}), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }); - - if state.local_verify_data.is_empty() { - let mut plain_text = cache - .pull_and_merge(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateVerify, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: true, - optional: false, - }, - ]) - .await; - - plain_text.extend_from_slice(&merged); - - let cipher_suite = state.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - state.local_verify_data = match prf_verify_data_client( - &state.master_secret, - &plain_text, - cipher_suite.hash_func(), - ) { - Ok(data) => data, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - } - } - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 1, - Content::Handshake(Handshake::new(HandshakeMessage::Finished( - HandshakeMessageFinished { - verify_data: state.local_verify_data.clone(), - }, - ))), - ), - should_encrypt: true, - reset_local_sequence_number: true, - }); - - Ok(pkts) - } -} -async fn initialize_cipher_suite( - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - h: &HandshakeMessageServerKeyExchange, - sending_plain_text: &[u8], -) -> Result<(), (Option, Option)> { - let mut cipher_suite = state.cipher_suite.lock().await; - - if let Some(cipher_suite) = &*cipher_suite { - if cipher_suite.is_initialized() { - return Ok(()); - } - } - - let mut client_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); - let _ = state.local_random.marshal(&mut writer); - } - let mut server_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); - let _ = state.remote_random.marshal(&mut writer); - } - - if let Some(cipher_suite) = &*cipher_suite { - if state.extended_master_secret { - let session_hash = match cache - .session_hash( - cipher_suite.hash_func(), - cfg.initial_epoch, - sending_plain_text, - ) - .await - { - Ok(s) => s, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - - state.master_secret = match prf_extended_master_secret( - &state.pre_master_secret, - &session_hash, - cipher_suite.hash_func(), - ) { - Ok(m) => m, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::IllegalParameter, - }), - Some(err), - )) - } - }; - } else { - state.master_secret = match prf_master_secret( - &state.pre_master_secret, - &client_random, - &server_random, - cipher_suite.hash_func(), - ) { - Ok(m) => m, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - } - } - - if cfg.local_psk_callback.is_none() { - // Verify that the pair of hash algorithm and signiture is listed. - let mut valid_signature_scheme = false; - for ss in &cfg.local_signature_schemes { - if ss.hash == h.algorithm.hash && ss.signature == h.algorithm.signature { - valid_signature_scheme = true; - break; - } - } - if !valid_signature_scheme { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InsufficientSecurity, - }), - Some(Error::ErrNoAvailableSignatureSchemes), - )); - } - - let expected_msg = - value_key_message(&client_random, &server_random, &h.public_key, h.named_curve); - if let Err(err) = verify_key_signature( - &expected_msg, - &h.algorithm, - &h.signature, - &state.peer_certificates, - cfg.insecure_verification, - ) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); - } - - let mut chains = vec![]; - if !cfg.insecure_skip_verify { - chains = match verify_server_cert( - &state.peer_certificates, - &cfg.server_cert_verifier, - &cfg.server_name, - ) { - Ok(chains) => chains, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )) - } - } - } - if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate { - if let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); - } - } - } - - if let Some(cipher_suite) = &mut *cipher_suite { - if let Err(err) = - cipher_suite.init(&state.master_secret, &client_random, &server_random, true) - { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - } - - Ok(()) -} diff --git a/dtls/src/flight/flight6.rs b/dtls/src/flight/flight6.rs deleted file mode 100644 index 1e9b00362..000000000 --- a/dtls/src/flight/flight6.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::fmt; - -use async_trait::async_trait; - -use super::*; -use crate::change_cipher_spec::*; -use crate::content::*; -use crate::handshake::handshake_message_finished::*; -use crate::handshake::*; -use crate::prf::*; -use crate::record_layer::record_layer_header::*; - -#[derive(Debug, PartialEq)] -pub(crate) struct Flight6; - -impl fmt::Display for Flight6 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flight 6") - } -} - -#[async_trait] -impl Flight for Flight6 { - fn is_last_send_flight(&self) -> bool { - true - } - - async fn parse( - &self, - _tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let (_, msgs) = match cache - .full_pull_map( - state.handshake_recv_sequence - 1, - &[HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: true, - optional: false, - }], - ) - .await - { - Ok((seq, msgs)) => (seq, msgs), - // No valid message received. Keep reading - Err(_) => return Err((None, None)), - }; - - if let Some(message) = msgs.get(&HandshakeType::Finished) { - match message { - HandshakeMessage::Finished(_) => {} - _ => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - None, - )) - } - }; - } - - // Other party retransmitted the last flight. - Ok(Box::new(Flight6 {})) - } - - async fn generate( - &self, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)> { - let mut pkts = vec![Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 0, - Content::ChangeCipherSpec(ChangeCipherSpec {}), - ), - should_encrypt: false, - reset_local_sequence_number: false, - }]; - - if state.local_verify_data.is_empty() { - let plain_text = cache - .pull_and_merge(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch: cfg.initial_epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateVerify, - epoch: cfg.initial_epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Finished, - epoch: cfg.initial_epoch + 1, - is_client: true, - optional: false, - }, - ]) - .await; - - let cipher_suite = state.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - state.local_verify_data = match prf_verify_data_server( - &state.master_secret, - &plain_text, - cipher_suite.hash_func(), - ) { - Ok(data) => data, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )) - } - }; - } - } - - pkts.push(Packet { - record: RecordLayer::new( - PROTOCOL_VERSION1_2, - 1, - Content::Handshake(Handshake::new(HandshakeMessage::Finished( - HandshakeMessageFinished { - verify_data: state.local_verify_data.clone(), - }, - ))), - ), - should_encrypt: true, - reset_local_sequence_number: true, - }); - - Ok(pkts) - } -} diff --git a/dtls/src/flight/mod.rs b/dtls/src/flight/mod.rs deleted file mode 100644 index 8e6b41e6f..000000000 --- a/dtls/src/flight/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -pub(crate) mod flight0; -pub(crate) mod flight1; -pub(crate) mod flight2; -pub(crate) mod flight3; -pub(crate) mod flight4; -pub(crate) mod flight5; -pub(crate) mod flight6; - -use std::fmt; - -use async_trait::async_trait; -use tokio::sync::mpsc; - -use crate::alert::*; -use crate::error::Error; -use crate::handshake::handshake_cache::*; -use crate::handshaker::*; -use crate::record_layer::*; -use crate::state::*; - -/* - DTLS messages are grouped into a series of message flights, according - to the diagrams below. Although each Flight of messages may consist - of a number of messages, they should be viewed as monolithic for the - purpose of timeout and retransmission. - https://tools.ietf.org/html/rfc4347#section-4.2.4 - Client Server - ------ ------ - Waiting Flight 0 - - ClientHello --------> Flight 1 - - <------- HelloVerifyRequest Flight 2 - - ClientHello --------> Flight 3 - - ServerHello \ - Certificate* \ - ServerKeyExchange* Flight 4 - CertificateRequest* / - <-------- ServerHelloDone / - - Certificate* \ - ClientKeyExchange \ - CertificateVerify* Flight 5 - [ChangeCipherSpec] / - Finished --------> / - - [ChangeCipherSpec] \ Flight 6 - <-------- Finished / - -*/ - -#[derive(Clone, Debug)] -pub(crate) struct Packet { - pub(crate) record: RecordLayer, - pub(crate) should_encrypt: bool, - pub(crate) reset_local_sequence_number: bool, -} - -#[async_trait] -pub(crate) trait Flight: fmt::Display + fmt::Debug { - fn is_last_send_flight(&self) -> bool { - false - } - fn is_last_recv_flight(&self) -> bool { - false - } - fn has_retransmit(&self) -> bool { - true - } - - async fn parse( - &self, - tx: &mut mpsc::Sender>, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)>; - - async fn generate( - &self, - state: &mut State, - cache: &HandshakeCache, - cfg: &HandshakeConfig, - ) -> Result, (Option, Option)>; -} diff --git a/dtls/src/fragment_buffer/fragment_buffer_test.rs b/dtls/src/fragment_buffer/fragment_buffer_test.rs deleted file mode 100644 index 0e9090809..000000000 --- a/dtls/src/fragment_buffer/fragment_buffer_test.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::*; - -#[test] -fn test_fragment_buffer() -> Result<()> { - let tests = vec![ - ( - "Single Fragment", - vec![vec![ - 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, - ]], - vec![vec![ - 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, - 0x00, - ]], - 0, - ), - ( - "Single Fragment Epoch 3", - vec![vec![ - 0x16, 0xfe, 0xff, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, - ]], - vec![vec![ - 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, - 0x00, - ]], - 3, - ), - ( - "Multiple Fragments", - vec![ - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, - 0x01, 0x02, 0x03, 0x04, - ], - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, - 0x06, 0x07, 0x08, 0x09, - ], - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, - 0x0B, 0x0C, 0x0D, 0x0E, - ], - ], - vec![vec![ - 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - ]], - 0, - ), - ( - "Multiple Unordered Fragments", - vec![ - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, - 0x01, 0x02, 0x03, 0x04, - ], - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, - 0x0B, 0x0C, 0x0D, 0x0E, - ], - vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x81, - 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, - 0x06, 0x07, 0x08, 0x09, - ], - ], - vec![vec![ - 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - ]], - 0, - ), - ( - "Multiple Handshakes in Single Fragment", - vec![vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x30, /* record header */ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, - 0x01, 0x01, /*handshake msg 1*/ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, - 0x01, 0x01, /*handshake msg 2*/ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, - 0x01, 0x01, /*handshake msg 3*/ - ]], - vec![ - vec![ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, - 0xff, 0x01, 0x01, - ], - vec![ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, - 0xff, 0x01, 0x01, - ], - vec![ - 0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, - 0xff, 0x01, 0x01, - ], - ], - 0, - ), - // Ensure zero length fragments don't cause an infinite recursive loop which in turn causes - // a stack overflow. - ( - "Zero length fragment", - vec![vec![ - 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]], - vec![vec![ - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - ]], - 0, - ), - ]; - - for (name, inputs, expects, expected_epoch) in tests { - let mut fragment_buffer = FragmentBuffer::new(); - for frag in inputs { - let status = fragment_buffer.push(&frag)?; - assert!( - status, - "fragment_buffer didn't accept fragments for '{name}'" - ); - } - - for expected in expects { - let (out, epoch) = fragment_buffer.pop()?; - assert_eq!( - out, expected, - "fragment_buffer '{name}' push/pop: got {out:?}, want {expected:?}" - ); - - assert_eq!( - epoch, expected_epoch, - "fragment_buffer returned wrong epoch: got {epoch}, want {expected_epoch}" - ); - } - - let result = fragment_buffer.pop(); - assert!( - result.is_err(), - "fragment_buffer popped single buffer multiple times for '{name}'" - ); - } - - Ok(()) -} - -#[test] -fn test_fragment_buffer_overflow() -> Result<()> { - let mut fragment_buffer = FragmentBuffer::new(); - - fragment_buffer.push(&[ - 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, - ])?; - - let big_buffer = vec![0; 2_000_000]; - let result = fragment_buffer.push(&big_buffer); - - assert!( - result.is_err(), - "Pushing a buffer of size 2MB should have caused FragmentBuffer::push to return an error" - ); - - Ok(()) -} diff --git a/dtls/src/fragment_buffer/mod.rs b/dtls/src/fragment_buffer/mod.rs deleted file mode 100644 index d375bc2c2..000000000 --- a/dtls/src/fragment_buffer/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -#[cfg(test)] -mod fragment_buffer_test; - -use std::collections::HashMap; -use std::io::{BufWriter, Cursor}; - -use crate::content::*; -use crate::error::*; -use crate::handshake::handshake_header::*; -use crate::record_layer::record_layer_header::*; - -// 2 mb max buffer size -const FRAGMENT_BUFFER_MAX_SIZE: usize = 2_000_000; - -pub(crate) struct Fragment { - record_layer_header: RecordLayerHeader, - handshake_header: HandshakeHeader, - data: Vec, -} - -pub(crate) struct FragmentBuffer { - // map of MessageSequenceNumbers that hold slices of fragments - cache: HashMap>, - - current_message_sequence_number: u16, -} - -impl FragmentBuffer { - pub fn new() -> Self { - FragmentBuffer { - cache: HashMap::new(), - current_message_sequence_number: 0, - } - } - - // Attempts to push a DTLS packet to the FragmentBuffer - // when it returns true it means the FragmentBuffer has inserted and the buffer shouldn't be handled - // when an error returns it is fatal, and the DTLS connection should be stopped - pub fn push(&mut self, mut buf: &[u8]) -> Result { - let current_size = self.size(); - if current_size + buf.len() >= FRAGMENT_BUFFER_MAX_SIZE { - return Err(Error::ErrFragmentBufferOverflow { - new_size: current_size + buf.len(), - max_size: FRAGMENT_BUFFER_MAX_SIZE, - }); - } - - let mut reader = Cursor::new(buf); - let record_layer_header = RecordLayerHeader::unmarshal(&mut reader)?; - - // Fragment isn't a handshake, we don't need to handle it - if record_layer_header.content_type != ContentType::Handshake { - return Ok(false); - } - - buf = &buf[RECORD_LAYER_HEADER_SIZE..]; - while !buf.is_empty() { - let mut reader = Cursor::new(buf); - let handshake_header = HandshakeHeader::unmarshal(&mut reader)?; - - self.cache - .entry(handshake_header.message_sequence) - .or_default(); - - // end index should be the length of handshake header but if the handshake - // was fragmented, we should keep them all - let mut end = HANDSHAKE_HEADER_LENGTH + handshake_header.length as usize; - if end > buf.len() { - end = buf.len(); - } - - // Discard all headers, when rebuilding the packet we will re-build - let data = buf[HANDSHAKE_HEADER_LENGTH..end].to_vec(); - - if let Some(x) = self.cache.get_mut(&handshake_header.message_sequence) { - x.push(Fragment { - record_layer_header, - handshake_header, - data, - }); - } - buf = &buf[end..]; - } - - Ok(true) - } - - pub fn pop(&mut self) -> Result<(Vec, u16)> { - let seq_num = self.current_message_sequence_number; - if !self.cache.contains_key(&seq_num) { - return Err(Error::ErrEmptyFragment); - } - - let (content, epoch) = if let Some(frags) = self.cache.get_mut(&seq_num) { - let mut raw_message = vec![]; - // Recursively collect up - if !append_message(0, frags, &mut raw_message) { - return Err(Error::ErrEmptyFragment); - } - - let mut first_header = frags[0].handshake_header; - first_header.fragment_offset = 0; - first_header.fragment_length = first_header.length; - - let mut raw_header = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw_header.as_mut()); - if first_header.marshal(&mut writer).is_err() { - return Err(Error::ErrEmptyFragment); - } - } - - let message_epoch = frags[0].record_layer_header.epoch; - - raw_header.extend_from_slice(&raw_message); - - (raw_header, message_epoch) - } else { - return Err(Error::ErrEmptyFragment); - }; - - self.cache.remove(&seq_num); - self.current_message_sequence_number += 1; - - Ok((content, epoch)) - } - - fn size(&self) -> usize { - self.cache - .values() - .map(|fragment| fragment.iter().map(|f| f.data.len()).sum::()) - .sum() - } -} - -fn append_message(target_offset: u32, frags: &[Fragment], raw_message: &mut Vec) -> bool { - for f in frags { - if f.handshake_header.fragment_offset == target_offset { - let fragment_end = - f.handshake_header.fragment_offset + f.handshake_header.fragment_length; - - // NB: Order here is important, the `f.handshake_header.fragment_length != 0` - // MUST come before the recursive call. - if fragment_end != f.handshake_header.length - && f.handshake_header.fragment_length != 0 - && !append_message(fragment_end, frags, raw_message) - { - return false; - } - - let mut message = vec![]; - message.extend_from_slice(&f.data); - message.extend_from_slice(raw_message); - *raw_message = message; - return true; - } - } - - false -} diff --git a/dtls/src/handshake/handshake_cache.rs b/dtls/src/handshake/handshake_cache.rs deleted file mode 100644 index 38667bb28..000000000 --- a/dtls/src/handshake/handshake_cache.rs +++ /dev/null @@ -1,241 +0,0 @@ -#[cfg(test)] -mod handshake_cache_test; - -use std::collections::HashMap; -use std::io::BufReader; -use std::sync::Arc; - -use sha2::{Digest, Sha256}; -use tokio::sync::Mutex; - -use crate::cipher_suite::*; -use crate::handshake::*; - -#[derive(Clone, Debug)] -pub(crate) struct HandshakeCacheItem { - typ: HandshakeType, - is_client: bool, - epoch: u16, - message_sequence: u16, - data: Vec, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct HandshakeCachePullRule { - pub(crate) typ: HandshakeType, - pub(crate) epoch: u16, - pub(crate) is_client: bool, - pub(crate) optional: bool, -} - -#[derive(Clone)] -pub(crate) struct HandshakeCache { - cache: Arc>>, -} - -impl HandshakeCache { - pub(crate) fn new() -> Self { - HandshakeCache { - cache: Arc::new(Mutex::new(vec![])), - } - } - - pub(crate) async fn push( - &mut self, - data: Vec, - epoch: u16, - message_sequence: u16, - typ: HandshakeType, - is_client: bool, - ) -> bool { - let mut cache = self.cache.lock().await; - - for i in &*cache { - if i.message_sequence == message_sequence && i.is_client == is_client { - return false; - } - } - - cache.push(HandshakeCacheItem { - typ, - is_client, - epoch, - message_sequence, - data, - }); - - true - } - - // returns a list handshakes that match the requested rules - // the list will contain null entries for rules that can't be satisfied - // multiple entries may match a rule, but only the last match is returned (ie ClientHello with cookies) - pub(crate) async fn pull(&self, rules: &[HandshakeCachePullRule]) -> Vec { - let cache = self.cache.lock().await; - - let mut out = vec![]; - for r in rules { - let mut item: Option = None; - for c in &*cache { - if c.typ == r.typ && c.is_client == r.is_client && c.epoch == r.epoch { - if let Some(x) = &item { - if x.message_sequence < c.message_sequence { - item = Some(c.clone()); - } - } else { - item = Some(c.clone()); - } - } - } - - if let Some(c) = item { - out.push(c); - } - } - - out - } - - // full_pull_map pulls all handshakes between rules[0] to rules[len(rules)-1] as map. - pub(crate) async fn full_pull_map( - &self, - start_seq: isize, - rules: &[HandshakeCachePullRule], - ) -> Result<(isize, HashMap)> { - let cache = self.cache.lock().await; - - let mut ci = HashMap::new(); - for r in rules { - let mut item: Option = None; - for c in &*cache { - if c.typ == r.typ && c.is_client == r.is_client && c.epoch == r.epoch { - if let Some(x) = &item { - if x.message_sequence < c.message_sequence { - item = Some(c.clone()); - } - } else { - item = Some(c.clone()); - } - } - } - if !r.optional && item.is_none() { - // Missing mandatory message. - return Err(Error::Other("Missing mandatory message".to_owned())); - } - - if let Some(c) = item { - ci.insert(r.typ, c); - } - } - - let mut out = HashMap::new(); - let mut seq = start_seq; - for r in rules { - let t = r.typ; - if let Some(i) = ci.get(&t) { - let mut reader = BufReader::new(i.data.as_slice()); - let raw_handshake = Handshake::unmarshal(&mut reader)?; - if seq as u16 != raw_handshake.handshake_header.message_sequence { - // There is a gap. Some messages are not arrived. - return Err(Error::Other( - "There is a gap. Some messages are not arrived.".to_owned(), - )); - } - seq += 1; - out.insert(t, raw_handshake.handshake_message); - } - } - - Ok((seq, out)) - } - - // pull_and_merge calls pull and then merges the results, ignoring any null entries - pub(crate) async fn pull_and_merge(&self, rules: &[HandshakeCachePullRule]) -> Vec { - let mut merged = vec![]; - - for p in &self.pull(rules).await { - merged.extend_from_slice(&p.data); - } - - merged - } - - // session_hash returns the session hash for Extended Master Secret support - // https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4 - pub(crate) async fn session_hash( - &self, - hf: CipherSuiteHash, - epoch: u16, - additional: &[u8], - ) -> Result> { - let mut merged = vec![]; - - // Order defined by https://tools.ietf.org/html/rfc5246#section-7.3 - let handshake_buffer = self - .pull(&[ - HandshakeCachePullRule { - typ: HandshakeType::ClientHello, - epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHello, - epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerKeyExchange, - epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::CertificateRequest, - epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ServerHelloDone, - epoch, - is_client: false, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::Certificate, - epoch, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: HandshakeType::ClientKeyExchange, - epoch, - is_client: true, - optional: false, - }, - ]) - .await; - - for p in &handshake_buffer { - merged.extend_from_slice(&p.data); - } - - merged.extend_from_slice(additional); - - let mut hasher = match hf { - CipherSuiteHash::Sha256 => Sha256::new(), - }; - hasher.update(&merged); - let result = hasher.finalize(); - - Ok(result.as_slice().to_vec()) - } -} diff --git a/dtls/src/handshake/handshake_cache/handshake_cache_test.rs b/dtls/src/handshake/handshake_cache/handshake_cache_test.rs deleted file mode 100644 index b17391b18..000000000 --- a/dtls/src/handshake/handshake_cache/handshake_cache_test.rs +++ /dev/null @@ -1,658 +0,0 @@ -use super::*; - -#[tokio::test] -async fn test_handshake_cache_single_push() -> Result<()> { - let tests = vec![ - ( - "Single Push", - vec![HandshakeCacheItem { - typ: 0.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }], - vec![HandshakeCachePullRule { - typ: 0.into(), - epoch: 0, - is_client: true, - optional: false, - }], - vec![0x00], - ), - ( - "Multi Push", - vec![ - HandshakeCacheItem { - typ: 0.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: 2.into(), - is_client: true, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 0.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 2.into(), - epoch: 0, - is_client: true, - optional: false, - }, - ], - vec![0x00, 0x01, 0x02], - ), - ( - "Multi Push, Rules set order", - vec![ - HandshakeCacheItem { - typ: 2.into(), - is_client: true, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: 0.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 0.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 2.into(), - epoch: 0, - is_client: true, - optional: false, - }, - ], - vec![0x00, 0x01, 0x02], - ), - ( - "Multi Push, Dupe Seqnum", - vec![ - HandshakeCacheItem { - typ: 0.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 0.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - ], - vec![0x00, 0x01], - ), - ( - "Multi Push, Dupe Seqnum Client/Server", - vec![ - HandshakeCacheItem { - typ: 0.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: 1.into(), - is_client: false, - epoch: 0, - message_sequence: 1, - data: vec![0x02], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 0.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: false, - optional: false, - }, - ], - vec![0x00, 0x01, 0x02], - ), - ( - "Multi Push, Dupe Seqnum with Unique HandshakeType", - vec![ - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 2.into(), - is_client: true, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: 3.into(), - is_client: false, - epoch: 0, - message_sequence: 0, - data: vec![0x02], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 2.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 3.into(), - epoch: 0, - is_client: false, - optional: false, - }, - ], - vec![0x00, 0x01, 0x02], - ), - ( - "Multi Push, Wrong epoch", - vec![ - HandshakeCacheItem { - typ: 1.into(), - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: 2.into(), - is_client: true, - epoch: 1, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: 2.into(), - is_client: true, - epoch: 0, - message_sequence: 2, - data: vec![0x11], - }, - HandshakeCacheItem { - typ: 3.into(), - is_client: false, - epoch: 0, - message_sequence: 0, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: 3.into(), - is_client: false, - epoch: 1, - message_sequence: 0, - data: vec![0x12], - }, - HandshakeCacheItem { - typ: 3.into(), - is_client: false, - epoch: 2, - message_sequence: 0, - data: vec![0x12], - }, - ], - vec![ - HandshakeCachePullRule { - typ: 1.into(), - epoch: 0, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 2.into(), - epoch: 1, - is_client: true, - optional: false, - }, - HandshakeCachePullRule { - typ: 3.into(), - epoch: 0, - is_client: false, - optional: false, - }, - ], - vec![0x00, 0x01, 0x02], - ), - ]; - - for (name, inputs, rules, expected) in tests { - let mut h = HandshakeCache::new(); - for i in inputs { - h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client) - .await; - } - let verify_data = h.pull_and_merge(&rules).await; - assert_eq!( - verify_data, expected, - "handshakeCache '{name}' exp:{expected:?} actual {verify_data:?}", - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_handshake_cache_session_hash() -> Result<()> { - let tests = vec![ - ( - "Standard Handshake", - vec![ - HandshakeCacheItem { - typ: HandshakeType::ClientHello, - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHello, - is_client: false, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: HandshakeType::Certificate, - is_client: false, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerKeyExchange, - is_client: false, - epoch: 0, - message_sequence: 3, - data: vec![0x03], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHelloDone, - is_client: false, - epoch: 0, - message_sequence: 4, - data: vec![0x04], - }, - HandshakeCacheItem { - typ: HandshakeType::ClientKeyExchange, - is_client: true, - epoch: 0, - message_sequence: 5, - data: vec![0x05], - }, - ], - vec![ - 0x17, 0xe8, 0x8d, 0xb1, 0x87, 0xaf, 0xd6, 0x2c, 0x16, 0xe5, 0xde, 0xbf, 0x3e, 0x65, - 0x27, 0xcd, 0x00, 0x6b, 0xc0, 0x12, 0xbc, 0x90, 0xb5, 0x1a, 0x81, 0x0c, 0xd8, 0x0c, - 0x2d, 0x51, 0x1f, 0x43, - ], - ), - ( - "Handshake With Client Cert Request", - vec![ - HandshakeCacheItem { - typ: HandshakeType::ClientHello, - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHello, - is_client: false, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: HandshakeType::Certificate, - is_client: false, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerKeyExchange, - is_client: false, - epoch: 0, - message_sequence: 3, - data: vec![0x03], - }, - HandshakeCacheItem { - typ: HandshakeType::CertificateRequest, - is_client: false, - epoch: 0, - message_sequence: 4, - data: vec![0x04], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHelloDone, - is_client: false, - epoch: 0, - message_sequence: 5, - data: vec![0x05], - }, - HandshakeCacheItem { - typ: HandshakeType::ClientKeyExchange, - is_client: true, - epoch: 0, - message_sequence: 6, - data: vec![0x06], - }, - ], - vec![ - 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, - 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, - 0x25, 0x74, 0x9a, 0x6b, - ], - ), - ( - "Handshake Ignores after ClientKeyExchange", - vec![ - HandshakeCacheItem { - typ: HandshakeType::ClientHello, - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHello, - is_client: false, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: HandshakeType::Certificate, - is_client: false, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerKeyExchange, - is_client: false, - epoch: 0, - message_sequence: 3, - data: vec![0x03], - }, - HandshakeCacheItem { - typ: HandshakeType::CertificateRequest, - is_client: false, - epoch: 0, - message_sequence: 4, - data: vec![0x04], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHelloDone, - is_client: false, - epoch: 0, - message_sequence: 5, - data: vec![0x05], - }, - HandshakeCacheItem { - typ: HandshakeType::ClientKeyExchange, - is_client: true, - epoch: 0, - message_sequence: 6, - data: vec![0x06], - }, - HandshakeCacheItem { - typ: HandshakeType::CertificateVerify, - is_client: true, - epoch: 0, - message_sequence: 7, - data: vec![0x07], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: true, - epoch: 1, - message_sequence: 7, - data: vec![0x08], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: false, - epoch: 1, - message_sequence: 7, - data: vec![0x09], - }, - ], - vec![ - 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, - 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, - 0x25, 0x74, 0x9a, 0x6b, - ], - ), - ( - "Handshake Ignores wrong epoch", - vec![ - HandshakeCacheItem { - typ: HandshakeType::ClientHello, - is_client: true, - epoch: 0, - message_sequence: 0, - data: vec![0x00], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHello, - is_client: false, - epoch: 0, - message_sequence: 1, - data: vec![0x01], - }, - HandshakeCacheItem { - typ: HandshakeType::Certificate, - is_client: false, - epoch: 0, - message_sequence: 2, - data: vec![0x02], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerKeyExchange, - is_client: false, - epoch: 0, - message_sequence: 3, - data: vec![0x03], - }, - HandshakeCacheItem { - typ: HandshakeType::CertificateRequest, - is_client: false, - epoch: 0, - message_sequence: 4, - data: vec![0x04], - }, - HandshakeCacheItem { - typ: HandshakeType::ServerHelloDone, - is_client: false, - epoch: 0, - message_sequence: 5, - data: vec![0x05], - }, - HandshakeCacheItem { - typ: HandshakeType::ClientKeyExchange, - is_client: true, - epoch: 0, - message_sequence: 6, - data: vec![0x06], - }, - HandshakeCacheItem { - typ: HandshakeType::CertificateVerify, - is_client: true, - epoch: 0, - message_sequence: 7, - data: vec![0x07], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: true, - epoch: 0, - message_sequence: 7, - data: vec![0xf0], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: false, - epoch: 0, - message_sequence: 7, - data: vec![0xf1], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: true, - epoch: 1, - message_sequence: 7, - data: vec![0x08], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: false, - epoch: 1, - message_sequence: 7, - data: vec![0x09], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: true, - epoch: 0, - message_sequence: 7, - data: vec![0xf0], - }, - HandshakeCacheItem { - typ: HandshakeType::Finished, - is_client: false, - epoch: 0, - message_sequence: 7, - data: vec![0xf1], - }, - ], - vec![ - 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, - 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, - 0x25, 0x74, 0x9a, 0x6b, - ], - ), - ]; - - for (name, inputs, expected) in tests { - let mut h = HandshakeCache::new(); - for i in inputs { - h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client) - .await; - } - - let verify_data = h.session_hash(CipherSuiteHash::Sha256, 0, &[]).await?; - - assert_eq!( - verify_data, expected, - "handshakeCacheSessionHassh '{name}' exp: {expected:?} actual {verify_data:?}" - ); - } - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_header.rs b/dtls/src/handshake/handshake_header.rs deleted file mode 100644 index 70c7610a6..000000000 --- a/dtls/src/handshake/handshake_header.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::*; - -// msg_len for Handshake messages assumes an extra 12 bytes for -// sequence, Fragment and version information -pub(crate) const HANDSHAKE_HEADER_LENGTH: usize = 12; - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct HandshakeHeader { - pub(crate) handshake_type: HandshakeType, - pub(crate) length: u32, // uint24 in spec - pub(crate) message_sequence: u16, - pub(crate) fragment_offset: u32, // uint24 in spec - pub(crate) fragment_length: u32, // uint24 in spec -} - -impl HandshakeHeader { - pub fn size(&self) -> usize { - 1 + 3 + 2 + 3 + 3 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.handshake_type as u8)?; - writer.write_u24::(self.length)?; - writer.write_u16::(self.message_sequence)?; - writer.write_u24::(self.fragment_offset)?; - writer.write_u24::(self.fragment_length)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let handshake_type = reader.read_u8()?.into(); - let length = reader.read_u24::()?; - let message_sequence = reader.read_u16::()?; - let fragment_offset = reader.read_u24::()?; - let fragment_length = reader.read_u24::()?; - - Ok(HandshakeHeader { - handshake_type, - length, - message_sequence, - fragment_offset, - fragment_length, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_certificate.rs b/dtls/src/handshake/handshake_message_certificate.rs deleted file mode 100644 index c4e0034e4..000000000 --- a/dtls/src/handshake/handshake_message_certificate.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::*; - -#[cfg(test)] -mod handshake_message_certificate_test; - -const HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE: usize = 3; - -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct HandshakeMessageCertificate { - pub(crate) certificate: Vec>, -} - -impl HandshakeMessageCertificate { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::Certificate - } - - pub fn size(&self) -> usize { - let mut len = 3; - - for r in &self.certificate { - len += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE + r.len(); - } - - len - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - let mut payload_size = 0; - for r in &self.certificate { - payload_size += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE + r.len(); - } - - // Total Payload Size - writer.write_u24::(payload_size as u32)?; - - for r in &self.certificate { - // Certificate Length - writer.write_u24::(r.len() as u32)?; - - // Certificate body - writer.write_all(r)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let mut certificate: Vec> = vec![]; - - let payload_size = reader.read_u24::()? as usize; - let mut offset = 0; - while offset < payload_size { - let certificate_len = reader.read_u24::()? as usize; - offset += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE; - - let mut buf = vec![0; certificate_len]; - reader.read_exact(&mut buf)?; - offset += certificate_len; - - certificate.push(buf); - } - - Ok(HandshakeMessageCertificate { certificate }) - } -} diff --git a/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs b/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs deleted file mode 100644 index 582d16df8..000000000 --- a/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_certificate() -> Result<()> { - let raw_certificate = vec![ - 0x00, 0x01, 0x8c, 0x00, 0x01, 0x89, 0x30, 0x82, 0x01, 0x85, 0x30, 0x82, 0x01, 0x2b, 0x02, - 0x14, 0x7d, 0x00, 0xcf, 0x07, 0xfc, 0xe2, 0xb6, 0xb8, 0x3f, 0x72, 0xeb, 0x11, 0x36, 0x1b, - 0xf6, 0x39, 0xf1, 0x3c, 0x33, 0x41, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, - 0x04, 0x03, 0x02, 0x30, 0x45, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, - 0x02, 0x41, 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x53, - 0x6f, 0x6d, 0x65, 0x2d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, - 0x55, 0x04, 0x0a, 0x0c, 0x18, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x20, 0x57, - 0x69, 0x64, 0x67, 0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4c, 0x74, 0x64, 0x30, - 0x1e, 0x17, 0x0d, 0x31, 0x38, 0x31, 0x30, 0x32, 0x35, 0x30, 0x38, 0x35, 0x31, 0x31, 0x32, - 0x5a, 0x17, 0x0d, 0x31, 0x39, 0x31, 0x30, 0x32, 0x35, 0x30, 0x38, 0x35, 0x31, 0x31, 0x32, - 0x5a, 0x30, 0x45, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x41, - 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x53, 0x6f, 0x6d, - 0x65, 0x2d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x04, - 0x0a, 0x0c, 0x18, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x20, 0x57, 0x69, 0x64, - 0x67, 0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4c, 0x74, 0x64, 0x30, 0x59, 0x30, - 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, - 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xf9, 0xb1, 0x62, 0xd6, 0x07, 0xae, - 0xc3, 0x36, 0x34, 0xf5, 0xa3, 0x09, 0x39, 0x86, 0xe7, 0x3b, 0x59, 0xf7, 0x4a, 0x1d, 0xf4, - 0x97, 0x4f, 0x91, 0x40, 0x56, 0x1b, 0x3d, 0x6c, 0x5a, 0x38, 0x10, 0x15, 0x58, 0xf5, 0xa4, - 0xcc, 0xdf, 0xd5, 0xf5, 0x4a, 0x35, 0x40, 0x0f, 0x9f, 0x54, 0xb7, 0xe9, 0xe2, 0xae, 0x63, - 0x83, 0x6a, 0x4c, 0xfc, 0xc2, 0x5f, 0x78, 0xa0, 0xbb, 0x46, 0x54, 0xa4, 0xda, 0x30, 0x0a, - 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, - 0x02, 0x20, 0x47, 0x1a, 0x5f, 0x58, 0x2a, 0x74, 0x33, 0x6d, 0xed, 0xac, 0x37, 0x21, 0xfa, - 0x76, 0x5a, 0x4d, 0x78, 0x68, 0x1a, 0xdd, 0x80, 0xa4, 0xd4, 0xb7, 0x7f, 0x7d, 0x78, 0xb3, - 0xfb, 0xf3, 0x95, 0xfb, 0x02, 0x21, 0x00, 0xc0, 0x73, 0x30, 0xda, 0x2b, 0xc0, 0x0c, 0x9e, - 0xb2, 0x25, 0x0d, 0x46, 0xb0, 0xbc, 0x66, 0x7f, 0x71, 0x66, 0xbf, 0x16, 0xb3, 0x80, 0x78, - 0xd0, 0x0c, 0xef, 0xcc, 0xf5, 0xc1, 0x15, 0x0f, 0x58, - ]; - - let mut reader = BufReader::new(raw_certificate.as_slice()); - let c = HandshakeMessageCertificate::unmarshal(&mut reader)?; - //TODO: add x509 parse - // certificate, err := x509.ParseCertificate(c.certificate[0]) - // if err != nil { - // t.Error(err) - // } - // copyCertificatePrivateMembers(certificate, parsedCertificate) - // if !reflect.DeepEqual(certificate, parsedCertificate) { - // t.Errorf("handshakeMessageCertificate unmarshal: got %#v, want %#v", c, parsedCertificate) - // } - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_certificate, - "handshakeMessageCertificate marshal: got {raw:?}, want {raw_certificate:?}" - ); - - Ok(()) -} - -#[test] -fn test_empty_handshake_message_certificate() -> Result<()> { - let raw_certificate = vec![0x00, 0x00, 0x00]; - - let expected_certificate = HandshakeMessageCertificate { - certificate: vec![], - }; - - let mut reader = BufReader::new(raw_certificate.as_slice()); - let c = HandshakeMessageCertificate::unmarshal(&mut reader)?; - - assert_eq!( - c, expected_certificate, - "handshakeMessageCertificate unmarshal: got {c:?}, want {expected_certificate:?}", - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_certificate_request.rs b/dtls/src/handshake/handshake_message_certificate_request.rs deleted file mode 100644 index f48cecbb6..000000000 --- a/dtls/src/handshake/handshake_message_certificate_request.rs +++ /dev/null @@ -1,77 +0,0 @@ -#[cfg(test)] -mod handshake_message_certificate_request_test; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::*; -use crate::client_certificate_type::*; -use crate::signature_hash_algorithm::*; - -/* -A non-anonymous server can optionally request a certificate from -the client, if appropriate for the selected cipher suite. This -message, if sent, will immediately follow the ServerKeyExchange -message (if it is sent; otherwise, this message follows the -server's Certificate message). -*/ -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageCertificateRequest { - pub(crate) certificate_types: Vec, - pub(crate) signature_hash_algorithms: Vec, -} - -const HANDSHAKE_MESSAGE_CERTIFICATE_REQUEST_MIN_LENGTH: usize = 5; - -impl HandshakeMessageCertificateRequest { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::CertificateRequest - } - - pub fn size(&self) -> usize { - 1 + self.certificate_types.len() + 2 + self.signature_hash_algorithms.len() * 2 + 2 - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.certificate_types.len() as u8)?; - for v in &self.certificate_types { - writer.write_u8(*v as u8)?; - } - - writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; - for v in &self.signature_hash_algorithms { - writer.write_u8(v.hash as u8)?; - writer.write_u8(v.signature as u8)?; - } - - writer.write_all(&[0x00, 0x00])?; // Distinguished Names Length - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let certificate_types_length = reader.read_u8()?; - - let mut certificate_types = vec![]; - for _ in 0..certificate_types_length { - let cert_type = reader.read_u8()?.into(); - certificate_types.push(cert_type); - } - - let signature_hash_algorithms_length = reader.read_u16::()?; - - let mut signature_hash_algorithms = vec![]; - for _ in (0..signature_hash_algorithms_length).step_by(2) { - let hash = reader.read_u8()?.into(); - let signature = reader.read_u8()?.into(); - - signature_hash_algorithms.push(SignatureHashAlgorithm { hash, signature }); - } - - Ok(HandshakeMessageCertificateRequest { - certificate_types, - signature_hash_algorithms, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs b/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs deleted file mode 100644 index f261cde5b..000000000 --- a/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; -use crate::signature_hash_algorithm::*; - -#[test] -fn test_handshake_message_certificate_request() -> Result<()> { - let raw_certificate_request = vec![ - 0x02, 0x01, 0x40, 0x00, 0x0C, 0x04, 0x03, 0x04, 0x01, 0x05, 0x03, 0x05, 0x01, 0x06, 0x01, - 0x02, 0x01, 0x00, 0x00, - ]; - - let parsed_certificate_request = HandshakeMessageCertificateRequest { - certificate_types: vec![ - ClientCertificateType::RsaSign, - ClientCertificateType::EcdsaSign, - ], - signature_hash_algorithms: vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha1, - signature: SignatureAlgorithm::Rsa, - }, - ], - }; - - let mut reader = BufReader::new(raw_certificate_request.as_slice()); - let c = HandshakeMessageCertificateRequest::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_certificate_request, - "parsedCertificateRequest unmarshal: got {c:?}, want {parsed_certificate_request:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_certificate_request, - "parsedCertificateRequest marshal: got {raw:?}, want {raw_certificate_request:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_certificate_verify.rs b/dtls/src/handshake/handshake_message_certificate_verify.rs deleted file mode 100644 index 4b535ce19..000000000 --- a/dtls/src/handshake/handshake_message_certificate_verify.rs +++ /dev/null @@ -1,52 +0,0 @@ -#[cfg(test)] -mod handshake_message_certificate_verify_test; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::*; -use crate::signature_hash_algorithm::*; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageCertificateVerify { - pub(crate) algorithm: SignatureHashAlgorithm, - pub(crate) signature: Vec, -} - -const HANDSHAKE_MESSAGE_CERTIFICATE_VERIFY_MIN_LENGTH: usize = 4; - -impl HandshakeMessageCertificateVerify { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::CertificateVerify - } - - pub fn size(&self) -> usize { - 1 + 1 + 2 + self.signature.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.algorithm.hash as u8)?; - writer.write_u8(self.algorithm.signature as u8)?; - writer.write_u16::(self.signature.len() as u16)?; - writer.write_all(&self.signature)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let hash_algorithm = reader.read_u8()?.into(); - let signature_algorithm = reader.read_u8()?.into(); - let signature_length = reader.read_u16::()? as usize; - let mut signature = vec![0; signature_length]; - reader.read_exact(&mut signature)?; - - Ok(HandshakeMessageCertificateVerify { - algorithm: SignatureHashAlgorithm { - hash: hash_algorithm, - signature: signature_algorithm, - }, - signature, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs b/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs deleted file mode 100644 index b52cebc5b..000000000 --- a/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_certificate_request() -> Result<()> { - let raw_certificate_verify = vec![ - 0x04, 0x03, 0x00, 0x47, 0x30, 0x45, 0x02, 0x20, 0x6b, 0x63, 0x17, 0xad, 0xbe, 0xb7, 0x7b, - 0x0f, 0x86, 0x73, 0x39, 0x1e, 0xba, 0xb3, 0x50, 0x9c, 0xce, 0x9c, 0xe4, 0x8b, 0xe5, 0x13, - 0x07, 0x59, 0x18, 0x1f, 0xe5, 0xa0, 0x2b, 0xca, 0xa6, 0xad, 0x02, 0x21, 0x00, 0xd3, 0xb5, - 0x01, 0xbe, 0x87, 0x6c, 0x04, 0xa1, 0xdc, 0x28, 0xaa, 0x5f, 0xf7, 0x1e, 0x9c, 0xc0, 0x1e, - 0x00, 0x2c, 0xe5, 0x94, 0xbb, 0x03, 0x0e, 0xf1, 0xcb, 0x28, 0x22, 0x33, 0x23, 0x88, 0xad, - ]; - let parsed_certificate_verify = HandshakeMessageCertificateVerify { - algorithm: SignatureHashAlgorithm { - hash: raw_certificate_verify[0].into(), - signature: raw_certificate_verify[1].into(), - }, - signature: raw_certificate_verify[4..].to_vec(), - }; - - let mut reader = BufReader::new(raw_certificate_verify.as_slice()); - let c = HandshakeMessageCertificateVerify::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_certificate_verify, - "handshakeMessageCertificate unmarshal: got {c:?}, want {parsed_certificate_verify:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_certificate_verify, - "handshakeMessageCertificateVerify marshal: got {raw:?}, want {raw_certificate_verify:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_client_hello.rs b/dtls/src/handshake/handshake_message_client_hello.rs deleted file mode 100644 index f98340e03..000000000 --- a/dtls/src/handshake/handshake_message_client_hello.rs +++ /dev/null @@ -1,196 +0,0 @@ -#[cfg(test)] -mod handshake_message_client_hello_test; - -use std::fmt; -use std::io::{BufReader, BufWriter}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::handshake_random::*; -use super::*; -use crate::cipher_suite::*; -use crate::compression_methods::*; -use crate::extension::*; -use crate::record_layer::record_layer_header::*; - -/* -When a client first connects to a server it is required to send -the client hello as its first message. The client can also send a -client hello in response to a hello request or on its own -initiative in order to renegotiate the security parameters in an -existing connection. -*/ -#[derive(Clone)] -pub struct HandshakeMessageClientHello { - pub(crate) version: ProtocolVersion, - pub(crate) random: HandshakeRandom, - pub(crate) cookie: Vec, - - pub(crate) cipher_suites: Vec, - pub(crate) compression_methods: CompressionMethods, - pub(crate) extensions: Vec, -} - -impl PartialEq for HandshakeMessageClientHello { - fn eq(&self, other: &Self) -> bool { - if !(self.version == other.version - && self.random == other.random - && self.cookie == other.cookie - && self.compression_methods == other.compression_methods - && self.extensions == other.extensions - && self.cipher_suites.len() == other.cipher_suites.len()) - { - return false; - } - - for i in 0..self.cipher_suites.len() { - if self.cipher_suites[i] != other.cipher_suites[i] { - return false; - } - } - - true - } -} - -impl fmt::Debug for HandshakeMessageClientHello { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut cipher_suites_str = String::new(); - for cipher_suite in &self.cipher_suites { - cipher_suites_str += &cipher_suite.to_string(); - cipher_suites_str += " "; - } - let s = [ - format!("version: {:?} random: {:?}", self.version, self.random), - format!("cookie: {:?}", self.cookie), - format!("cipher_suites: {cipher_suites_str:?}"), - format!("compression_methods: {:?}", self.compression_methods), - format!("extensions: {:?}", self.extensions), - ]; - write!(f, "{}", s.join(" ")) - } -} - -const HANDSHAKE_MESSAGE_CLIENT_HELLO_VARIABLE_WIDTH_START: usize = 34; - -impl HandshakeMessageClientHello { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::ClientHello - } - - pub fn size(&self) -> usize { - let mut len = 0; - - len += 2; // version.major+minor - len += self.random.size(); - - // SessionID - len += 1; - - len += 1 + self.cookie.len(); - - len += 2 + 2 * self.cipher_suites.len(); - - len += self.compression_methods.size(); - - len += 2; - for extension in &self.extensions { - len += extension.size(); - } - - len - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - if self.cookie.len() > 255 { - return Err(Error::ErrCookieTooLong); - } - - writer.write_u8(self.version.major)?; - writer.write_u8(self.version.minor)?; - self.random.marshal(writer)?; - - // SessionID - writer.write_u8(0x00)?; - - writer.write_u8(self.cookie.len() as u8)?; - writer.write_all(&self.cookie)?; - - writer.write_u16::(2 * self.cipher_suites.len() as u16)?; - for cipher_suite in &self.cipher_suites { - writer.write_u16::(*cipher_suite as u16)?; - } - - self.compression_methods.marshal(writer)?; - - let mut extension_buffer = vec![]; - { - let mut extension_writer = BufWriter::<&mut Vec>::new(extension_buffer.as_mut()); - for extension in &self.extensions { - extension.marshal(&mut extension_writer)?; - } - } - - writer.write_u16::(extension_buffer.len() as u16)?; - writer.write_all(&extension_buffer)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let major = reader.read_u8()?; - let minor = reader.read_u8()?; - let random = HandshakeRandom::unmarshal(reader)?; - - // Session ID - reader.read_u8()?; - - let cookie_len = reader.read_u8()? as usize; - let mut cookie = vec![0; cookie_len]; - reader.read_exact(&mut cookie)?; - - let cipher_suites_len = reader.read_u16::()? as usize / 2; - let mut cipher_suites = vec![]; - for _ in 0..cipher_suites_len { - let id: CipherSuiteId = reader.read_u16::()?.into(); - //let cipher_suite = cipher_suite_for_id(id)?; - cipher_suites.push(id); - } - - let compression_methods = CompressionMethods::unmarshal(reader)?; - let mut extensions = vec![]; - - let extension_buffer_len = reader.read_u16::()? as usize; - let mut extension_buffer = vec![0u8; extension_buffer_len]; - reader.read_exact(&mut extension_buffer)?; - - let mut offset = 0; - while offset < extension_buffer_len { - let mut extension_reader = BufReader::new(&extension_buffer[offset..]); - if let Ok(extension) = Extension::unmarshal(&mut extension_reader) { - extensions.push(extension); - } else { - log::warn!( - "Unsupported Extension Type {} {}", - extension_buffer[offset], - extension_buffer[offset + 1] - ); - } - - let extension_len = - u16::from_be_bytes([extension_buffer[offset + 2], extension_buffer[offset + 3]]) - as usize; - offset += 4 + extension_len; - } - - Ok(HandshakeMessageClientHello { - version: ProtocolVersion { major, minor }, - random, - cookie, - - cipher_suites, - compression_methods, - extensions, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs b/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs deleted file mode 100644 index ba4260315..000000000 --- a/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::io::{BufReader, BufWriter}; -use std::time::{Duration, SystemTime}; - -use super::*; -use crate::curve::named_curve::*; -use crate::extension::extension_supported_elliptic_curves::*; - -#[test] -fn test_handshake_message_client_hello() -> Result<()> { - let raw_client_hello = vec![ - 0xfe, 0xfd, 0xb6, 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, - 0x62, 0x15, 0xad, 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, - 0xd8, 0x3d, 0xdc, 0x4b, 0x00, 0x14, 0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, - 0xd6, 0x6c, 0x57, 0xd0, 0x0e, 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8, 0x00, 0x04, 0xc0, 0x2b, - 0xc0, 0x0a, 0x01, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d, - ]; - - let gmt_unix_time = if let Some(unix_time) = - SystemTime::UNIX_EPOCH.checked_add(Duration::new(3056586332u64, 0)) - { - unix_time - } else { - SystemTime::UNIX_EPOCH - }; - let parsed_client_hello = HandshakeMessageClientHello { - version: ProtocolVersion { - major: 0xFE, - minor: 0xFD, - }, - random: HandshakeRandom { - gmt_unix_time, - random_bytes: [ - 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, 0xc9, - 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, 0x4b, - ], - }, - cookie: vec![ - 0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, 0xd6, 0x6c, 0x57, 0xd0, 0x0e, - 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8, - ], - cipher_suites: vec![ - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, - //Box::::default(), - //Box::::default(), - ], - compression_methods: CompressionMethods { - ids: vec![CompressionMethodId::Null], - }, - extensions: vec![Extension::SupportedEllipticCurves( - ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::X25519], - }, - )], - }; - - let mut reader = BufReader::new(raw_client_hello.as_slice()); - let c = HandshakeMessageClientHello::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_client_hello, - "handshakeMessageClientHello unmarshal: got {c:?}, want {parsed_client_hello:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_client_hello, - "handshakeMessageClientHello marshal: got {raw:?}, want {raw_client_hello:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_client_key_exchange.rs b/dtls/src/handshake/handshake_message_client_key_exchange.rs deleted file mode 100644 index 4c027ff8b..000000000 --- a/dtls/src/handshake/handshake_message_client_key_exchange.rs +++ /dev/null @@ -1,70 +0,0 @@ -#[cfg(test)] -mod handshake_message_client_key_exchange_test; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, WriteBytesExt}; - -use super::*; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageClientKeyExchange { - pub(crate) identity_hint: Vec, - pub(crate) public_key: Vec, -} - -impl HandshakeMessageClientKeyExchange { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::ClientKeyExchange - } - - pub fn size(&self) -> usize { - if !self.public_key.is_empty() { - 1 + self.public_key.len() - } else { - 2 + self.identity_hint.len() - } - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - if (!self.identity_hint.is_empty() && !self.public_key.is_empty()) - || (self.identity_hint.is_empty() && self.public_key.is_empty()) - { - return Err(Error::ErrInvalidClientKeyExchange); - } - - if !self.public_key.is_empty() { - writer.write_u8(self.public_key.len() as u8)?; - writer.write_all(&self.public_key)?; - } else { - writer.write_u16::(self.identity_hint.len() as u16)?; - writer.write_all(&self.identity_hint)?; - } - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let mut data = vec![]; - reader.read_to_end(&mut data)?; - - // If parsed as PSK return early and only populate PSK Identity Hint - let psk_length = ((data[0] as u16) << 8) | data[1] as u16; - if data.len() == psk_length as usize + 2 { - return Ok(HandshakeMessageClientKeyExchange { - identity_hint: data[2..].to_vec(), - public_key: vec![], - }); - } - - let public_key_length = data[0] as usize; - if data.len() != public_key_length + 1 { - return Err(Error::ErrBufferTooSmall); - } - - Ok(HandshakeMessageClientKeyExchange { - identity_hint: vec![], - public_key: data[1..].to_vec(), - }) - } -} diff --git a/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs b/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs deleted file mode 100644 index 0b9d6c178..000000000 --- a/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_client_key_exchange() -> Result<()> { - let raw_client_key_exchange = vec![ - 0x20, 0x26, 0x78, 0x4a, 0x78, 0x70, 0xc1, 0xf9, 0x71, 0xea, 0x50, 0x4a, 0xb5, 0xbb, 0x00, - 0x76, 0x02, 0x05, 0xda, 0xf7, 0xd0, 0x3f, 0xe3, 0xf7, 0x4e, 0x8a, 0x14, 0x6f, 0xb7, 0xe0, - 0xc0, 0xff, 0x54, - ]; - let parsed_client_key_exchange = HandshakeMessageClientKeyExchange { - identity_hint: vec![], - public_key: raw_client_key_exchange[1..].to_vec(), - }; - - let mut reader = BufReader::new(raw_client_key_exchange.as_slice()); - let c = HandshakeMessageClientKeyExchange::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_client_key_exchange, - "parsedCertificateRequest unmarshal: got {c:?}, want {parsed_client_key_exchange:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_client_key_exchange, - "handshakeMessageClientKeyExchange marshal: got {raw:?}, want {raw_client_key_exchange:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_finished.rs b/dtls/src/handshake/handshake_message_finished.rs deleted file mode 100644 index d20feb6e9..000000000 --- a/dtls/src/handshake/handshake_message_finished.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[cfg(test)] -mod handshake_message_finished_test; - -use std::io::{Read, Write}; - -use super::*; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageFinished { - pub(crate) verify_data: Vec, -} - -impl HandshakeMessageFinished { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::Finished - } - - pub fn size(&self) -> usize { - self.verify_data.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_all(&self.verify_data)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let mut verify_data: Vec = vec![]; - reader.read_to_end(&mut verify_data)?; - - Ok(HandshakeMessageFinished { verify_data }) - } -} diff --git a/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs b/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs deleted file mode 100644 index 7980ee909..000000000 --- a/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_finished() -> Result<()> { - let raw_finished = vec![ - 0x01, 0x01, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - ]; - let parsed_finished = HandshakeMessageFinished { - verify_data: raw_finished.clone(), - }; - - let mut reader = BufReader::new(raw_finished.as_slice()); - let c = HandshakeMessageFinished::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_finished, - "handshakeMessageFinished unmarshal: got {c:?}, want {parsed_finished:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_finished, - "handshakeMessageFinished marshal: got {raw:?}, want {raw_finished:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_hello_verify_request.rs b/dtls/src/handshake/handshake_message_hello_verify_request.rs deleted file mode 100644 index 1d738d9cb..000000000 --- a/dtls/src/handshake/handshake_message_hello_verify_request.rs +++ /dev/null @@ -1,72 +0,0 @@ -#[cfg(test)] -mod handshake_message_hello_verify_request_test; - -use std::io::{Read, Write}; - -use byteorder::{ReadBytesExt, WriteBytesExt}; - -use super::*; -use crate::record_layer::record_layer_header::*; - -/* - The definition of HelloVerifyRequest is as follows: - - struct { - ProtocolVersion server_version; - opaque cookie<0..2^8-1>; - } HelloVerifyRequest; - - The HelloVerifyRequest message type is hello_verify_request(3). - - When the client sends its ClientHello message to the server, the server - MAY respond with a HelloVerifyRequest message. This message contains - a stateless cookie generated using the technique of [PHOTURIS]. The - client MUST retransmit the ClientHello with the cookie added. - - https://tools.ietf.org/html/rfc6347#section-4.2.1 -*/ -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageHelloVerifyRequest { - pub(crate) version: ProtocolVersion, - pub(crate) cookie: Vec, -} - -impl HandshakeMessageHelloVerifyRequest { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::HelloVerifyRequest - } - - pub fn size(&self) -> usize { - 1 + 1 + 1 + self.cookie.len() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - if self.cookie.len() > 255 { - return Err(Error::ErrCookieTooLong); - } - - writer.write_u8(self.version.major)?; - writer.write_u8(self.version.minor)?; - writer.write_u8(self.cookie.len() as u8)?; - writer.write_all(&self.cookie)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let major = reader.read_u8()?; - let minor = reader.read_u8()?; - let cookie_length = reader.read_u8()?; - let mut cookie = vec![]; - reader.read_to_end(&mut cookie)?; - - if cookie.len() < cookie_length as usize { - return Err(Error::ErrBufferTooSmall); - } - - Ok(HandshakeMessageHelloVerifyRequest { - version: ProtocolVersion { major, minor }, - cookie, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs b/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs deleted file mode 100644 index 5a25ce008..000000000 --- a/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_hello_verify_request() -> Result<()> { - let raw_hello_verify_request = vec![ - 0xfe, 0xff, 0x14, 0x25, 0xfb, 0xee, 0xb3, 0x7c, 0x95, 0xcf, 0x00, 0xeb, 0xad, 0xe2, 0xef, - 0xc7, 0xfd, 0xbb, 0xed, 0xf7, 0x1f, 0x6c, 0xcd, - ]; - let parsed_hello_verify_request = HandshakeMessageHelloVerifyRequest { - version: ProtocolVersion { - major: 0xFE, - minor: 0xFF, - }, - cookie: vec![ - 0x25, 0xfb, 0xee, 0xb3, 0x7c, 0x95, 0xcf, 0x00, 0xeb, 0xad, 0xe2, 0xef, 0xc7, 0xfd, - 0xbb, 0xed, 0xf7, 0x1f, 0x6c, 0xcd, - ], - }; - - let mut reader = BufReader::new(raw_hello_verify_request.as_slice()); - let c = HandshakeMessageHelloVerifyRequest::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_hello_verify_request, - "parsed_hello_verify_request unmarshal: got {c:?}, want {parsed_hello_verify_request:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_hello_verify_request, - "parsed_hello_verify_request marshal: got {raw:?}, want {raw_hello_verify_request:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_server_hello.rs b/dtls/src/handshake/handshake_message_server_hello.rs deleted file mode 100644 index 6be32e458..000000000 --- a/dtls/src/handshake/handshake_message_server_hello.rs +++ /dev/null @@ -1,151 +0,0 @@ -#[cfg(test)] -mod handshake_message_server_hello_test; - -use std::fmt; -use std::io::{BufReader, BufWriter}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use super::handshake_random::*; -use super::*; -use crate::cipher_suite::*; -use crate::compression_methods::*; -use crate::extension::*; -use crate::record_layer::record_layer_header::*; - -/* -The server will send this message in response to a ClientHello -message when it was able to find an acceptable set of algorithms. -If it cannot find such a match, it will respond with a handshake -failure alert. -https://tools.ietf.org/html/rfc5246#section-7.4.1.3 -*/ -#[derive(Clone)] -pub struct HandshakeMessageServerHello { - pub(crate) version: ProtocolVersion, - pub(crate) random: HandshakeRandom, - - pub(crate) cipher_suite: CipherSuiteId, - pub(crate) compression_method: CompressionMethodId, - pub(crate) extensions: Vec, -} - -impl PartialEq for HandshakeMessageServerHello { - fn eq(&self, other: &Self) -> bool { - self.version == other.version - && self.random == other.random - && self.compression_method == other.compression_method - && self.extensions == other.extensions - && self.cipher_suite == other.cipher_suite - } -} - -impl fmt::Debug for HandshakeMessageServerHello { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = [ - format!("version: {:?} random: {:?}", self.version, self.random), - format!("cipher_suites: {:?}", self.cipher_suite), - format!("compression_method: {:?}", self.compression_method), - format!("extensions: {:?}", self.extensions), - ]; - write!(f, "{}", s.join(" ")) - } -} - -impl HandshakeMessageServerHello { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::ServerHello - } - - pub fn size(&self) -> usize { - let mut len = 2 + self.random.size(); - - // SessionID - len += 1; - - len += 2; - - len += 1; - - len += 2; - for extension in &self.extensions { - len += extension.size(); - } - - len - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - writer.write_u8(self.version.major)?; - writer.write_u8(self.version.minor)?; - self.random.marshal(writer)?; - - // SessionID - writer.write_u8(0x00)?; - - writer.write_u16::(self.cipher_suite as u16)?; - - writer.write_u8(self.compression_method as u8)?; - - let mut extension_buffer = vec![]; - { - let mut extension_writer = BufWriter::<&mut Vec>::new(extension_buffer.as_mut()); - for extension in &self.extensions { - extension.marshal(&mut extension_writer)?; - } - } - - writer.write_u16::(extension_buffer.len() as u16)?; - writer.write_all(&extension_buffer)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let major = reader.read_u8()?; - let minor = reader.read_u8()?; - let random = HandshakeRandom::unmarshal(reader)?; - - // Session ID - let session_id_len = reader.read_u8()? as usize; - let mut session_id_buffer = vec![0u8; session_id_len]; - reader.read_exact(&mut session_id_buffer)?; - - let cipher_suite: CipherSuiteId = reader.read_u16::()?.into(); - - let compression_method = reader.read_u8()?.into(); - let mut extensions = vec![]; - - let extension_buffer_len = reader.read_u16::()? as usize; - let mut extension_buffer = vec![0u8; extension_buffer_len]; - reader.read_exact(&mut extension_buffer)?; - - let mut offset = 0; - while offset < extension_buffer_len { - let mut extension_reader = BufReader::new(&extension_buffer[offset..]); - if let Ok(extension) = Extension::unmarshal(&mut extension_reader) { - extensions.push(extension); - } else { - log::warn!( - "Unsupported Extension Type {} {}", - extension_buffer[offset], - extension_buffer[offset + 1] - ); - } - - let extension_len = - u16::from_be_bytes([extension_buffer[offset + 2], extension_buffer[offset + 3]]) - as usize; - offset += 4 + extension_len; - } - - Ok(HandshakeMessageServerHello { - version: ProtocolVersion { major, minor }, - random, - - cipher_suite, - compression_method, - extensions, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs b/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs deleted file mode 100644 index ba906e06f..000000000 --- a/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::io::{BufReader, BufWriter}; -use std::time::{Duration, SystemTime}; - -use super::*; - -#[test] -fn test_handshake_message_server_hello() -> Result<()> { - let raw_server_hello = vec![ - 0xfe, 0xfd, 0x21, 0x63, 0x32, 0x21, 0x81, 0x0e, 0x98, 0x6c, 0x85, 0x3d, 0xa4, 0x39, 0xaf, - 0x5f, 0xd6, 0x5c, 0xcc, 0x20, 0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e, - 0xcf, 0x63, 0x84, 0x28, 0x00, 0xc0, 0x2b, 0x00, 0x00, 0x00, - ]; - - let gmt_unix_time = if let Some(unix_time) = - SystemTime::UNIX_EPOCH.checked_add(Duration::new(560149025u64, 0)) - { - unix_time - } else { - SystemTime::UNIX_EPOCH - }; - let parsed_server_hello = HandshakeMessageServerHello { - version: ProtocolVersion { - major: 0xFE, - minor: 0xFD, - }, - random: HandshakeRandom { - gmt_unix_time, - random_bytes: [ - 0x81, 0x0e, 0x98, 0x6c, 0x85, 0x3d, 0xa4, 0x39, 0xaf, 0x5f, 0xd6, 0x5c, 0xcc, 0x20, - 0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e, 0xcf, 0x63, 0x84, 0x28, - ], - }, - cipher_suite: CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, - compression_method: CompressionMethodId::Null, - extensions: vec![], - }; - - let mut reader = BufReader::new(raw_server_hello.as_slice()); - let c = HandshakeMessageServerHello::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_server_hello, - "handshakeMessageServerHello unmarshal: got {c:?}, want {parsed_server_hello:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_server_hello, - "handshakeMessageServerHello marshal: got {raw:?}, want {raw_server_hello:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_server_hello_done.rs b/dtls/src/handshake/handshake_message_server_hello_done.rs deleted file mode 100644 index bae58f218..000000000 --- a/dtls/src/handshake/handshake_message_server_hello_done.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[cfg(test)] -mod handshake_message_server_hello_done_test; - -use std::io::{Read, Write}; - -use super::*; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageServerHelloDone; - -impl HandshakeMessageServerHelloDone { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::ServerHelloDone - } - - pub fn size(&self) -> usize { - 0 - } - - pub fn marshal(&self, _writer: &mut W) -> Result<()> { - Ok(()) - } - - pub fn unmarshal(_reader: &mut R) -> Result { - Ok(HandshakeMessageServerHelloDone {}) - } -} diff --git a/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs b/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs deleted file mode 100644 index b9d3b8359..000000000 --- a/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_server_hello_done() -> Result<()> { - let raw_server_hello_done = vec![]; - let parsed_server_hello_done = HandshakeMessageServerHelloDone {}; - - let mut reader = BufReader::new(raw_server_hello_done.as_slice()); - let c = HandshakeMessageServerHelloDone::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_server_hello_done, - "handshakeMessageServerHelloDone unmarshal: got {c:?}, want {parsed_server_hello_done:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_server_hello_done, - "handshakeMessageServerHelloDone marshal: got {raw:?}, want {raw_server_hello_done:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_message_server_key_exchange.rs b/dtls/src/handshake/handshake_message_server_key_exchange.rs deleted file mode 100644 index e84384275..000000000 --- a/dtls/src/handshake/handshake_message_server_key_exchange.rs +++ /dev/null @@ -1,133 +0,0 @@ -#[cfg(test)] -mod handshake_message_server_key_exchange_test; - -use std::io::{Read, Write}; - -use byteorder::{BigEndian, WriteBytesExt}; - -use super::*; -use crate::curve::named_curve::*; -use crate::curve::*; -use crate::signature_hash_algorithm::*; - -// Structure supports ECDH and PSK -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeMessageServerKeyExchange { - pub(crate) identity_hint: Vec, - - pub(crate) elliptic_curve_type: EllipticCurveType, - pub(crate) named_curve: NamedCurve, - pub(crate) public_key: Vec, - pub(crate) algorithm: SignatureHashAlgorithm, - pub(crate) signature: Vec, -} - -impl HandshakeMessageServerKeyExchange { - pub fn handshake_type(&self) -> HandshakeType { - HandshakeType::ServerKeyExchange - } - - pub fn size(&self) -> usize { - if !self.identity_hint.is_empty() { - 2 + self.identity_hint.len() - } else { - 1 + 2 + 1 + self.public_key.len() + 2 + 2 + self.signature.len() - } - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - if !self.identity_hint.is_empty() { - writer.write_u16::(self.identity_hint.len() as u16)?; - writer.write_all(&self.identity_hint)?; - return Ok(writer.flush()?); - } - - writer.write_u8(self.elliptic_curve_type as u8)?; - writer.write_u16::(self.named_curve as u16)?; - - writer.write_u8(self.public_key.len() as u8)?; - writer.write_all(&self.public_key)?; - - writer.write_u8(self.algorithm.hash as u8)?; - writer.write_u8(self.algorithm.signature as u8)?; - - writer.write_u16::(self.signature.len() as u16)?; - writer.write_all(&self.signature)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let mut data = vec![]; - reader.read_to_end(&mut data)?; - - // If parsed as PSK return early and only populate PSK Identity Hint - let psk_length = ((data[0] as u16) << 8) | data[1] as u16; - if data.len() == psk_length as usize + 2 { - return Ok(HandshakeMessageServerKeyExchange { - identity_hint: data[2..].to_vec(), - - elliptic_curve_type: EllipticCurveType::Unsupported, - named_curve: NamedCurve::Unsupported, - public_key: vec![], - algorithm: SignatureHashAlgorithm { - hash: HashAlgorithm::Unsupported, - signature: SignatureAlgorithm::Unsupported, - }, - signature: vec![], - }); - } - - let elliptic_curve_type = data[0].into(); - if data[1..].len() < 2 { - return Err(Error::ErrBufferTooSmall); - } - - let named_curve = (((data[1] as u16) << 8) | data[2] as u16).into(); - if data.len() < 4 { - return Err(Error::ErrBufferTooSmall); - } - - let public_key_length = data[3] as usize; - let mut offset = 4 + public_key_length; - if data.len() < offset { - return Err(Error::ErrBufferTooSmall); - } - let public_key = data[4..offset].to_vec(); - if data.len() <= offset { - return Err(Error::ErrBufferTooSmall); - } - - let hash_algorithm = data[offset].into(); - offset += 1; - if data.len() <= offset { - return Err(Error::ErrBufferTooSmall); - } - - let signature_algorithm = data[offset].into(); - offset += 1; - if data.len() < offset + 2 { - return Err(Error::ErrBufferTooSmall); - } - - let signature_length = (((data[offset] as u16) << 8) | data[offset + 1] as u16) as usize; - offset += 2; - if data.len() < offset + signature_length { - return Err(Error::ErrBufferTooSmall); - } - let signature = data[offset..offset + signature_length].to_vec(); - - Ok(HandshakeMessageServerKeyExchange { - identity_hint: vec![], - - elliptic_curve_type, - named_curve, - public_key, - algorithm: SignatureHashAlgorithm { - hash: hash_algorithm, - signature: signature_algorithm, - }, - signature, - }) - } -} diff --git a/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs b/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs deleted file mode 100644 index 4fd7adf53..000000000 --- a/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; - -#[test] -fn test_handshake_message_server_key_exchange() -> Result<()> { - let raw_server_key_exchange = vec![ - 0x03, 0x00, 0x1d, 0x41, 0x04, 0x0c, 0xb9, 0xa3, 0xb9, 0x90, 0x71, 0x35, 0x4a, 0x08, 0x66, - 0xaf, 0xd6, 0x88, 0x58, 0x29, 0x69, 0x98, 0xf1, 0x87, 0x0f, 0xb5, 0xa8, 0xcd, 0x92, 0xf6, - 0x2b, 0x08, 0x0c, 0xd4, 0x16, 0x5b, 0xcc, 0x81, 0xf2, 0x58, 0x91, 0x8e, 0x62, 0xdf, 0xc1, - 0xec, 0x72, 0xe8, 0x47, 0x24, 0x42, 0x96, 0xb8, 0x7b, 0xee, 0xe7, 0x0d, 0xdc, 0x44, 0xec, - 0xf3, 0x97, 0x6b, 0x1b, 0x45, 0x28, 0xac, 0x3f, 0x35, 0x02, 0x03, 0x00, 0x47, 0x30, 0x45, - 0x02, 0x21, 0x00, 0xb2, 0x0b, 0x22, 0x95, 0x3d, 0x56, 0x57, 0x6a, 0x3f, 0x85, 0x30, 0x6f, - 0x55, 0xc3, 0xf4, 0x24, 0x1b, 0x21, 0x07, 0xe5, 0xdf, 0xba, 0x24, 0x02, 0x68, 0x95, 0x1f, - 0x6e, 0x13, 0xbd, 0x9f, 0xaa, 0x02, 0x20, 0x49, 0x9c, 0x9d, 0xdf, 0x84, 0x60, 0x33, 0x27, - 0x96, 0x9e, 0x58, 0x6d, 0x72, 0x13, 0xe7, 0x3a, 0xe8, 0xdf, 0x43, 0x75, 0xc7, 0xb9, 0x37, - 0x6e, 0x90, 0xe5, 0x3b, 0x81, 0xd4, 0xda, 0x68, 0xcd, - ]; - let parsed_server_key_exchange = HandshakeMessageServerKeyExchange { - identity_hint: vec![], - elliptic_curve_type: EllipticCurveType::NamedCurve, - named_curve: NamedCurve::X25519, - public_key: raw_server_key_exchange[4..69].to_vec(), - algorithm: SignatureHashAlgorithm { - hash: HashAlgorithm::Sha1, - signature: SignatureAlgorithm::Ecdsa, - }, - - signature: raw_server_key_exchange[73..144].to_vec(), - }; - - let mut reader = BufReader::new(raw_server_key_exchange.as_slice()); - let c = HandshakeMessageServerKeyExchange::unmarshal(&mut reader)?; - assert_eq!( - c, parsed_server_key_exchange, - "handshakeMessageServerKeyExchange unmarshal: got {c:?}, want {parsed_server_key_exchange:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - c.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_server_key_exchange, - "handshakeMessageServerKeyExchange marshal: got {raw:?}, want {raw_server_key_exchange:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/handshake_random.rs b/dtls/src/handshake/handshake_random.rs deleted file mode 100644 index 4ff468a45..000000000 --- a/dtls/src/handshake/handshake_random.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::io::{self, Read, Write}; -use std::time::{Duration, SystemTime}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use rand::Rng; - -pub const RANDOM_BYTES_LENGTH: usize = 28; -pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; - -// https://tools.ietf.org/html/rfc4346#section-7.4.1.2 -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HandshakeRandom { - pub gmt_unix_time: SystemTime, - pub random_bytes: [u8; RANDOM_BYTES_LENGTH], -} - -impl Default for HandshakeRandom { - fn default() -> Self { - HandshakeRandom { - gmt_unix_time: SystemTime::UNIX_EPOCH, - random_bytes: [0u8; RANDOM_BYTES_LENGTH], - } - } -} - -impl HandshakeRandom { - pub fn size(&self) -> usize { - 4 + RANDOM_BYTES_LENGTH - } - - pub fn marshal(&self, writer: &mut W) -> io::Result<()> { - let secs = match self.gmt_unix_time.duration_since(SystemTime::UNIX_EPOCH) { - Ok(d) => d.as_secs() as u32, - Err(_) => 0, - }; - writer.write_u32::(secs)?; - writer.write_all(&self.random_bytes)?; - - writer.flush() - } - - pub fn unmarshal(reader: &mut R) -> io::Result { - let secs = reader.read_u32::()?; - let gmt_unix_time = if let Some(unix_time) = - SystemTime::UNIX_EPOCH.checked_add(Duration::new(secs as u64, 0)) - { - unix_time - } else { - SystemTime::UNIX_EPOCH - }; - - let mut random_bytes = [0u8; RANDOM_BYTES_LENGTH]; - reader.read_exact(&mut random_bytes)?; - - Ok(HandshakeRandom { - gmt_unix_time, - random_bytes, - }) - } - - // populate fills the HandshakeRandom with random values - // may be called multiple times - pub fn populate(&mut self) { - self.gmt_unix_time = SystemTime::now(); - rand::thread_rng().fill(&mut self.random_bytes); - } -} diff --git a/dtls/src/handshake/handshake_test.rs b/dtls/src/handshake/handshake_test.rs deleted file mode 100644 index a61da45c6..000000000 --- a/dtls/src/handshake/handshake_test.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::io::{BufReader, BufWriter}; -use std::time::{Duration, SystemTime}; - -use super::*; -use crate::compression_methods::*; -use crate::handshake::handshake_message_client_hello::*; -use crate::handshake::handshake_random::HandshakeRandom; -use crate::record_layer::record_layer_header::ProtocolVersion; - -#[test] -fn test_handshake_message() -> Result<()> { - let raw_handshake_message = vec![ - 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xfe, 0xfd, 0xb6, - 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, - 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, - 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - let parsed_handshake = Handshake { - handshake_header: HandshakeHeader { - handshake_type: HandshakeType::ClientHello, - length: 0x29, - message_sequence: 0, - fragment_offset: 0, - fragment_length: 0x29, - }, - handshake_message: HandshakeMessage::ClientHello(HandshakeMessageClientHello { - version: ProtocolVersion { - major: 0xFE, - minor: 0xFD, - }, - random: HandshakeRandom { - gmt_unix_time: if let Some(unix_time) = - SystemTime::UNIX_EPOCH.checked_add(Duration::new(3056586332u64, 0)) - { - unix_time - } else { - SystemTime::UNIX_EPOCH - }, - random_bytes: [ - 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, - 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, - 0xdc, 0x4b, - ], - }, - cookie: vec![], - cipher_suites: vec![], - compression_methods: CompressionMethods { ids: vec![] }, - extensions: vec![], - }), - }; - - let mut reader = BufReader::new(raw_handshake_message.as_slice()); - let h = Handshake::unmarshal(&mut reader)?; - assert_eq!( - h, parsed_handshake, - "handshakeMessageClientHello unmarshal: got {h:?}, want {parsed_handshake:?}" - ); - - let mut raw = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); - h.marshal(&mut writer)?; - } - assert_eq!( - raw, raw_handshake_message, - "handshakeMessageClientHello marshal: got {raw:?}, want {raw_handshake_message:?}" - ); - - Ok(()) -} diff --git a/dtls/src/handshake/mod.rs b/dtls/src/handshake/mod.rs deleted file mode 100644 index a71f44f34..000000000 --- a/dtls/src/handshake/mod.rs +++ /dev/null @@ -1,238 +0,0 @@ -pub mod handshake_cache; -pub mod handshake_header; -pub mod handshake_message_certificate; -pub mod handshake_message_certificate_request; -pub mod handshake_message_certificate_verify; -pub mod handshake_message_client_hello; -pub mod handshake_message_client_key_exchange; -pub mod handshake_message_finished; -pub mod handshake_message_hello_verify_request; -pub mod handshake_message_server_hello; -pub mod handshake_message_server_hello_done; -pub mod handshake_message_server_key_exchange; -pub mod handshake_random; - -#[cfg(test)] -mod handshake_test; - -use std::fmt; -use std::io::{Read, Write}; - -use handshake_header::*; -use handshake_message_certificate::*; -use handshake_message_certificate_request::*; -use handshake_message_certificate_verify::*; -use handshake_message_client_hello::*; -use handshake_message_client_key_exchange::*; -use handshake_message_finished::*; -use handshake_message_hello_verify_request::*; -use handshake_message_server_hello::*; -use handshake_message_server_hello_done::*; -use handshake_message_server_key_exchange::*; - -use super::content::*; -use super::error::*; - -// https://tools.ietf.org/html/rfc5246#section-7.4 -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum HandshakeType { - HelloRequest = 0, - ClientHello = 1, - ServerHello = 2, - HelloVerifyRequest = 3, - Certificate = 11, - ServerKeyExchange = 12, - CertificateRequest = 13, - ServerHelloDone = 14, - CertificateVerify = 15, - ClientKeyExchange = 16, - Finished = 20, - #[default] - Invalid, -} - -impl fmt::Display for HandshakeType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - HandshakeType::HelloRequest => write!(f, "HelloRequest"), - HandshakeType::ClientHello => write!(f, "ClientHello"), - HandshakeType::ServerHello => write!(f, "ServerHello"), - HandshakeType::HelloVerifyRequest => write!(f, "HelloVerifyRequest"), - HandshakeType::Certificate => write!(f, "Certificate"), - HandshakeType::ServerKeyExchange => write!(f, "ServerKeyExchange"), - HandshakeType::CertificateRequest => write!(f, "CertificateRequest"), - HandshakeType::ServerHelloDone => write!(f, "ServerHelloDone"), - HandshakeType::CertificateVerify => write!(f, "CertificateVerify"), - HandshakeType::ClientKeyExchange => write!(f, "ClientKeyExchange"), - HandshakeType::Finished => write!(f, "Finished"), - HandshakeType::Invalid => write!(f, "Invalid"), - } - } -} - -impl From for HandshakeType { - fn from(val: u8) -> Self { - match val { - 0 => HandshakeType::HelloRequest, - 1 => HandshakeType::ClientHello, - 2 => HandshakeType::ServerHello, - 3 => HandshakeType::HelloVerifyRequest, - 11 => HandshakeType::Certificate, - 12 => HandshakeType::ServerKeyExchange, - 13 => HandshakeType::CertificateRequest, - 14 => HandshakeType::ServerHelloDone, - 15 => HandshakeType::CertificateVerify, - 16 => HandshakeType::ClientKeyExchange, - 20 => HandshakeType::Finished, - _ => HandshakeType::Invalid, - } - } -} - -#[derive(PartialEq, Debug, Clone)] -pub enum HandshakeMessage { - //HelloRequest(errNotImplemented), - ClientHello(HandshakeMessageClientHello), - ServerHello(HandshakeMessageServerHello), - HelloVerifyRequest(HandshakeMessageHelloVerifyRequest), - Certificate(HandshakeMessageCertificate), - ServerKeyExchange(HandshakeMessageServerKeyExchange), - CertificateRequest(HandshakeMessageCertificateRequest), - ServerHelloDone(HandshakeMessageServerHelloDone), - CertificateVerify(HandshakeMessageCertificateVerify), - ClientKeyExchange(HandshakeMessageClientKeyExchange), - Finished(HandshakeMessageFinished), -} - -impl HandshakeMessage { - pub fn handshake_type(&self) -> HandshakeType { - match self { - HandshakeMessage::ClientHello(msg) => msg.handshake_type(), - HandshakeMessage::ServerHello(msg) => msg.handshake_type(), - HandshakeMessage::HelloVerifyRequest(msg) => msg.handshake_type(), - HandshakeMessage::Certificate(msg) => msg.handshake_type(), - HandshakeMessage::ServerKeyExchange(msg) => msg.handshake_type(), - HandshakeMessage::CertificateRequest(msg) => msg.handshake_type(), - HandshakeMessage::ServerHelloDone(msg) => msg.handshake_type(), - HandshakeMessage::CertificateVerify(msg) => msg.handshake_type(), - HandshakeMessage::ClientKeyExchange(msg) => msg.handshake_type(), - HandshakeMessage::Finished(msg) => msg.handshake_type(), - } - } - - pub fn size(&self) -> usize { - match self { - HandshakeMessage::ClientHello(msg) => msg.size(), - HandshakeMessage::ServerHello(msg) => msg.size(), - HandshakeMessage::HelloVerifyRequest(msg) => msg.size(), - HandshakeMessage::Certificate(msg) => msg.size(), - HandshakeMessage::ServerKeyExchange(msg) => msg.size(), - HandshakeMessage::CertificateRequest(msg) => msg.size(), - HandshakeMessage::ServerHelloDone(msg) => msg.size(), - HandshakeMessage::CertificateVerify(msg) => msg.size(), - HandshakeMessage::ClientKeyExchange(msg) => msg.size(), - HandshakeMessage::Finished(msg) => msg.size(), - } - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - match self { - HandshakeMessage::ClientHello(msg) => msg.marshal(writer)?, - HandshakeMessage::ServerHello(msg) => msg.marshal(writer)?, - HandshakeMessage::HelloVerifyRequest(msg) => msg.marshal(writer)?, - HandshakeMessage::Certificate(msg) => msg.marshal(writer)?, - HandshakeMessage::ServerKeyExchange(msg) => msg.marshal(writer)?, - HandshakeMessage::CertificateRequest(msg) => msg.marshal(writer)?, - HandshakeMessage::ServerHelloDone(msg) => msg.marshal(writer)?, - HandshakeMessage::CertificateVerify(msg) => msg.marshal(writer)?, - HandshakeMessage::ClientKeyExchange(msg) => msg.marshal(writer)?, - HandshakeMessage::Finished(msg) => msg.marshal(writer)?, - } - - Ok(()) - } -} - -// The handshake protocol is responsible for selecting a cipher spec and -// generating a master secret, which together comprise the primary -// cryptographic parameters associated with a secure session. The -// handshake protocol can also optionally authenticate parties who have -// certificates signed by a trusted certificate authority. -// https://tools.ietf.org/html/rfc5246#section-7.3 -#[derive(PartialEq, Debug, Clone)] -pub struct Handshake { - pub(crate) handshake_header: HandshakeHeader, - pub(crate) handshake_message: HandshakeMessage, -} - -impl Handshake { - pub fn new(handshake_message: HandshakeMessage) -> Self { - Handshake { - handshake_header: HandshakeHeader { - handshake_type: handshake_message.handshake_type(), - length: handshake_message.size() as u32, - message_sequence: 0, - fragment_offset: 0, - fragment_length: handshake_message.size() as u32, - }, - handshake_message, - } - } - - pub fn content_type(&self) -> ContentType { - ContentType::Handshake - } - - pub fn size(&self) -> usize { - self.handshake_header.size() + self.handshake_message.size() - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - self.handshake_header.marshal(writer)?; - self.handshake_message.marshal(writer)?; - Ok(()) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let handshake_header = HandshakeHeader::unmarshal(reader)?; - - let handshake_message = match handshake_header.handshake_type { - HandshakeType::ClientHello => { - HandshakeMessage::ClientHello(HandshakeMessageClientHello::unmarshal(reader)?) - } - HandshakeType::ServerHello => { - HandshakeMessage::ServerHello(HandshakeMessageServerHello::unmarshal(reader)?) - } - HandshakeType::HelloVerifyRequest => HandshakeMessage::HelloVerifyRequest( - HandshakeMessageHelloVerifyRequest::unmarshal(reader)?, - ), - HandshakeType::Certificate => { - HandshakeMessage::Certificate(HandshakeMessageCertificate::unmarshal(reader)?) - } - HandshakeType::ServerKeyExchange => HandshakeMessage::ServerKeyExchange( - HandshakeMessageServerKeyExchange::unmarshal(reader)?, - ), - HandshakeType::CertificateRequest => HandshakeMessage::CertificateRequest( - HandshakeMessageCertificateRequest::unmarshal(reader)?, - ), - HandshakeType::ServerHelloDone => HandshakeMessage::ServerHelloDone( - HandshakeMessageServerHelloDone::unmarshal(reader)?, - ), - HandshakeType::CertificateVerify => HandshakeMessage::CertificateVerify( - HandshakeMessageCertificateVerify::unmarshal(reader)?, - ), - HandshakeType::ClientKeyExchange => HandshakeMessage::ClientKeyExchange( - HandshakeMessageClientKeyExchange::unmarshal(reader)?, - ), - HandshakeType::Finished => { - HandshakeMessage::Finished(HandshakeMessageFinished::unmarshal(reader)?) - } - _ => return Err(Error::ErrNotImplemented), - }; - - Ok(Handshake { - handshake_header, - handshake_message, - }) - } -} diff --git a/dtls/src/handshaker.rs b/dtls/src/handshaker.rs deleted file mode 100644 index b6e1a9e2b..000000000 --- a/dtls/src/handshaker.rs +++ /dev/null @@ -1,429 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; - -use log::*; - -use crate::cipher_suite::*; -use crate::config::*; -use crate::conn::*; -use crate::content::*; -use crate::crypto::*; -use crate::error::*; -use crate::extension::extension_use_srtp::*; -use crate::signature_hash_algorithm::*; - -use rustls::client::danger::ServerCertVerifier; -use rustls::pki_types::CertificateDer; -use rustls::server::danger::ClientCertVerifier; - -//use std::io::BufWriter; - -// [RFC6347 Section-4.2.4] -// +-----------+ -// +---> | PREPARING | <--------------------+ -// | +-----------+ | -// | | | -// | | Buffer next flight | -// | | | -// | \|/ | -// | +-----------+ | -// | | SENDING |<------------------+ | Send -// | +-----------+ | | HelloRequest -// Receive | | | | -// next | | Send flight | | or -// flight | +--------+ | | -// | | | Set retransmit timer | | Receive -// | | \|/ | | HelloRequest -// | | +-----------+ | | Send -// +--)--| WAITING |-------------------+ | ClientHello -// | | +-----------+ Timer expires | | -// | | | | | -// | | +------------------------+ | -// Receive | | Send Read retransmit | -// last | | last | -// flight | | flight | -// | | | -// \|/\|/ | -// +-----------+ | -// | FINISHED | -------------------------------+ -// +-----------+ -// | /|\ -// | | -// +---+ -// Read retransmit -// Retransmit last flight - -#[derive(Copy, Clone, PartialEq)] -pub(crate) enum HandshakeState { - Errored, - Preparing, - Sending, - Waiting, - Finished, -} - -impl fmt::Display for HandshakeState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - HandshakeState::Errored => write!(f, "Errored"), - HandshakeState::Preparing => write!(f, "Preparing"), - HandshakeState::Sending => write!(f, "Sending"), - HandshakeState::Waiting => write!(f, "Waiting"), - HandshakeState::Finished => write!(f, "Finished"), - } - } -} - -pub(crate) type VerifyPeerCertificateFn = - Arc], &[CertificateDer<'static>]) -> Result<()>) + Send + Sync>; - -pub(crate) struct HandshakeConfig { - pub(crate) local_psk_callback: Option, - pub(crate) local_psk_identity_hint: Option>, - pub(crate) local_cipher_suites: Vec, // Available CipherSuites - pub(crate) local_signature_schemes: Vec, // Available signature schemes - pub(crate) extended_master_secret: ExtendedMasterSecretType, // Policy for the Extended Master Support extension - pub(crate) local_srtp_protection_profiles: Vec, // Available SRTPProtectionProfiles, if empty no SRTP support - pub(crate) server_name: String, - pub(crate) client_auth: ClientAuthType, // If we are a client should we request a client certificate - pub(crate) local_certificates: Vec, - pub(crate) name_to_certificate: HashMap, - pub(crate) insecure_skip_verify: bool, - pub(crate) insecure_verification: bool, - pub(crate) verify_peer_certificate: Option, - pub(crate) server_cert_verifier: Arc, - pub(crate) client_cert_verifier: Option>, - pub(crate) retransmit_interval: tokio::time::Duration, - pub(crate) initial_epoch: u16, - //log logging.LeveledLogger - //mu sync.Mutex -} - -pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { - let mut certs = rustls::RootCertStore::empty(); - certs - .add( - rcgen::generate_simple_self_signed(vec![]) - .unwrap() - .cert - .der() - .to_owned(), - ) - .unwrap(); - certs -} - -impl Default for HandshakeConfig { - fn default() -> Self { - HandshakeConfig { - local_psk_callback: None, - local_psk_identity_hint: None, - local_cipher_suites: vec![], - local_signature_schemes: vec![], - extended_master_secret: ExtendedMasterSecretType::Disable, - local_srtp_protection_profiles: vec![], - server_name: String::new(), - client_auth: ClientAuthType::NoClientCert, - local_certificates: vec![], - name_to_certificate: HashMap::new(), - insecure_skip_verify: false, - insecure_verification: false, - verify_peer_certificate: None, - server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new( - gen_self_signed_root_cert(), - )) - .build() - .unwrap(), - client_cert_verifier: None, - retransmit_interval: tokio::time::Duration::from_secs(0), - initial_epoch: 0, - } - } -} - -impl HandshakeConfig { - pub(crate) fn get_certificate(&self, server_name: &str) -> Result { - //TODO - /*if self.name_to_certificate.is_empty() { - let mut name_to_certificate = HashMap::new(); - for cert in &self.local_certificates { - if let Ok((_rem, x509_cert)) = x509_parser::parse_x509_der(&cert.certificate) { - if let Some(a) = x509_cert.tbs_certificate.subject.iter_common_name().next() { - let common_name = match a.attr_value.as_str() { - Ok(cn) => cn.to_lowercase(), - Err(err) => return Err(Error::new(err.to_string())), - }; - name_to_certificate.insert(common_name, cert.clone()); - } - if let Some((_, sans)) = x509_cert.tbs_certificate.subject_alternative_name() { - for gn in &sans.general_names { - match gn { - x509_parser::extensions::GeneralName::DNSName(san) => { - let san = san.to_lowercase(); - name_to_certificate.insert(san, cert.clone()); - } - _ => {} - } - } - } - } else { - continue; - } - } - self.name_to_certificate = name_to_certificate; - }*/ - - if self.local_certificates.is_empty() { - return Err(Error::ErrNoCertificates); - } - - if self.local_certificates.len() == 1 { - // There's only one choice, so no point doing any work. - return Ok(self.local_certificates[0].clone()); - } - - if server_name.is_empty() { - return Ok(self.local_certificates[0].clone()); - } - - let lower = server_name.to_lowercase(); - let name = lower.trim_end_matches('.'); - - if let Some(cert) = self.name_to_certificate.get(name) { - return Ok(cert.clone()); - } - - // try replacing labels in the name with wildcards until we get a - // match. - let mut labels: Vec<&str> = name.split_terminator('.').collect(); - for i in 0..labels.len() { - labels[i] = "*"; - let candidate = labels.join("."); - if let Some(cert) = self.name_to_certificate.get(&candidate) { - return Ok(cert.clone()); - } - } - - // If nothing matches, return the first certificate. - Ok(self.local_certificates[0].clone()) - } -} - -pub(crate) fn srv_cli_str(is_client: bool) -> String { - if is_client { - return "client".to_owned(); - } - "server".to_owned() -} - -impl DTLSConn { - pub(crate) async fn handshake(&mut self, mut state: HandshakeState) -> Result<()> { - loop { - trace!( - "[handshake:{}] {}: {}", - srv_cli_str(self.state.is_client), - self.current_flight.to_string(), - state.to_string() - ); - - if state == HandshakeState::Finished && !self.is_handshake_completed_successfully() { - self.set_handshake_completed_successfully(); - self.handshake_done_tx.take(); // drop it by take - return Ok(()); - } - - state = match state { - HandshakeState::Preparing => self.prepare().await?, - HandshakeState::Sending => self.send().await?, - HandshakeState::Waiting => self.wait().await?, - HandshakeState::Finished => self.finish().await?, - _ => return Err(Error::ErrInvalidFsmTransition), - }; - } - } - - async fn prepare(&mut self) -> Result { - self.flights = None; - - // Prepare flights - self.retransmit = self.current_flight.has_retransmit(); - - let result = self - .current_flight - .generate(&mut self.state, &self.cache, &self.cfg) - .await; - - match result { - Err((a, mut err)) => { - if let Some(a) = a { - let alert_err = self.notify(a.alert_level, a.alert_description).await; - - if let Err(alert_err) = alert_err { - if err.is_some() { - err = Some(alert_err); - } - } - } - if let Some(err) = err { - return Err(err); - } - } - Ok(pkts) => { - /*if !pkts.is_empty() { - let mut s = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(s.as_mut()); - pkts[0].record.content.marshal(&mut writer)?; - } - trace!( - "[handshake:{}] {}: {:?}", - srv_cli_str(self.state.is_client), - self.current_flight.to_string(), - s, - ); - }*/ - self.flights = Some(pkts) - } - }; - - let epoch = self.cfg.initial_epoch; - let mut next_epoch = epoch; - if let Some(pkts) = &mut self.flights { - for p in pkts { - p.record.record_layer_header.epoch += epoch; - if p.record.record_layer_header.epoch > next_epoch { - next_epoch = p.record.record_layer_header.epoch; - } - if let Content::Handshake(h) = &mut p.record.content { - h.handshake_header.message_sequence = self.state.handshake_send_sequence as u16; - self.state.handshake_send_sequence += 1; - } - } - } - if epoch != next_epoch { - trace!( - "[handshake:{}] -> changeCipherSpec (epoch: {})", - srv_cli_str(self.state.is_client), - next_epoch - ); - self.set_local_epoch(next_epoch); - } - - Ok(HandshakeState::Sending) - } - async fn send(&mut self) -> Result { - // Send flights - if let Some(pkts) = self.flights.clone() { - self.write_packets(pkts).await?; - } - - if self.current_flight.is_last_send_flight() { - Ok(HandshakeState::Finished) - } else { - Ok(HandshakeState::Waiting) - } - } - async fn wait(&mut self) -> Result { - let retransmit_timer = tokio::time::sleep(self.cfg.retransmit_interval); - tokio::pin!(retransmit_timer); - - loop { - tokio::select! { - done = self.handshake_rx.recv() =>{ - if done.is_none() { - trace!("[handshake:{}] {} handshake_tx is dropped", srv_cli_str(self.state.is_client), self.current_flight.to_string()); - return Err(Error::ErrAlertFatalOrClose); - } - - //trace!("[handshake:{}] {} received handshake_rx", srv_cli_str(self.state.is_client), self.current_flight.to_string()); - let result = self.current_flight.parse(&mut self.handle_queue_tx, &mut self.state, &self.cache, &self.cfg).await; - drop(done); - match result { - Err((alert, mut err)) => { - trace!("[handshake:{}] {} result alert:{:?}, err:{:?}", - srv_cli_str(self.state.is_client), - self.current_flight.to_string(), - alert, - err); - - if let Some(alert) = alert { - let alert_err = self.notify(alert.alert_level, alert.alert_description).await; - - if let Err(alert_err) = alert_err { - if err.is_some() { - err = Some(alert_err); - } - } - } - if let Some(err) = err { - return Err(err); - } - } - Ok(next_flight) => { - trace!("[handshake:{}] {} -> {}", srv_cli_str(self.state.is_client), self.current_flight.to_string(), next_flight.to_string()); - if next_flight.is_last_recv_flight() && self.current_flight.to_string() == next_flight.to_string() { - return Ok(HandshakeState::Finished); - } - self.current_flight = next_flight; - return Ok(HandshakeState::Preparing); - } - }; - } - - _ = retransmit_timer.as_mut() =>{ - trace!("[handshake:{}] {} retransmit_timer", srv_cli_str(self.state.is_client), self.current_flight.to_string()); - - if !self.retransmit { - return Ok(HandshakeState::Waiting); - } - return Ok(HandshakeState::Sending); - } - - /*_ = self.done_rx.recv() => { - return Err(Error::new("done_rx recv".to_owned())); - }*/ - } - } - } - async fn finish(&mut self) -> Result { - let retransmit_timer = tokio::time::sleep(self.cfg.retransmit_interval); - - tokio::select! { - done = self.handshake_rx.recv() =>{ - if done.is_none() { - trace!("[handshake:{}] {} handshake_tx is dropped", srv_cli_str(self.state.is_client), self.current_flight.to_string()); - return Err(Error::ErrAlertFatalOrClose); - } - let result = self.current_flight.parse(&mut self.handle_queue_tx, &mut self.state, &self.cache, &self.cfg).await; - drop(done); - match result { - Err((alert, mut err)) => { - if let Some(alert) = alert { - let alert_err = self.notify(alert.alert_level, alert.alert_description).await; - if let Err(alert_err) = alert_err { - if err.is_some() { - err = Some(alert_err); - } - } - } - if let Some(err) = err { - return Err(err); - } - } - Ok(_) => { - retransmit_timer.await; - // Retransmit last flight - return Ok(HandshakeState::Sending); - } - }; - } - - /*_ = self.done_rx.recv() => { - return Err(Error::new("done_rx recv".to_owned())); - }*/ - } - - Ok(HandshakeState::Finished) - } -} diff --git a/dtls/src/lib.rs b/dtls/src/lib.rs deleted file mode 100644 index fd75b6bc8..000000000 --- a/dtls/src/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod alert; -pub mod application_data; -pub mod change_cipher_spec; -pub mod cipher_suite; -pub mod client_certificate_type; -pub mod compression_methods; -pub mod config; -pub mod conn; -pub mod content; -pub mod crypto; -pub mod curve; -mod error; -pub mod extension; -pub mod flight; -pub mod fragment_buffer; -pub mod handshake; -pub mod handshaker; -pub mod listener; -pub mod prf; -pub mod record_layer; -pub mod signature_hash_algorithm; -pub mod state; - -use cipher_suite::*; -pub use error::Error; -use extension::extension_use_srtp::SrtpProtectionProfile; - -pub(crate) fn find_matching_srtp_profile( - a: &[SrtpProtectionProfile], - b: &[SrtpProtectionProfile], -) -> Result { - for a_profile in a { - for b_profile in b { - if a_profile == b_profile { - return Ok(*a_profile); - } - } - } - Err(()) -} - -pub(crate) fn find_matching_cipher_suite( - a: &[CipherSuiteId], - b: &[CipherSuiteId], -) -> Result { - for a_suite in a { - for b_suite in b { - if a_suite == b_suite { - return Ok(*a_suite); - } - } - } - Err(()) -} diff --git a/dtls/src/listener.rs b/dtls/src/listener.rs deleted file mode 100644 index 0dc6257ce..000000000 --- a/dtls/src/listener.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::future::Future; -use std::io::BufReader; -use std::net::SocketAddr; -use std::pin::Pin; -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::net::ToSocketAddrs; -use util::conn::conn_udp_listener::*; -use util::conn::*; - -use crate::config::*; -use crate::conn::DTLSConn; -use crate::content::ContentType; -use crate::error::Result; -use crate::record_layer::record_layer_header::RecordLayerHeader; -use crate::record_layer::unpack_datagram; - -/// Listen creates a DTLS listener -pub async fn listen(laddr: A, config: Config) -> Result { - validate_config(false, &config)?; - - let mut lc = ListenConfig { - accept_filter: Some(Box::new( - |packet: &[u8]| -> Pin + Send + 'static>> { - let pkts = match unpack_datagram(packet) { - Ok(pkts) => { - if pkts.is_empty() { - return Box::pin(async { false }); - } - pkts - } - Err(_) => return Box::pin(async { false }), - }; - - let mut reader = BufReader::new(pkts[0].as_slice()); - match RecordLayerHeader::unmarshal(&mut reader) { - Ok(h) => { - let content_type = h.content_type; - Box::pin(async move { content_type == ContentType::Handshake }) - } - Err(_) => Box::pin(async { false }), - } - }, - )), - ..Default::default() - }; - - let parent = Arc::new(lc.listen(laddr).await?); - Ok(DTLSListener { parent, config }) -} - -/// DTLSListener represents a DTLS listener -pub struct DTLSListener { - parent: Arc, - config: Config, -} - -impl DTLSListener { - /// creates a DTLS listener which accepts connections from an inner Listener. - pub fn new(parent: Arc, config: Config) -> Result { - validate_config(false, &config)?; - - Ok(DTLSListener { parent, config }) - } -} - -type UtilResult = std::result::Result; - -#[async_trait] -impl Listener for DTLSListener { - /// Accept waits for and returns the next connection to the listener. - /// You have to either close or read on all connection that are created. - /// Connection handshake will timeout using ConnectContextMaker in the Config. - /// If you want to specify the timeout duration, set ConnectContextMaker. - async fn accept(&self) -> UtilResult<(Arc, SocketAddr)> { - let (conn, raddr) = self.parent.accept().await?; - let dtls_conn = DTLSConn::new(conn, self.config.clone(), false, None) - .await - .map_err(util::Error::from_std)?; - Ok((Arc::new(dtls_conn), raddr)) - } - - /// Close closes the listener. - /// Any blocked Accept operations will be unblocked and return errors. - /// Already Accepted connections are not closed. - async fn close(&self) -> UtilResult<()> { - self.parent.close().await - } - - /// Addr returns the listener's network address. - async fn addr(&self) -> UtilResult { - self.parent.addr().await - } -} diff --git a/dtls/src/prf/mod.rs b/dtls/src/prf/mod.rs deleted file mode 100644 index e38cd454e..000000000 --- a/dtls/src/prf/mod.rs +++ /dev/null @@ -1,315 +0,0 @@ -#[cfg(test)] -mod prf_test; - -use std::convert::TryInto; -use std::fmt; - -use hmac::{Hmac, Mac}; -use sha1::Sha1; -use sha2::{Digest, Sha256}; - -type HmacSha256 = Hmac; -type HmacSha1 = Hmac; - -use crate::cipher_suite::CipherSuiteHash; -use crate::content::ContentType; -use crate::curve::named_curve::*; -use crate::error::*; -use crate::record_layer::record_layer_header::ProtocolVersion; - -pub(crate) const PRF_MASTER_SECRET_LABEL: &str = "master secret"; -pub(crate) const PRF_EXTENDED_MASTER_SECRET_LABEL: &str = "extended master secret"; -pub(crate) const PRF_KEY_EXPANSION_LABEL: &str = "key expansion"; -pub(crate) const PRF_VERIFY_DATA_CLIENT_LABEL: &str = "client finished"; -pub(crate) const PRF_VERIFY_DATA_SERVER_LABEL: &str = "server finished"; - -#[derive(PartialEq, Debug, Clone)] -pub(crate) struct EncryptionKeys { - pub(crate) master_secret: Vec, - pub(crate) client_mac_key: Vec, - pub(crate) server_mac_key: Vec, - pub(crate) client_write_key: Vec, - pub(crate) server_write_key: Vec, - pub(crate) client_write_iv: Vec, - pub(crate) server_write_iv: Vec, -} - -impl fmt::Display for EncryptionKeys { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = "EncryptionKeys:\n".to_string(); - - out += format!("- master_secret: {:?}\n", self.master_secret).as_str(); - out += format!("- client_mackey: {:?}\n", self.client_mac_key).as_str(); - out += format!("- server_mackey: {:?}\n", self.server_mac_key).as_str(); - out += format!("- client_write_key: {:?}\n", self.client_write_key).as_str(); - out += format!("- server_write_key: {:?}\n", self.server_write_key).as_str(); - out += format!("- client_write_iv: {:?}\n", self.client_write_iv).as_str(); - out += format!("- server_write_iv: {:?}\n", self.server_write_iv).as_str(); - - write!(f, "{out}") - } -} - -// The premaster secret is formed as follows: if the PSK is N octets -// long, concatenate a uint16 with the value N, N zero octets, a second -// uint16 with the value N, and the PSK itself. -// -// https://tools.ietf.org/html/rfc4279#section-2 -pub(crate) fn prf_psk_pre_master_secret(psk: &[u8]) -> Vec { - let psk_len = psk.len(); - - let mut out = vec![0u8; 2 + psk_len + 2]; - - out.extend_from_slice(psk); - let be = (psk_len as u16).to_be_bytes(); - out[..2].copy_from_slice(&be); - out[2 + psk_len..2 + psk_len + 2].copy_from_slice(&be); - - out -} - -pub(crate) fn prf_pre_master_secret( - public_key: &[u8], - private_key: &NamedCurvePrivateKey, - curve: NamedCurve, -) -> Result> { - match curve { - NamedCurve::P256 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::P384 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::X25519 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - _ => Err(Error::ErrInvalidNamedCurve), - } -} - -fn elliptic_curve_pre_master_secret( - public_key: &[u8], - private_key: &NamedCurvePrivateKey, - curve: NamedCurve, -) -> Result> { - match curve { - NamedCurve::P256 => { - let pub_key = p256::EncodedPoint::from_bytes(public_key)?; - let public = p256::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP256(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::P384 => { - let pub_key = p384::EncodedPoint::from_bytes(public_key)?; - let public = p384::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP384(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::X25519 => { - if public_key.len() != 32 { - return Err(Error::Other("Public key is not 32 len".into())); - } - let pub_key: [u8; 32] = public_key.try_into().unwrap(); - let public = x25519_dalek::PublicKey::from(pub_key); - if let NamedCurvePrivateKey::StaticSecretX25519(secret) = private_key { - return Ok(secret.diffie_hellman(&public).as_bytes().to_vec()); - } - } - _ => return Err(Error::ErrInvalidNamedCurve), - } - Err(Error::ErrNamedCurveAndPrivateKeyMismatch) -} - -// This PRF with the SHA-256 hash function is used for all cipher suites -// defined in this document and in TLS documents published prior to this -// document when TLS 1.2 is negotiated. New cipher suites MUST explicitly -// specify a PRF and, in general, SHOULD use the TLS PRF with SHA-256 or a -// stronger standard hash function. -// -// P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + -// HMAC_hash(secret, A(2) + seed) + -// HMAC_hash(secret, A(3) + seed) + ... -// -// A() is defined as: -// -// A(0) = seed -// A(i) = HMAC_hash(secret, A(i-1)) -// -// P_hash can be iterated as many times as necessary to produce the -// required quantity of data. For example, if P_SHA256 is being used to -// create 80 bytes of data, it will have to be iterated three times -// (through A(3)), creating 96 bytes of output data; the last 16 bytes -// of the final iteration will then be discarded, leaving 80 bytes of -// output data. -// -// https://tools.ietf.org/html/rfc4346w -fn hmac_sha(h: CipherSuiteHash, key: &[u8], data: &[u8]) -> Result> { - let mut mac = match h { - CipherSuiteHash::Sha256 => { - HmacSha256::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))? - } - }; - mac.update(data); - let result = mac.finalize(); - let code_bytes = result.into_bytes(); - Ok(code_bytes.to_vec()) -} - -pub(crate) fn prf_p_hash( - secret: &[u8], - seed: &[u8], - requested_length: usize, - h: CipherSuiteHash, -) -> Result> { - let mut last_round = seed.to_vec(); - let mut out = vec![]; - - let iterations = ((requested_length as f64) / (h.size() as f64)).ceil() as usize; - for _ in 0..iterations { - last_round = hmac_sha(h, secret, &last_round)?; - - let mut last_round_seed = last_round.clone(); - last_round_seed.extend_from_slice(seed); - let with_secret = hmac_sha(h, secret, &last_round_seed)?; - - out.extend_from_slice(&with_secret); - } - - Ok(out[..requested_length].to_vec()) -} - -pub(crate) fn prf_extended_master_secret( - pre_master_secret: &[u8], - session_hash: &[u8], - h: CipherSuiteHash, -) -> Result> { - let mut seed = PRF_EXTENDED_MASTER_SECRET_LABEL.as_bytes().to_vec(); - seed.extend_from_slice(session_hash); - prf_p_hash(pre_master_secret, &seed, 48, h) -} - -pub(crate) fn prf_master_secret( - pre_master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - h: CipherSuiteHash, -) -> Result> { - let mut seed = PRF_MASTER_SECRET_LABEL.as_bytes().to_vec(); - seed.extend_from_slice(client_random); - seed.extend_from_slice(server_random); - prf_p_hash(pre_master_secret, &seed, 48, h) -} - -pub(crate) fn prf_encryption_keys( - master_secret: &[u8], - client_random: &[u8], - server_random: &[u8], - prf_mac_len: usize, - prf_key_len: usize, - prf_iv_len: usize, - h: CipherSuiteHash, -) -> Result { - let mut seed = PRF_KEY_EXPANSION_LABEL.as_bytes().to_vec(); - seed.extend_from_slice(server_random); - seed.extend_from_slice(client_random); - - let material = prf_p_hash( - master_secret, - &seed, - (2 * prf_mac_len) + (2 * prf_key_len) + (2 * prf_iv_len), - h, - )?; - let mut key_material = &material[..]; - - let client_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; - - let server_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; - - let client_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; - - let server_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; - - let client_write_iv = key_material[..prf_iv_len].to_vec(); - key_material = &key_material[prf_iv_len..]; - - let server_write_iv = key_material[..prf_iv_len].to_vec(); - - Ok(EncryptionKeys { - master_secret: master_secret.to_vec(), - client_mac_key, - server_mac_key, - client_write_key, - server_write_key, - client_write_iv, - server_write_iv, - }) -} - -pub(crate) fn prf_verify_data( - master_secret: &[u8], - handshake_bodies: &[u8], - label: &str, - h: CipherSuiteHash, -) -> Result> { - let mut hasher = match h { - CipherSuiteHash::Sha256 => Sha256::new(), - }; - hasher.update(handshake_bodies); - let result = hasher.finalize(); - let mut seed = label.as_bytes().to_vec(); - seed.extend_from_slice(&result); - - prf_p_hash(master_secret, &seed, 12, h) -} - -pub(crate) fn prf_verify_data_client( - master_secret: &[u8], - handshake_bodies: &[u8], - h: CipherSuiteHash, -) -> Result> { - prf_verify_data( - master_secret, - handshake_bodies, - PRF_VERIFY_DATA_CLIENT_LABEL, - h, - ) -} - -pub(crate) fn prf_verify_data_server( - master_secret: &[u8], - handshake_bodies: &[u8], - h: CipherSuiteHash, -) -> Result> { - prf_verify_data( - master_secret, - handshake_bodies, - PRF_VERIFY_DATA_SERVER_LABEL, - h, - ) -} - -// compute the MAC using HMAC-SHA1 -pub(crate) fn prf_mac( - epoch: u16, - sequence_number: u64, - content_type: ContentType, - protocol_version: ProtocolVersion, - payload: &[u8], - key: &[u8], -) -> Result> { - let mut hmac = HmacSha1::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))?; - - let mut msg = vec![0u8; 13]; - msg[..2].copy_from_slice(&epoch.to_be_bytes()); - msg[2..8].copy_from_slice(&sequence_number.to_be_bytes()[2..]); - msg[8] = content_type as u8; - msg[9] = protocol_version.major; - msg[10] = protocol_version.minor; - msg[11..].copy_from_slice(&(payload.len() as u16).to_be_bytes()); - - hmac.update(&msg); - hmac.update(payload); - let result = hmac.finalize(); - - Ok(result.into_bytes().to_vec()) -} diff --git a/dtls/src/prf/prf_test.rs b/dtls/src/prf/prf_test.rs deleted file mode 100644 index 33d5241f2..000000000 --- a/dtls/src/prf/prf_test.rs +++ /dev/null @@ -1,261 +0,0 @@ -use super::*; -use crate::cipher_suite::CipherSuiteHash; - -#[test] -fn test_pre_master_secret() -> Result<()> { - let private_key: [u8; 32] = [ - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, - 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, - 0x3e, 0x3f, - ]; - let private_key = - NamedCurvePrivateKey::StaticSecretX25519(x25519_dalek::StaticSecret::from(private_key)); - let public_key = [ - 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, - 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, - 0xb6, 0x15, - ]; - - let expected_pre_master_secret = vec![ - 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, - 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, - 0x76, 0x24, - ]; - - let pre_master_secret = prf_pre_master_secret(&public_key, &private_key, NamedCurve::X25519)?; - - assert_eq!( - expected_pre_master_secret, pre_master_secret, - "PremasterSecret exp: {expected_pre_master_secret:?} actual: {pre_master_secret:?}" - ); - - Ok(()) -} - -#[test] -fn test_master_secret() -> Result<()> { - let pre_master_secret = vec![ - 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, - 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, - 0x76, 0x24, - ]; - let client_random = vec![ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, - 0x1e, 0x1f, - ]; - let server_random = vec![ - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, - 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, - 0x8e, 0x8f, - ]; - let expected_master_secret = vec![ - 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, - 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, - 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, - 0x37, 0x34, 0x4c, - ]; - - let master_secret = prf_master_secret( - &pre_master_secret, - &client_random, - &server_random, - CipherSuiteHash::Sha256, - )?; - - assert_eq!( - expected_master_secret, master_secret, - "master_secret exp: {expected_master_secret:?} actual: {master_secret:?}" - ); - - Ok(()) -} - -#[test] -fn test_encryption_keys() -> Result<()> { - let client_random = vec![ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, - 0x1e, 0x1f, - ]; - let server_random = vec![ - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, - 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, - 0x8e, 0x8f, - ]; - let master_secret = vec![ - 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, - 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, - 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, - 0x37, 0x34, 0x4c, - ]; - - let expected_encryption_keys = EncryptionKeys { - master_secret: master_secret.clone(), - client_mac_key: vec![], - server_mac_key: vec![], - client_write_key: vec![ - 0x1b, 0x7d, 0x11, 0x7c, 0x7d, 0x5f, 0x69, 0x0b, 0xc2, 0x63, 0xca, 0xe8, 0xef, 0x60, - 0xaf, 0x0f, - ], - server_write_key: vec![ - 0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, - 0x63, 0x54, - ], - client_write_iv: vec![0x0e, 0xb2, 0x09, 0x06], - server_write_iv: vec![0xf7, 0x81, 0xfa, 0xd2], - }; - - let keys = prf_encryption_keys( - &master_secret, - &client_random, - &server_random, - 0, - 16, - 4, - CipherSuiteHash::Sha256, - )?; - - assert_eq!( - expected_encryption_keys, keys, - "master_secret exp: {expected_encryption_keys:?} actual: {keys:?}", - ); - - Ok(()) -} - -#[test] -fn test_verify_data() -> Result<()> { - let client_hello = vec![ - 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00, 0x20, 0xcc, 0xa8, 0xcc, 0xa9, - 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09, 0xc0, 0x14, 0xc0, - 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, - 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, - 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, - 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, - 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, 0x00, - ]; - let server_hello = vec![ - 0x02, 0x00, 0x00, 0x2d, 0x03, 0x03, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, - 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x00, 0xc0, 0x13, 0x00, 0x00, 0x05, 0xff, - 0x01, 0x00, 0x01, 0x00, - ]; - let server_certificate = vec![ - 0x0b, 0x00, 0x03, 0x2b, 0x00, 0x03, 0x28, 0x00, 0x03, 0x25, 0x30, 0x82, 0x03, 0x21, 0x30, - 0x82, 0x02, 0x09, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x08, 0x15, 0x5a, 0x92, 0xad, 0xc2, - 0x04, 0x8f, 0x90, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, - 0x0b, 0x05, 0x00, 0x30, 0x22, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, - 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0a, 0x45, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x38, - 0x31, 0x30, 0x30, 0x35, 0x30, 0x31, 0x33, 0x38, 0x31, 0x37, 0x5a, 0x17, 0x0d, 0x31, 0x39, - 0x31, 0x30, 0x30, 0x35, 0x30, 0x31, 0x33, 0x38, 0x31, 0x37, 0x5a, 0x30, 0x2b, 0x31, 0x0b, - 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x1c, 0x30, 0x1a, - 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x13, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, - 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x82, 0x01, 0x22, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, - 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc4, - 0x80, 0x36, 0x06, 0xba, 0xe7, 0x47, 0x6b, 0x08, 0x94, 0x04, 0xec, 0xa7, 0xb6, 0x91, 0x04, - 0x3f, 0xf7, 0x92, 0xbc, 0x19, 0xee, 0xfb, 0x7d, 0x74, 0xd7, 0xa8, 0x0d, 0x00, 0x1e, 0x7b, - 0x4b, 0x3a, 0x4a, 0xe6, 0x0f, 0xe8, 0xc0, 0x71, 0xfc, 0x73, 0xe7, 0x02, 0x4c, 0x0d, 0xbc, - 0xf4, 0xbd, 0xd1, 0x1d, 0x39, 0x6b, 0xba, 0x70, 0x46, 0x4a, 0x13, 0xe9, 0x4a, 0xf8, 0x3d, - 0xf3, 0xe1, 0x09, 0x59, 0x54, 0x7b, 0xc9, 0x55, 0xfb, 0x41, 0x2d, 0xa3, 0x76, 0x52, 0x11, - 0xe1, 0xf3, 0xdc, 0x77, 0x6c, 0xaa, 0x53, 0x37, 0x6e, 0xca, 0x3a, 0xec, 0xbe, 0xc3, 0xaa, - 0xb7, 0x3b, 0x31, 0xd5, 0x6c, 0xb6, 0x52, 0x9c, 0x80, 0x98, 0xbc, 0xc9, 0xe0, 0x28, 0x18, - 0xe2, 0x0b, 0xf7, 0xf8, 0xa0, 0x3a, 0xfd, 0x17, 0x04, 0x50, 0x9e, 0xce, 0x79, 0xbd, 0x9f, - 0x39, 0xf1, 0xea, 0x69, 0xec, 0x47, 0x97, 0x2e, 0x83, 0x0f, 0xb5, 0xca, 0x95, 0xde, 0x95, - 0xa1, 0xe6, 0x04, 0x22, 0xd5, 0xee, 0xbe, 0x52, 0x79, 0x54, 0xa1, 0xe7, 0xbf, 0x8a, 0x86, - 0xf6, 0x46, 0x6d, 0x0d, 0x9f, 0x16, 0x95, 0x1a, 0x4c, 0xf7, 0xa0, 0x46, 0x92, 0x59, 0x5c, - 0x13, 0x52, 0xf2, 0x54, 0x9e, 0x5a, 0xfb, 0x4e, 0xbf, 0xd7, 0x7a, 0x37, 0x95, 0x01, 0x44, - 0xe4, 0xc0, 0x26, 0x87, 0x4c, 0x65, 0x3e, 0x40, 0x7d, 0x7d, 0x23, 0x07, 0x44, 0x01, 0xf4, - 0x84, 0xff, 0xd0, 0x8f, 0x7a, 0x1f, 0xa0, 0x52, 0x10, 0xd1, 0xf4, 0xf0, 0xd5, 0xce, 0x79, - 0x70, 0x29, 0x32, 0xe2, 0xca, 0xbe, 0x70, 0x1f, 0xdf, 0xad, 0x6b, 0x4b, 0xb7, 0x11, 0x01, - 0xf4, 0x4b, 0xad, 0x66, 0x6a, 0x11, 0x13, 0x0f, 0xe2, 0xee, 0x82, 0x9e, 0x4d, 0x02, 0x9d, - 0xc9, 0x1c, 0xdd, 0x67, 0x16, 0xdb, 0xb9, 0x06, 0x18, 0x86, 0xed, 0xc1, 0xba, 0x94, 0x21, - 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x52, 0x30, 0x50, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, - 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x05, 0xa0, 0x30, 0x1d, 0x06, 0x03, 0x55, - 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, - 0x02, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x30, 0x1f, 0x06, 0x03, - 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x89, 0x4f, 0xde, 0x5b, 0xcc, 0x69, - 0xe2, 0x52, 0xcf, 0x3e, 0xa3, 0x00, 0xdf, 0xb1, 0x97, 0xb8, 0x1d, 0xe1, 0xc1, 0x46, 0x30, - 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, - 0x82, 0x01, 0x01, 0x00, 0x59, 0x16, 0x45, 0xa6, 0x9a, 0x2e, 0x37, 0x79, 0xe4, 0xf6, 0xdd, - 0x27, 0x1a, 0xba, 0x1c, 0x0b, 0xfd, 0x6c, 0xd7, 0x55, 0x99, 0xb5, 0xe7, 0xc3, 0x6e, 0x53, - 0x3e, 0xff, 0x36, 0x59, 0x08, 0x43, 0x24, 0xc9, 0xe7, 0xa5, 0x04, 0x07, 0x9d, 0x39, 0xe0, - 0xd4, 0x29, 0x87, 0xff, 0xe3, 0xeb, 0xdd, 0x09, 0xc1, 0xcf, 0x1d, 0x91, 0x44, 0x55, 0x87, - 0x0b, 0x57, 0x1d, 0xd1, 0x9b, 0xdf, 0x1d, 0x24, 0xf8, 0xbb, 0x9a, 0x11, 0xfe, 0x80, 0xfd, - 0x59, 0x2b, 0xa0, 0x39, 0x8c, 0xde, 0x11, 0xe2, 0x65, 0x1e, 0x61, 0x8c, 0xe5, 0x98, 0xfa, - 0x96, 0xe5, 0x37, 0x2e, 0xef, 0x3d, 0x24, 0x8a, 0xfd, 0xe1, 0x74, 0x63, 0xeb, 0xbf, 0xab, - 0xb8, 0xe4, 0xd1, 0xab, 0x50, 0x2a, 0x54, 0xec, 0x00, 0x64, 0xe9, 0x2f, 0x78, 0x19, 0x66, - 0x0d, 0x3f, 0x27, 0xcf, 0x20, 0x9e, 0x66, 0x7f, 0xce, 0x5a, 0xe2, 0xe4, 0xac, 0x99, 0xc7, - 0xc9, 0x38, 0x18, 0xf8, 0xb2, 0x51, 0x07, 0x22, 0xdf, 0xed, 0x97, 0xf3, 0x2e, 0x3e, 0x93, - 0x49, 0xd4, 0xc6, 0x6c, 0x9e, 0xa6, 0x39, 0x6d, 0x74, 0x44, 0x62, 0xa0, 0x6b, 0x42, 0xc6, - 0xd5, 0xba, 0x68, 0x8e, 0xac, 0x3a, 0x01, 0x7b, 0xdd, 0xfc, 0x8e, 0x2c, 0xfc, 0xad, 0x27, - 0xcb, 0x69, 0xd3, 0xcc, 0xdc, 0xa2, 0x80, 0x41, 0x44, 0x65, 0xd3, 0xae, 0x34, 0x8c, 0xe0, - 0xf3, 0x4a, 0xb2, 0xfb, 0x9c, 0x61, 0x83, 0x71, 0x31, 0x2b, 0x19, 0x10, 0x41, 0x64, 0x1c, - 0x23, 0x7f, 0x11, 0xa5, 0xd6, 0x5c, 0x84, 0x4f, 0x04, 0x04, 0x84, 0x99, 0x38, 0x71, 0x2b, - 0x95, 0x9e, 0xd6, 0x85, 0xbc, 0x5c, 0x5d, 0xd6, 0x45, 0xed, 0x19, 0x90, 0x94, 0x73, 0x40, - 0x29, 0x26, 0xdc, 0xb4, 0x0e, 0x34, 0x69, 0xa1, 0x59, 0x41, 0xe8, 0xe2, 0xcc, 0xa8, 0x4b, - 0xb6, 0x08, 0x46, 0x36, 0xa0, - ]; - let server_key_exchange = vec![ - 0x0c, 0x00, 0x01, 0x28, 0x03, 0x00, 0x1d, 0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, - 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, - 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, 0xb6, 0x15, 0x04, 0x01, 0x01, 0x00, 0x04, - 0x02, 0xb6, 0x61, 0xf7, 0xc1, 0x91, 0xee, 0x59, 0xbe, 0x45, 0x37, 0x66, 0x39, 0xbd, 0xc3, - 0xd4, 0xbb, 0x81, 0xe1, 0x15, 0xca, 0x73, 0xc8, 0x34, 0x8b, 0x52, 0x5b, 0x0d, 0x23, 0x38, - 0xaa, 0x14, 0x46, 0x67, 0xed, 0x94, 0x31, 0x02, 0x14, 0x12, 0xcd, 0x9b, 0x84, 0x4c, 0xba, - 0x29, 0x93, 0x4a, 0xaa, 0xcc, 0xe8, 0x73, 0x41, 0x4e, 0xc1, 0x1c, 0xb0, 0x2e, 0x27, 0x2d, - 0x0a, 0xd8, 0x1f, 0x76, 0x7d, 0x33, 0x07, 0x67, 0x21, 0xf1, 0x3b, 0xf3, 0x60, 0x20, 0xcf, - 0x0b, 0x1f, 0xd0, 0xec, 0xb0, 0x78, 0xde, 0x11, 0x28, 0xbe, 0xba, 0x09, 0x49, 0xeb, 0xec, - 0xe1, 0xa1, 0xf9, 0x6e, 0x20, 0x9d, 0xc3, 0x6e, 0x4f, 0xff, 0xd3, 0x6b, 0x67, 0x3a, 0x7d, - 0xdc, 0x15, 0x97, 0xad, 0x44, 0x08, 0xe4, 0x85, 0xc4, 0xad, 0xb2, 0xc8, 0x73, 0x84, 0x12, - 0x49, 0x37, 0x25, 0x23, 0x80, 0x9e, 0x43, 0x12, 0xd0, 0xc7, 0xb3, 0x52, 0x2e, 0xf9, 0x83, - 0xca, 0xc1, 0xe0, 0x39, 0x35, 0xff, 0x13, 0xa8, 0xe9, 0x6b, 0xa6, 0x81, 0xa6, 0x2e, 0x40, - 0xd3, 0xe7, 0x0a, 0x7f, 0xf3, 0x58, 0x66, 0xd3, 0xd9, 0x99, 0x3f, 0x9e, 0x26, 0xa6, 0x34, - 0xc8, 0x1b, 0x4e, 0x71, 0x38, 0x0f, 0xcd, 0xd6, 0xf4, 0xe8, 0x35, 0xf7, 0x5a, 0x64, 0x09, - 0xc7, 0xdc, 0x2c, 0x07, 0x41, 0x0e, 0x6f, 0x87, 0x85, 0x8c, 0x7b, 0x94, 0xc0, 0x1c, 0x2e, - 0x32, 0xf2, 0x91, 0x76, 0x9e, 0xac, 0xca, 0x71, 0x64, 0x3b, 0x8b, 0x98, 0xa9, 0x63, 0xdf, - 0x0a, 0x32, 0x9b, 0xea, 0x4e, 0xd6, 0x39, 0x7e, 0x8c, 0xd0, 0x1a, 0x11, 0x0a, 0xb3, 0x61, - 0xac, 0x5b, 0xad, 0x1c, 0xcd, 0x84, 0x0a, 0x6c, 0x8a, 0x6e, 0xaa, 0x00, 0x1a, 0x9d, 0x7d, - 0x87, 0xdc, 0x33, 0x18, 0x64, 0x35, 0x71, 0x22, 0x6c, 0x4d, 0xd2, 0xc2, 0xac, 0x41, 0xfb, - ]; - let server_hello_done = vec![0x0e, 0x00, 0x00, 0x00]; - let client_key_exchange = vec![ - 0x10, 0x00, 0x00, 0x21, 0x20, 0x35, 0x80, 0x72, 0xd6, 0x36, 0x58, 0x80, 0xd1, 0xae, 0xea, - 0x32, 0x9a, 0xdf, 0x91, 0x21, 0x38, 0x38, 0x51, 0xed, 0x21, 0xa2, 0x8e, 0x3b, 0x75, 0xe9, - 0x65, 0xd0, 0xd2, 0xcd, 0x16, 0x62, 0x54, - ]; - - let mut final_msg = vec![]; - final_msg.extend_from_slice(&client_hello); - final_msg.extend_from_slice(&server_hello); - final_msg.extend_from_slice(&server_certificate); - final_msg.extend_from_slice(&server_key_exchange); - final_msg.extend_from_slice(&server_hello_done); - final_msg.extend_from_slice(&client_key_exchange); - - let master_secret = vec![ - 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, - 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, - 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, - 0x37, 0x34, 0x4c, - ]; - - let expected_verify_data = vec![ - 0xcf, 0x91, 0x96, 0x26, 0xf1, 0x36, 0x0c, 0x53, 0x6a, 0xaa, 0xd7, 0x3a, - ]; - - let verify_data = prf_verify_data_client(&master_secret, &final_msg, CipherSuiteHash::Sha256)?; - - assert_eq!( - expected_verify_data, verify_data, - "verify_data exp: {expected_verify_data:?} actual: {verify_data:?}" - ); - - Ok(()) -} diff --git a/dtls/src/record_layer/mod.rs b/dtls/src/record_layer/mod.rs deleted file mode 100644 index 8be337db7..000000000 --- a/dtls/src/record_layer/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub mod record_layer_header; - -#[cfg(test)] -mod record_layer_test; - -use std::io::{Read, Write}; - -use record_layer_header::*; - -use super::content::*; -use super::error::*; -use crate::alert::Alert; -use crate::application_data::ApplicationData; -use crate::change_cipher_spec::ChangeCipherSpec; -use crate::handshake::Handshake; - -/* - The TLS Record Layer which handles all data transport. - The record layer is assumed to sit directly on top of some - reliable transport such as TCP. The record layer can carry four types of content: - - 1. Handshake messages—used for algorithm negotiation and key establishment. - 2. ChangeCipherSpec messages—really part of the handshake but technically a separate kind of message. - 3. Alert messages—used to signal that errors have occurred - 4. Application layer data - - The DTLS record layer is extremely similar to that of TLS 1.1. The - only change is the inclusion of an explicit sequence number in the - record. This sequence number allows the recipient to correctly - verify the TLS MAC. - https://tools.ietf.org/html/rfc4347#section-4.1 -*/ -#[derive(Debug, Clone, PartialEq)] -pub struct RecordLayer { - pub record_layer_header: RecordLayerHeader, - pub content: Content, -} - -impl RecordLayer { - pub fn new(protocol_version: ProtocolVersion, epoch: u16, content: Content) -> Self { - RecordLayer { - record_layer_header: RecordLayerHeader { - content_type: content.content_type(), - protocol_version, - epoch, - sequence_number: 0, - content_len: content.size() as u16, - }, - content, - } - } - - pub fn marshal(&self, writer: &mut W) -> Result<()> { - self.record_layer_header.marshal(writer)?; - self.content.marshal(writer)?; - Ok(()) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let record_layer_header = RecordLayerHeader::unmarshal(reader)?; - let content = match record_layer_header.content_type { - ContentType::Alert => Content::Alert(Alert::unmarshal(reader)?), - ContentType::ApplicationData => { - Content::ApplicationData(ApplicationData::unmarshal(reader)?) - } - ContentType::ChangeCipherSpec => { - Content::ChangeCipherSpec(ChangeCipherSpec::unmarshal(reader)?) - } - ContentType::Handshake => Content::Handshake(Handshake::unmarshal(reader)?), - _ => return Err(Error::Other("Invalid Content Type".to_owned())), - }; - - Ok(RecordLayer { - record_layer_header, - content, - }) - } -} - -// Note that as with TLS, multiple handshake messages may be placed in -// the same DTLS record, provided that there is room and that they are -// part of the same flight. Thus, there are two acceptable ways to pack -// two DTLS messages into the same datagram: in the same record or in -// separate records. -// https://tools.ietf.org/html/rfc6347#section-4.2.3 -pub(crate) fn unpack_datagram(buf: &[u8]) -> Result>> { - let mut out = vec![]; - - let mut offset = 0; - while buf.len() != offset { - if buf.len() - offset <= RECORD_LAYER_HEADER_SIZE { - return Err(Error::ErrInvalidPacketLength); - } - - let pkt_len = RECORD_LAYER_HEADER_SIZE - + (((buf[offset + RECORD_LAYER_HEADER_SIZE - 2] as usize) << 8) - | buf[offset + RECORD_LAYER_HEADER_SIZE - 1] as usize); - if offset + pkt_len > buf.len() { - return Err(Error::ErrInvalidPacketLength); - } - - out.push(buf[offset..offset + pkt_len].to_vec()); - offset += pkt_len - } - - Ok(out) -} diff --git a/dtls/src/record_layer/record_layer_header.rs b/dtls/src/record_layer/record_layer_header.rs deleted file mode 100644 index 571168248..000000000 --- a/dtls/src/record_layer/record_layer_header.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::io::{Read, Write}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use crate::content::*; -use crate::error::*; - -pub const RECORD_LAYER_HEADER_SIZE: usize = 13; -pub const MAX_SEQUENCE_NUMBER: u64 = 0x0000FFFFFFFFFFFF; - -pub const DTLS1_2MAJOR: u8 = 0xfe; -pub const DTLS1_2MINOR: u8 = 0xfd; - -pub const DTLS1_0MAJOR: u8 = 0xfe; -pub const DTLS1_0MINOR: u8 = 0xff; - -// VERSION_DTLS12 is the DTLS version in the same style as -// VersionTLSXX from crypto/tls -pub const VERSION_DTLS12: u16 = 0xfefd; - -pub const PROTOCOL_VERSION1_0: ProtocolVersion = ProtocolVersion { - major: DTLS1_0MAJOR, - minor: DTLS1_0MINOR, -}; -pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { - major: DTLS1_2MAJOR, - minor: DTLS1_2MINOR, -}; - -// https://tools.ietf.org/html/rfc4346#section-6.2.1 -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct ProtocolVersion { - pub major: u8, - pub minor: u8, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct RecordLayerHeader { - pub content_type: ContentType, - pub protocol_version: ProtocolVersion, - pub epoch: u16, - pub sequence_number: u64, // uint48 in spec - pub content_len: u16, -} - -impl RecordLayerHeader { - pub fn marshal(&self, writer: &mut W) -> Result<()> { - if self.sequence_number > MAX_SEQUENCE_NUMBER { - return Err(Error::ErrSequenceNumberOverflow); - } - - writer.write_u8(self.content_type as u8)?; - writer.write_u8(self.protocol_version.major)?; - writer.write_u8(self.protocol_version.minor)?; - writer.write_u16::(self.epoch)?; - - let be: [u8; 8] = self.sequence_number.to_be_bytes(); - writer.write_all(&be[2..])?; // uint48 in spec - - writer.write_u16::(self.content_len)?; - - Ok(writer.flush()?) - } - - pub fn unmarshal(reader: &mut R) -> Result { - let content_type = reader.read_u8()?.into(); - let major = reader.read_u8()?; - let minor = reader.read_u8()?; - let epoch = reader.read_u16::()?; - - // SequenceNumber is stored as uint48, make into uint64 - let mut be: [u8; 8] = [0u8; 8]; - reader.read_exact(&mut be[2..])?; - let sequence_number = u64::from_be_bytes(be); - - let protocol_version = ProtocolVersion { major, minor }; - if protocol_version != PROTOCOL_VERSION1_0 && protocol_version != PROTOCOL_VERSION1_2 { - return Err(Error::ErrUnsupportedProtocolVersion); - } - let content_len = reader.read_u16::()?; - - Ok(RecordLayerHeader { - content_type, - protocol_version, - epoch, - sequence_number, - content_len, - }) - } -} diff --git a/dtls/src/record_layer/record_layer_test.rs b/dtls/src/record_layer/record_layer_test.rs deleted file mode 100644 index 3e644e503..000000000 --- a/dtls/src/record_layer/record_layer_test.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::record_layer_header::*; -use super::*; -use crate::change_cipher_spec::ChangeCipherSpec; - -#[test] -fn test_udp_decode() -> Result<()> { - let tests = vec![ - ( - "Change Cipher Spec, single packet", - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, - ], - vec![vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, - ]], - None, - ), - ( - "Change Cipher Spec, multi packet", - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x01, 0x01, - ], - vec![ - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, - 0x01, - ], - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x01, - 0x01, - ], - ], - None, - ), - ( - "Invalid packet length", - vec![0x14, 0xfe], - vec![], - Some(Error::ErrInvalidPacketLength), - ), - ( - "Packet declared invalid length", - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0xFF, 0x01, - ], - vec![], - Some(Error::ErrInvalidPacketLength), - ), - ]; - - for (name, data, wanted, wanted_err) in tests { - let dtls_pkts = unpack_datagram(&data); - if let Some(err) = wanted_err { - if let Err(dtls) = dtls_pkts { - assert_eq!(err.to_string(), dtls.to_string()); - } else { - panic!("something wrong for {name} when wanted_err is Some"); - } - } else if let Ok(pkts) = dtls_pkts { - assert_eq!( - wanted, pkts, - "{name} UDP decode: got {pkts:?}, want {wanted:?}", - ); - } else { - panic!("something wrong for {name} when wanted_err is None"); - } - } - - Ok(()) -} - -#[test] -fn test_record_layer_round_trip() -> Result<()> { - let tests = vec![( - "Change Cipher Spec, single packet", - vec![ - 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, - ], - RecordLayer { - record_layer_header: RecordLayerHeader { - content_type: ContentType::ChangeCipherSpec, - protocol_version: ProtocolVersion { - major: 0xfe, - minor: 0xff, - }, - epoch: 0, - sequence_number: 18, - content_len: 1, - }, - content: Content::ChangeCipherSpec(ChangeCipherSpec {}), - }, - )]; - - for (name, data, want) in tests { - let mut reader = BufReader::new(data.as_slice()); - let r = RecordLayer::unmarshal(&mut reader)?; - - assert_eq!( - want, r, - "{name} recordLayer.unmarshal: got {r:?}, want {want:?}" - ); - - let mut data2 = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(data2.as_mut()); - r.marshal(&mut writer)?; - } - assert_eq!( - data, data2, - "{name} recordLayer.marshal: got {data2:?}, want {data:?}" - ); - } - - Ok(()) -} diff --git a/dtls/src/signature_hash_algorithm/mod.rs b/dtls/src/signature_hash_algorithm/mod.rs deleted file mode 100644 index 41e5dab34..000000000 --- a/dtls/src/signature_hash_algorithm/mod.rs +++ /dev/null @@ -1,215 +0,0 @@ -#[cfg(test)] -mod signature_hash_algorithm_test; - -use std::fmt; - -use crate::crypto::*; -use crate::error::*; - -// HashAlgorithm is used to indicate the hash algorithm used -// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18 -// Supported hash hash algorithms -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum HashAlgorithm { - Md2 = 0, // Blacklisted - Md5 = 1, // Blacklisted - Sha1 = 2, // Blacklisted - Sha224 = 3, - Sha256 = 4, - Sha384 = 5, - Sha512 = 6, - Ed25519 = 8, - Unsupported, -} - -impl From for HashAlgorithm { - fn from(val: u8) -> Self { - match val { - 0 => HashAlgorithm::Md2, - 1 => HashAlgorithm::Md5, - 2 => HashAlgorithm::Sha1, - 3 => HashAlgorithm::Sha224, - 4 => HashAlgorithm::Sha256, - 5 => HashAlgorithm::Sha384, - 6 => HashAlgorithm::Sha512, - 8 => HashAlgorithm::Ed25519, - _ => HashAlgorithm::Unsupported, - } - } -} - -impl fmt::Display for HashAlgorithm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - HashAlgorithm::Md2 => write!(f, "md2"), - HashAlgorithm::Md5 => write!(f, "md5"), // [RFC3279] - HashAlgorithm::Sha1 => write!(f, "sha-1"), // [RFC3279] - HashAlgorithm::Sha224 => write!(f, "sha-224"), // [RFC4055] - HashAlgorithm::Sha256 => write!(f, "sha-256"), // [RFC4055] - HashAlgorithm::Sha384 => write!(f, "sha-384"), // [RFC4055] - HashAlgorithm::Sha512 => write!(f, "sha-512"), // [RFC4055] - HashAlgorithm::Ed25519 => write!(f, "null"), // [RFC4055] - _ => write!(f, "unknown or unsupported hash algorithm"), - } - } -} - -impl HashAlgorithm { - pub(crate) fn insecure(&self) -> bool { - matches!( - *self, - HashAlgorithm::Md2 | HashAlgorithm::Md5 | HashAlgorithm::Sha1 - ) - } - - pub(crate) fn invalid(&self) -> bool { - matches!(*self, HashAlgorithm::Md2) - } -} - -// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-16 -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum SignatureAlgorithm { - Rsa = 1, - Ecdsa = 3, - Ed25519 = 7, - Unsupported, -} - -impl From for SignatureAlgorithm { - fn from(val: u8) -> Self { - match val { - 1 => SignatureAlgorithm::Rsa, - 3 => SignatureAlgorithm::Ecdsa, - 7 => SignatureAlgorithm::Ed25519, - _ => SignatureAlgorithm::Unsupported, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct SignatureHashAlgorithm { - pub hash: HashAlgorithm, - pub signature: SignatureAlgorithm, -} - -impl SignatureHashAlgorithm { - // is_compatible checks that given private key is compatible with the signature scheme. - pub(crate) fn is_compatible(&self, private_key: &CryptoPrivateKey) -> bool { - match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => self.signature == SignatureAlgorithm::Ed25519, - CryptoPrivateKeyKind::Ecdsa256(_) => self.signature == SignatureAlgorithm::Ecdsa, - CryptoPrivateKeyKind::Rsa256(_) => self.signature == SignatureAlgorithm::Rsa, - } - } -} - -pub(crate) fn default_signature_schemes() -> Vec { - vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Ed25519, - signature: SignatureAlgorithm::Ed25519, - }, - ] -} - -// select Signature Scheme returns most preferred and compatible scheme. -pub(crate) fn select_signature_scheme( - sigs: &[SignatureHashAlgorithm], - private_key: &CryptoPrivateKey, -) -> Result { - for ss in sigs { - if ss.is_compatible(private_key) { - return Ok(*ss); - } - } - - Err(Error::ErrNoAvailableSignatureSchemes) -} - -// SignatureScheme identifies a signature algorithm supported by TLS. See -// RFC 8446, Section 4.2.3. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum SignatureScheme { - // RSASSA-PKCS1-v1_5 algorithms. - Pkcs1WithSha256 = 0x0401, - Pkcs1WithSha384 = 0x0501, - Pkcs1WithSha512 = 0x0601, - - // RSASSA-PSS algorithms with public key OID rsaEncryption. - PssWithSha256 = 0x0804, - PssWithSha384 = 0x0805, - PssWithSha512 = 0x0806, - - // ECDSA algorithms. Only constrained to a specific curve in TLS 1.3. - EcdsaWithP256AndSha256 = 0x0403, - EcdsaWithP384AndSha384 = 0x0503, - EcdsaWithP521AndSha512 = 0x0603, - - // EdDSA algorithms. - Ed25519 = 0x0807, - - // Legacy signature and hash algorithms for TLS 1.2. - Pkcs1WithSha1 = 0x0201, - EcdsaWithSha1 = 0x0203, -} - -// parse_signature_schemes translates []tls.SignatureScheme to []signatureHashAlgorithm. -// It returns default signature scheme list if no SignatureScheme is passed. -pub(crate) fn parse_signature_schemes( - sigs: &[u16], - insecure_hashes: bool, -) -> Result> { - if sigs.is_empty() { - return Ok(default_signature_schemes()); - } - - let mut out = vec![]; - for ss in sigs { - let sig: SignatureAlgorithm = ((*ss & 0xFF) as u8).into(); - if sig == SignatureAlgorithm::Unsupported { - return Err(Error::ErrInvalidSignatureAlgorithm); - } - let h: HashAlgorithm = (((*ss >> 8) & 0xFF) as u8).into(); - if h == HashAlgorithm::Unsupported || h.invalid() { - return Err(Error::ErrInvalidHashAlgorithm); - } - if h.insecure() && !insecure_hashes { - continue; - } - out.push(SignatureHashAlgorithm { - hash: h, - signature: sig, - }) - } - - if out.is_empty() { - Err(Error::ErrNoAvailableSignatureSchemes) - } else { - Ok(out) - } -} diff --git a/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs b/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs deleted file mode 100644 index 775ef3277..000000000 --- a/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs +++ /dev/null @@ -1,141 +0,0 @@ -use super::*; - -#[test] -fn test_parse_signature_schemes() -> Result<()> { - let tests = vec![ - ( - "Translate", - vec![ - SignatureScheme::EcdsaWithP256AndSha256 as u16, - SignatureScheme::EcdsaWithP384AndSha384 as u16, - SignatureScheme::EcdsaWithP521AndSha512 as u16, - SignatureScheme::Pkcs1WithSha256 as u16, - SignatureScheme::Pkcs1WithSha384 as u16, - SignatureScheme::Pkcs1WithSha512 as u16, - ], - vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha384, - signature: SignatureAlgorithm::Rsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha512, - signature: SignatureAlgorithm::Rsa, - }, - ], - false, - None, - ), - ( - "InvalidSignatureAlgorithm", - vec![ - SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid - 0x04FF, // Invalid: unknown signature with SHA-256 - ], - vec![], - false, - Some(Error::ErrInvalidSignatureAlgorithm), - ), - ( - "InvalidHashAlgorithm", - vec![ - SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid - 0x0003, // Invalid: ECDSA with MD2 - ], - vec![], - false, - Some(Error::ErrInvalidHashAlgorithm), - ), - ( - "InsecureHashAlgorithmDenied", - vec![ - SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid - SignatureScheme::EcdsaWithSha1 as u16, // Insecure - ], - vec![SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }], - false, - None, - ), - ( - "InsecureHashAlgorithmAllowed", - vec![ - SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid - SignatureScheme::EcdsaWithSha1 as u16, // Insecure - ], - vec![ - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, - SignatureHashAlgorithm { - hash: HashAlgorithm::Sha1, - signature: SignatureAlgorithm::Ecdsa, - }, - ], - true, - None, - ), - ( - "OnlyInsecureHashAlgorithm", - vec![ - SignatureScheme::EcdsaWithSha1 as u16, // Insecure - ], - vec![], - false, - Some(Error::ErrNoAvailableSignatureSchemes), - ), - ( - "Translate", - vec![SignatureScheme::Ed25519 as u16], - vec![SignatureHashAlgorithm { - hash: HashAlgorithm::Ed25519, - signature: SignatureAlgorithm::Ed25519, - }], - false, - None, - ), - ]; - - for (name, inputs, expected, insecure_hashes, want_err) in tests { - let output = parse_signature_schemes(&inputs, insecure_hashes); - if let Some(err) = want_err { - if let Err(output_err) = output { - assert_eq!( - err.to_string(), - output_err.to_string(), - "Expected error: {err:?}, got: {output_err:?}" - ); - } else { - panic!("expect err, but got non-err for {name}"); - } - } else if let Ok(output_val) = output { - assert_eq!( - expected, output_val, - "Expected signatureHashAlgorithm:\n{expected:?}\ngot:\n{output_val:?}", - ); - } else { - panic!("expect non-err, but got err for {name}"); - } - } - - Ok(()) -} diff --git a/dtls/src/state.rs b/dtls/src/state.rs deleted file mode 100644 index 2d8fbc864..000000000 --- a/dtls/src/state.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::io::{BufWriter, Cursor}; -use std::marker::{Send, Sync}; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use portable_atomic::AtomicU16; -use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex; -use util::{KeyingMaterialExporter, KeyingMaterialExporterError}; - -use super::cipher_suite::*; -use super::conn::*; -use super::curve::named_curve::*; -use super::extension::extension_use_srtp::SrtpProtectionProfile; -use super::handshake::handshake_random::*; -use super::prf::*; -use crate::error::*; - -// State holds the dtls connection state and implements both encoding.BinaryMarshaler and encoding.BinaryUnmarshaler -pub struct State { - pub(crate) local_epoch: Arc, - pub(crate) remote_epoch: Arc, - pub(crate) local_sequence_number: Arc>>, // uint48 - pub(crate) local_random: HandshakeRandom, - pub(crate) remote_random: HandshakeRandom, - pub(crate) master_secret: Vec, - pub(crate) cipher_suite: Arc>>>, // nil if a cipher_suite hasn't been chosen - - pub(crate) srtp_protection_profile: SrtpProtectionProfile, // Negotiated srtp_protection_profile - pub peer_certificates: Vec>, - pub identity_hint: Vec, - - pub(crate) is_client: bool, - - pub(crate) pre_master_secret: Vec, - pub(crate) extended_master_secret: bool, - - pub(crate) named_curve: NamedCurve, - pub(crate) local_keypair: Option, - pub(crate) cookie: Vec, - pub(crate) handshake_send_sequence: isize, - pub(crate) handshake_recv_sequence: isize, - pub(crate) server_name: String, - pub(crate) remote_requested_certificate: bool, // Did we get a CertificateRequest - pub(crate) local_certificates_verify: Vec, // cache CertificateVerify - pub(crate) local_verify_data: Vec, // cached VerifyData - pub(crate) local_key_signature: Vec, // cached keySignature - pub(crate) peer_certificates_verified: bool, - //pub(crate) replay_detector: Vec>, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -struct SerializedState { - local_epoch: u16, - remote_epoch: u16, - local_random: [u8; HANDSHAKE_RANDOM_LENGTH], - remote_random: [u8; HANDSHAKE_RANDOM_LENGTH], - cipher_suite_id: u16, - master_secret: Vec, - sequence_number: u64, - srtp_protection_profile: u16, - peer_certificates: Vec>, - identity_hint: Vec, - is_client: bool, -} - -impl Default for State { - fn default() -> Self { - State { - local_epoch: Arc::new(AtomicU16::new(0)), - remote_epoch: Arc::new(AtomicU16::new(0)), - local_sequence_number: Arc::new(Mutex::new(vec![])), - local_random: HandshakeRandom::default(), - remote_random: HandshakeRandom::default(), - master_secret: vec![], - cipher_suite: Arc::new(Mutex::new(None)), // nil if a cipher_suite hasn't been chosen - - srtp_protection_profile: SrtpProtectionProfile::Unsupported, // Negotiated srtp_protection_profile - peer_certificates: vec![], - identity_hint: vec![], - - is_client: false, - - pre_master_secret: vec![], - extended_master_secret: false, - - named_curve: NamedCurve::Unsupported, - local_keypair: None, - cookie: vec![], - handshake_send_sequence: 0, - handshake_recv_sequence: 0, - server_name: "".to_string(), - remote_requested_certificate: false, // Did we get a CertificateRequest - local_certificates_verify: vec![], // cache CertificateVerify - local_verify_data: vec![], // cached VerifyData - local_key_signature: vec![], // cached keySignature - peer_certificates_verified: false, - //replay_detector: vec![], - } - } -} - -impl State { - pub(crate) async fn clone(&self) -> Self { - let mut state = State::default(); - - if let Ok(serialized) = self.serialize().await { - let _ = state.deserialize(&serialized).await; - } - - state - } - - async fn serialize(&self) -> Result { - let mut local_rand = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(local_rand.as_mut()); - self.local_random.marshal(&mut writer)?; - } - let mut remote_rand = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(remote_rand.as_mut()); - self.remote_random.marshal(&mut writer)?; - } - - let mut local_random = [0u8; HANDSHAKE_RANDOM_LENGTH]; - let mut remote_random = [0u8; HANDSHAKE_RANDOM_LENGTH]; - - local_random.copy_from_slice(&local_rand); - remote_random.copy_from_slice(&remote_rand); - - let local_epoch = self.local_epoch.load(Ordering::SeqCst); - let remote_epoch = self.remote_epoch.load(Ordering::SeqCst); - let sequence_number = { - let lsn = self.local_sequence_number.lock().await; - lsn[local_epoch as usize] - }; - let cipher_suite_id = { - let cipher_suite = self.cipher_suite.lock().await; - match &*cipher_suite { - Some(cipher_suite) => cipher_suite.id() as u16, - None => return Err(Error::ErrCipherSuiteUnset), - } - }; - - Ok(SerializedState { - local_epoch, - remote_epoch, - local_random, - remote_random, - cipher_suite_id, - master_secret: self.master_secret.clone(), - sequence_number, - srtp_protection_profile: self.srtp_protection_profile as u16, - peer_certificates: self.peer_certificates.clone(), - identity_hint: self.identity_hint.clone(), - is_client: self.is_client, - }) - } - - async fn deserialize(&mut self, serialized: &SerializedState) -> Result<()> { - // Set epoch values - self.local_epoch - .store(serialized.local_epoch, Ordering::SeqCst); - self.remote_epoch - .store(serialized.remote_epoch, Ordering::SeqCst); - { - let mut lsn = self.local_sequence_number.lock().await; - while lsn.len() <= serialized.local_epoch as usize { - lsn.push(0); - } - lsn[serialized.local_epoch as usize] = serialized.sequence_number; - } - - // Set random values - let mut reader = Cursor::new(&serialized.local_random); - self.local_random = HandshakeRandom::unmarshal(&mut reader)?; - - let mut reader = Cursor::new(&serialized.remote_random); - self.remote_random = HandshakeRandom::unmarshal(&mut reader)?; - - self.is_client = serialized.is_client; - - // Set master secret - self.master_secret.clone_from(&serialized.master_secret); - - // Set cipher suite - self.cipher_suite = Arc::new(Mutex::new(Some(cipher_suite_for_id( - serialized.cipher_suite_id.into(), - )?))); - - self.srtp_protection_profile = serialized.srtp_protection_profile.into(); - - // Set remote certificate - self.peer_certificates - .clone_from(&serialized.peer_certificates); - self.identity_hint.clone_from(&serialized.identity_hint); - - Ok(()) - } - - pub async fn init_cipher_suite(&mut self) -> Result<()> { - let mut cipher_suite = self.cipher_suite.lock().await; - if let Some(cipher_suite) = &mut *cipher_suite { - if cipher_suite.is_initialized() { - return Ok(()); - } - - let mut local_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(local_random.as_mut()); - self.local_random.marshal(&mut writer)?; - } - let mut remote_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(remote_random.as_mut()); - self.remote_random.marshal(&mut writer)?; - } - - if self.is_client { - cipher_suite.init(&self.master_secret, &local_random, &remote_random, true) - } else { - cipher_suite.init(&self.master_secret, &remote_random, &local_random, false) - } - } else { - Err(Error::ErrCipherSuiteUnset) - } - } - - // marshal_binary is a binary.BinaryMarshaler.marshal_binary implementation - pub async fn marshal_binary(&self) -> Result> { - let serialized = self.serialize().await?; - - match bincode::serialize(&serialized) { - Ok(enc) => Ok(enc), - Err(err) => Err(Error::Other(err.to_string())), - } - } - - // unmarshal_binary is a binary.BinaryUnmarshaler.unmarshal_binary implementation - pub async fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { - let serialized: SerializedState = match bincode::deserialize(data) { - Ok(dec) => dec, - Err(err) => return Err(Error::Other(err.to_string())), - }; - self.deserialize(&serialized).await?; - self.init_cipher_suite().await?; - - Ok(()) - } -} - -#[async_trait] -impl KeyingMaterialExporter for State { - /// export_keying_material returns length bytes of exported key material in a new - /// slice as defined in RFC 5705. - /// This allows protocols to use DTLS for key establishment, but - /// then use some of the keying material for their own purposes - async fn export_keying_material( - &self, - label: &str, - context: &[u8], - length: usize, - ) -> std::result::Result, KeyingMaterialExporterError> { - use KeyingMaterialExporterError::*; - - if self.local_epoch.load(Ordering::SeqCst) == 0 { - return Err(HandshakeInProgress); - } else if !context.is_empty() { - return Err(ContextUnsupported); - } else if INVALID_KEYING_LABELS.contains(&label) { - return Err(ReservedExportKeyingMaterial); - } - - let mut local_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(local_random.as_mut()); - self.local_random.marshal(&mut writer)?; - } - let mut remote_random = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(remote_random.as_mut()); - self.remote_random.marshal(&mut writer)?; - } - - let mut seed = label.as_bytes().to_vec(); - if self.is_client { - seed.extend_from_slice(&local_random); - seed.extend_from_slice(&remote_random); - } else { - seed.extend_from_slice(&remote_random); - seed.extend_from_slice(&local_random); - } - - let cipher_suite = self.cipher_suite.lock().await; - if let Some(cipher_suite) = &*cipher_suite { - match prf_p_hash(&self.master_secret, &seed, length, cipher_suite.hash_func()) { - Ok(v) => Ok(v), - Err(err) => Err(Hash(err.to_string())), - } - } else { - Err(CipherSuiteUnset) - } - } -} diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/examples/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/examples/Cargo.toml b/examples/Cargo.toml deleted file mode 100644 index 04733ac77..000000000 --- a/examples/Cargo.toml +++ /dev/null @@ -1,153 +0,0 @@ -[package] -name = "examples" -version = "0.5.0" -authors = ["Rain Liu "] -edition = "2021" -description = "Examples of WebRTC.rs stack" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/examples" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/examples" - -[dependencies] - - -[dev-dependencies] -webrtc = { path = "../webrtc" } - -tokio = { version = "1.32.0", features = ["full"] } -env_logger = "0.10" -clap = "3" -hyper = { version = "0.14.27", features = ["full"] } -signal = { path = "examples/signal" } -tokio-util = { version = "0.7", features = ["codec"] } -anyhow = "1" -chrono = "0.4.28" -log = "0.4" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -bytes = "1" -lazy_static = "1" -rand = "0.8" - -memchr = "2.1.1" - -[[example]] -name = "rc-cycle" -path = "examples/rc-cycle/rc-cycle.rs" -bench = false - -[[example]] -name = "broadcast" -path = "examples/broadcast/broadcast.rs" -bench = false - -[[example]] -name = "data-channels" -path = "examples/data-channels/data-channels.rs" -bench = false - -[[example]] -name = "data-channels-close" -path = "examples/data-channels-close/data-channels-close.rs" -bench = false - -[[example]] -name = "data-channels-create" -path = "examples/data-channels-create/data-channels-create.rs" -bench = false - -[[example]] -name = "data-channels-detach" -path = "examples/data-channels-detach/data-channels-detach.rs" -bench = false - -[[example]] -name = "data-channels-detach-create" -path = "examples/data-channels-detach-create/data-channels-detach-create.rs" -bench = false - -[[example]] -name = "data-channels-flow-control" -path = "examples/data-channels-flow-control/data-channels-flow-control.rs" -bench = false - -[[example]] -name = "insertable-streams" -path = "examples/insertable-streams/insertable-streams.rs" -bench = false - -[[example]] -name = "play-from-disk-vpx" -path = "examples/play-from-disk-vpx/play-from-disk-vpx.rs" -bench = false - -[[example]] -name = "play-from-disk-h264" -path = "examples/play-from-disk-h264/play-from-disk-h264.rs" -bench = false - -[[example]] -name = "play-from-disk-hevc" -path = "examples/play-from-disk-hevc/play-from-disk-hevc.rs" -bench = false - -[[example]] -name = "play-from-disk-renegotiation" -path = "examples/play-from-disk-renegotiation/play-from-disk-renegotiation.rs" -bench = false - -[[example]] -name = "reflect" -path = "examples/reflect/reflect.rs" -bench = false - -[[example]] -name = "rtp-forwarder" -path = "examples/rtp-forwarder/rtp-forwarder.rs" -bench = false - -[[example]] -name = "rtp-to-webrtc" -path = "examples/rtp-to-webrtc/rtp-to-webrtc.rs" -bench = false - -[[example]] -name = "save-to-disk-vpx" -path = "examples/save-to-disk-vpx/save-to-disk-vpx.rs" -bench = false - -[[example]] -name = "save-to-disk-h264" -path = "examples/save-to-disk-h264/save-to-disk-h264.rs" -bench = false - -[[example]] -name = "simulcast" -path = "examples/simulcast/simulcast.rs" -bench = false - -[[example]] -name = "swap-tracks" -path = "examples/swap-tracks/swap-tracks.rs" -bench = false - -[[example]] -name = "ortc" -path = "examples/ortc/ortc.rs" -bench = false - -[[example]] -name = "offer" -path = "examples/offer-answer/offer.rs" -bench = false - -[[example]] -name = "answer" -path = "examples/offer-answer/answer.rs" -bench = false - -[[example]] -name = "ice-restart" -path = "examples/ice-restart/ice-restart.rs" -bench = false diff --git a/examples/LICENSE-APACHE b/examples/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/examples/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/examples/LICENSE-MIT b/examples/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/examples/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 173a7a724..000000000 --- a/examples/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- Examples of WebRTC.rs stack. Rewrite Pion Examples in Rust -

diff --git a/examples/codecov.yml b/examples/codecov.yml deleted file mode 100644 index bb738da7e..000000000 --- a/examples/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: ec7b9766-689c-46bf-99fe-6c8e9971d20b - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/examples/doc/webrtc.rs.png b/examples/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/examples/examples/README.md b/examples/examples/README.md deleted file mode 100644 index 4382f6635..000000000 --- a/examples/examples/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- Examples -

- -All examples are ported from [Pion](https://github.com/pion/webrtc/tree/master/examples#readme). Please check [Pion Examples](https://github.com/pion/webrtc/tree/master/examples#readme) for more details: - -#### Media API -- [x] [Reflect](reflect): The reflect example demonstrates how to have webrtc-rs send back to the user exactly what it receives using the same PeerConnection. -- [x] [Play from Disk VPx](play-from-disk-vpx): The play-from-disk-vp8 example demonstrates how to send VP8/VP9 video to your browser from a file saved to disk. -- [x] [Play from Disk H264](play-from-disk-h264): The play-from-disk-h264 example demonstrates how to send H264 video to your browser from a file saved to disk. -- [x] [Play from Disk Renegotiation](play-from-disk-renegotiation): The play-from-disk-renegotiation example is an extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already negotiated PeerConnection. -- [x] [Insertable Streams](insertable-streams): The insertable-streams example demonstrates how webrtc-rs can be used to send E2E encrypted video and decrypt via insertable streams in the browser. -- [x] [Save to Disk VPx](save-to-disk-vpx): The save-to-disk example shows how to record your webcam and save the footage (VP8/VP9 for video, Opus for audio) to disk on the server side. -- [x] [Save to Disk H264](save-to-disk-h264): The save-to-disk example shows how to record your webcam and save the footage (H264 for video, Opus for audio) to disk on the server side. -- [x] [Broadcast](broadcast): The broadcast example demonstrates how to broadcast a video to multiple peers. A broadcaster uploads the video once and the server forwards it to all other peers. -- [x] [RTP Forwarder](rtp-forwarder): The rtp-forwarder example demonstrates how to forward your audio/video streams using RTP. -- [x] [RTP to WebRTC](rtp-to-webrtc): The rtp-to-webrtc example demonstrates how to take RTP packets sent to a webrtc-rs process into your browser. -- [x] [Simulcast](simulcast): The simulcast example demonstrates how to accept and demux 1 Track that contains 3 Simulcast streams. It then returns the media as 3 independent Tracks back to the sender. -- [x] [Swap Tracks](swap-tracks): The swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing track. - -#### Data Channel API -- [x] [Data Channels](data-channels): The data-channels example shows how you can send/recv DataChannel messages from a web browser. -- [x] [Data Channels Create](data-channels-create): Example data-channels-create shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the data channel is initialized from the server side in this example. -- [x] [Data Channels Close](data-channels-close): Example data-channels-close is a variant of data-channels that allow playing with the life cycle of data channels. -- [x] [Data Channels Detach](data-channels-detach): The data-channels-detach example shows how you can send/recv DataChannel messages using the underlying DataChannel implementation directly. This provides a more idiomatic way of interacting with Data Channels. -- [x] [Data Channels Detach Create](data-channels-detach-create): Example data-channels-detach-create shows how you can send/recv DataChannel messages using the underlying DataChannel implementation directly. This provides a more idiomatic way of interacting with Data Channels. The difference with the data-channels-detach example is that the data channel is initialized in this example. -- [x] [Data Channels Flow Control](data-channels-flow-control): Example data-channels-flow-control shows how to use flow control. -- [x] [ORTC](ortc): Example ortc shows how to use the ORTC API for DataChannel communication. -- [x] [Offer Answer](offer-answer): Example offer-answer is an example of two webrtc-rs or pion instances communicating directly! -- [x] [ICE Restart](ice-restart): The ice-restart demonstrates webrtc-rs ICE Restart abilities. diff --git a/examples/examples/broadcast/README.md b/examples/examples/broadcast/README.md deleted file mode 100644 index 21a003dc2..000000000 --- a/examples/examples/broadcast/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# broadcast - -broadcast is a WebRTC.rs application that demonstrates how to broadcast a video to many peers, while only requiring the broadcaster to upload once. - -This could serve as the building block to building conferencing software, and other applications where publishers are bandwidth constrained. - -## Instructions - -### Build broadcast - -```shell -cargo build --example broadcast -``` - -### Open broadcast example page - -[jsfiddle.net](https://jsfiddle.net/1jc4go7v/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast' - -### Run Broadcast - -#### Linux/macOS - -Run `broadcast` - -### Start a publisher - -* Click `Publish a Broadcast` -* Copy the string in the first input labelled `Browser base64 Session Description` -* Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step. -* The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser. -* Press `Start Session` -* The connection state will be printed in the terminal and under `logs` in the browser. - -### Join the broadcast - -* Click `Join a Broadcast` -* Copy the string in the first input labelled `Browser base64 Session Description` -* Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step. -* The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser. -* Press `Start Session` -* The connection state will be printed in the terminal and under `logs` in the browser. - -You can change the listening port using `-port 8011` - -You can `Join the broadcast` as many times as you want. The `broadcast` application is relaying all traffic, so your browser only has to upload once. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/broadcast/broadcast.rs b/examples/examples/broadcast/broadcast.rs deleted file mode 100644 index 2626863d4..000000000 --- a/examples/examples/broadcast/broadcast.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; -use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; -use webrtc::Error; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("broadcast") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of broadcast.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("port") - .takes_value(true) - .default_value("8080") - .long("port") - .help("http server port."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let port = matches.value_of("port").unwrap().parse::()?; - let mut sdp_chan_rx = signal::http_sdp_server(port).await; - - // Wait for the offer - println!("wait for the offer from http_sdp_server\n"); - let line = sdp_chan_rx.recv().await.unwrap(); - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - //println!("Receive offer from http_sdp_server:\n{:?}", offer); - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Allow us to receive 1 video track - peer_connection - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let (local_track_chan_tx, mut local_track_chan_rx) = - tokio::sync::mpsc::channel::>(1); - - let local_track_chan_tx = Arc::new(local_track_chan_tx); - // Set a handler for when a new remote track starts, this handler copies inbound RTP packets, - // replaces the SSRC and sends them back - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval - // This is a temporary fix until we implement incoming RTCP events, then we would push a PLI only when a viewer requests it - let media_ssrc = track.ssrc(); - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else{ - break; - } - } - }; - } - }); - - let local_track_chan_tx2 = Arc::clone(&local_track_chan_tx); - tokio::spawn(async move { - // Create Track that we send video back to browser on - let local_track = Arc::new(TrackLocalStaticRTP::new( - track.codec().capability, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let _ = local_track_chan_tx2.send(Arc::clone(&local_track)).await; - - // Read RTP packets being sent to webrtc-rs - while let Ok((rtp, _)) = track.read_rtp().await { - if let Err(err) = local_track.write_rtp(&rtp).await { - if Error::ErrClosedPipe != err { - print!("output track write_rtp got error: {err} and break"); - break; - } else { - print!("output track write_rtp got error: {err}"); - } - } - } - }); - - Box::pin(async {}) - })); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - Box::pin(async {}) - })); - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - if let Some(local_track) = local_track_chan_rx.recv().await { - loop { - println!("\nCurl an base64 SDP to start sendonly peer connection"); - - let line = sdp_chan_rx.recv().await.unwrap(); - let desc_data = signal::decode(line.as_str())?; - let recv_only_offer = serde_json::from_str::(&desc_data)?; - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let rtp_sender = peer_connection - .add_track(Arc::clone(&local_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new( - move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - Box::pin(async {}) - }, - )); - - // Set the remote SessionDescription - peer_connection - .set_remote_description(recv_only_offer) - .await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - } - } - - Ok(()) -} diff --git a/examples/examples/data-channels-close/README.md b/examples/examples/data-channels-close/README.md deleted file mode 100644 index 7deb096ea..000000000 --- a/examples/examples/data-channels-close/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# data-channels-close -data-channels-close is a variant of the data-channels example that allow playing with the life cycle of data channels. diff --git a/examples/examples/data-channels-close/data-channels-close.rs b/examples/examples/data-channels-close/data-channels-close.rs deleted file mode 100644 index ce76b605f..000000000 --- a/examples/examples/data-channels-close/data-channels-close.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::io::Write; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::Mutex; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::data_channel::RTCDataChannel; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("data-channels-close") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of Data-Channels-Close.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("close-after") - .takes_value(true) - .default_value("5") - .long("close-after") - .help("Close data channel after sending X times."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let close_after = Arc::new(AtomicI32::new( - matches - .value_of("close-after") - .unwrap() - .to_owned() - .parse::()?, - )); - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Register default codecs - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register data channel creation handling - peer_connection - .on_data_channel(Box::new(move |d: Arc| { - let d_label = d.label().to_owned(); - let d_id = d.id(); - println!("New DataChannel {d_label} {d_id}"); - - let close_after2 = Arc::clone(&close_after); - - // Register channel opening handling - Box::pin(async move { - let d2 = Arc::clone(&d); - let d_label2 = d_label.clone(); - let d_id2 = d_id; - d.on_open(Box::new(move || { - println!("Data channel '{d_label2}'-'{d_id2}' open. Random messages will now be sent to any connected DataChannels every 5 seconds"); - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - Box::pin(async move { - d2.on_close(Box::new(move || { - println!("Data channel '{d_label2}'-'{d_id2}' closed."); - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move{ - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = done_rx.recv() => { - break; - } - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d2.send_text(message).await.map_err(Into::into); - - let cnt = close_after2.fetch_sub(1, Ordering::SeqCst); - if cnt <= 0 { - println!("Sent times out. Closing data channel '{}'-'{}'.", d2.label(), d2.id()); - let _ = d2.close().await; - break; - } - } - }; - } - }) - })); - - // Register text message handling - d.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - }) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/data-channels-create/README.md b/examples/examples/data-channels-create/README.md deleted file mode 100644 index 786670b37..000000000 --- a/examples/examples/data-channels-create/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# data-channels-create - -data-channels-create is a WebRTC.rs application that shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the datachannel is initialized from the WebRTC.rs side in this example. - -## Instructions - -### Build data-channels-create - -```shell -cargo build --example data-channels-create -``` - -### Open data-channels-create example page - -[jsfiddle.net](https://jsfiddle.net/swgxrp94/20/) - -### Run data-channels-create - -Just run `data-channels-create`. - -### Input data-channels-create's SessionDescription into your browser - -Copy the text that `data-channels-create` just emitted and copy into first text area of the jsfiddle. - -### Hit 'Start Session' in jsfiddle - -Hit the 'Start Session' button in the browser. You should see `have-remote-offer` below the `Send Message` button. - -### Input browser's SessionDescription into data-channels-create - -Meanwhile text has appeared in the second text area of the jsfiddle. Copy the text and paste it into `data-channels-create` and hit ENTER. -In the browser you'll now see `connected` as the connection is created. If everything worked you should see `New DataChannel data`. - -Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! - -WebRTC.rs will send random messages every 5 seconds that will appear in your browser. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/data-channels-create/data-channels-create.rs b/examples/examples/data-channels-create/data-channels-create.rs deleted file mode 100644 index 87d3c0789..000000000 --- a/examples/examples/data-channels-create/data-channels-create.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("data-channels-create") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of Data-Channels-Create.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Register default codecs - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Create a datachannel with label 'data' - let data_channel = peer_connection.create_data_channel("data", None).await?; - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register channel opening handling - let d1 = Arc::clone(&data_channel); - data_channel.on_open(Box::new(move || { - println!("Data channel '{}'-'{}' open. Random messages will now be sent to any connected DataChannels every 5 seconds", d1.label(), d1.id()); - - let d2 = Arc::clone(&d1); - Box::pin(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d2.send_text(message).await.map_err(Into::into); - } - }; - } - }) - })); - - // Register text message handling - let d_label = data_channel.label().to_owned(); - data_channel.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - - // Create an offer to send to the browser - let offer = peer_connection.create_offer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(offer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - // Wait for the answer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let answer = serde_json::from_str::(&desc_data)?; - - // Apply the answer as the remote description - peer_connection.set_remote_description(answer).await?; - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/data-channels-detach-create/README.md b/examples/examples/data-channels-detach-create/README.md deleted file mode 100644 index 928b1024a..000000000 --- a/examples/examples/data-channels-detach-create/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# data-channels-detach-create - -data-channels-detach-create is an example that shows how you can detach a data channel. -This allows direct access the the underlying [webrtc-rs/data](https://github.com/webrtc-rs/data). - -The example mirrors the data-channels-create example. - -## Install - -```shell -cargo build --example data-channels-detach-create -``` - -## Usage - -The example can be used in the same way as the [Data Channels Create](data-channels-create) example. diff --git a/examples/examples/data-channels-detach-create/data-channels-detach-create.rs b/examples/examples/data-channels-detach-create/data-channels-detach-create.rs deleted file mode 100644 index 2c6301790..000000000 --- a/examples/examples/data-channels-detach-create/data-channels-detach-create.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use bytes::Bytes; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::setting_engine::SettingEngine; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; - -const MESSAGE_SIZE: usize = 1500; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("data-channels-detach-create") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of Data-Channels-Detach-Create.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Register default codecs - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Since this behavior diverges from the WebRTC API it has to be - // enabled using a settings engine. Mixing both detached and the - // OnMessage DataChannel API is not supported. - - // Create a SettingEngine and enable Detach - let mut s = SettingEngine::default(); - s.detach_data_channels(); - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .with_setting_engine(s) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Create a datachannel with label 'data' - let data_channel = peer_connection.create_data_channel("data", None).await?; - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register channel opening handling - let d = Arc::clone(&data_channel); - data_channel.on_open(Box::new(move || { - println!("Data channel '{}'-'{}' open.", d.label(), d.id()); - - let d2 = Arc::clone(&d); - Box::pin(async move { - let raw = match d2.detach().await { - Ok(raw) => raw, - Err(err) => { - println!("data channel detach got err: {err}"); - return; - } - }; - - // Handle reading from the data channel - let r = Arc::clone(&raw); - tokio::spawn(async move { - let _ = read_loop(r).await; - }); - - // Handle writing to the data channel - tokio::spawn(async move { - let _ = write_loop(raw).await; - }); - }) - })); - - // Create an offer to send to the browser - let offer = peer_connection.create_offer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(offer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the offer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - // Wait for the answer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let answer = serde_json::from_str::(&desc_data)?; - - // Apply the answer as the remote description - peer_connection.set_remote_description(answer).await?; - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} - -// read_loop shows how to read from the datachannel directly -async fn read_loop(d: Arc) -> Result<()> { - let mut buffer = vec![0u8; MESSAGE_SIZE]; - loop { - let n = match d.read(&mut buffer).await { - Ok(n) => n, - Err(err) => { - println!("Datachannel closed; Exit the read_loop: {err}"); - return Ok(()); - } - }; - - println!( - "Message from DataChannel: {}", - String::from_utf8(buffer[..n].to_vec())? - ); - } -} - -// write_loop shows how to write to the datachannel directly -async fn write_loop(d: Arc) -> Result<()> { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d.write(&Bytes::from(message)).await.map_err(Into::into); - } - }; - } - - Ok(()) -} diff --git a/examples/examples/data-channels-detach/README.md b/examples/examples/data-channels-detach/README.md deleted file mode 100644 index a181a9737..000000000 --- a/examples/examples/data-channels-detach/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# data-channels - -data-channels is a WebRTC.rs application that shows how you can send/recv DataChannel messages from a web browser - -## Instructions - -### Build data-channels-detach - -```shell -cargo build --example data-channels-detach -``` - -### Open data-channels-detach example page - -[jsfiddle.net](https://jsfiddle.net/9tsx15mg/90/) - -### Run data-channels-detach, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser's session description, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/data-channels-detach` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/data-channels-detach < my_file` - -### Input data-channels-detach's SessionDescription into your browser - -Copy the text that `data-channels` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle - -Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1` - -Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! - -WebRTC.rs will send random messages every 5 seconds that will appear in your browser. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/data-channels-detach/data-channels-detach.rs b/examples/examples/data-channels-detach/data-channels-detach.rs deleted file mode 100644 index 7c90a294a..000000000 --- a/examples/examples/data-channels-detach/data-channels-detach.rs +++ /dev/null @@ -1,249 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use bytes::Bytes; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::setting_engine::SettingEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::RTCDataChannel; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; - -const MESSAGE_SIZE: usize = 1500; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("data-channels-detach") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of Data-Channels-Detach.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Register default codecs - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Since this behavior diverges from the WebRTC API it has to be - // enabled using a settings engine. Mixing both detached and the - // OnMessage DataChannel API is not supported. - - // Create a SettingEngine and enable Detach - let mut s = SettingEngine::default(); - s.detach_data_channels(); - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .with_setting_engine(s) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register data channel creation handling - peer_connection.on_data_channel(Box::new(move |d: Arc| { - let d_label = d.label().to_owned(); - let d_id = d.id(); - println!("New DataChannel {d_label} {d_id}"); - - // Register channel opening handling - Box::pin(async move { - let d2 = Arc::clone(&d); - let d_label2 = d_label.clone(); - let d_id2 = d_id; - d.on_open(Box::new(move || { - println!("Data channel '{d_label2}'-'{d_id2}' open."); - - Box::pin(async move { - let raw = match d2.detach().await { - Ok(raw) => raw, - Err(err) => { - println!("data channel detach got err: {err}"); - return; - } - }; - - // Handle reading from the data channel - let r = Arc::clone(&raw); - tokio::spawn(async move { - let _ = read_loop(r).await; - }); - - // Handle writing to the data channel - tokio::spawn(async move { - let _ = write_loop(raw).await; - }); - }) - })); - }) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} - -// read_loop shows how to read from the datachannel directly -async fn read_loop(d: Arc) -> Result<()> { - let mut buffer = vec![0u8; MESSAGE_SIZE]; - loop { - let n = match d.read(&mut buffer).await { - Ok(n) => n, - Err(err) => { - println!("Datachannel closed; Exit the read_loop: {err}"); - return Ok(()); - } - }; - - println!( - "Message from DataChannel: {}", - String::from_utf8(buffer[..n].to_vec())? - ); - } -} - -// write_loop shows how to write to the datachannel directly -async fn write_loop(d: Arc) -> Result<()> { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d.write(&Bytes::from(message)).await.map_err(Into::into); - } - }; - } - - Ok(()) -} diff --git a/examples/examples/data-channels-flow-control/README.md b/examples/examples/data-channels-flow-control/README.md deleted file mode 100644 index d9111eee6..000000000 --- a/examples/examples/data-channels-flow-control/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# data-channels-flow-control - -This example demonstrates how to use the following property / methods. - -* pub async fn buffered_amount(&self) -> usize -* pub async fn set_buffered_amount_low_threshold(&self, th: usize) -* pub async fn buffered_amount_low_threshold(&self) -> usize -* pub async fn on_buffered_amount_low(&self, f: OnBufferedAmountLowFn) - -These methods are equivalent to that of JavaScript WebRTC API. -See for more details. - -## When do we need it? - -Send or SendText methods are called on DataChannel to send data to the connected peer. -The methods return immediately, but it does not mean the data was actually sent onto -the wire. Instead, it is queued in a buffer until it actually gets sent out to the wire. - -When you have a large amount of data to send, it is an application's responsibility to -control the buffered amount in order not to indefinitely grow the buffer size to eventually -exhaust the memory. - -The rate you wish to send data might be much higher than the rate the data channel can -actually send to the peer over the Internet. The above properties/methods help your -application to pace the amount of data to be pushed into the data channel. - -## How to run the example code - -The demo code implements two endpoints (requester and responder) in it. - -```plain - signaling messages - +----------------------------------------+ - | | - v v - +---------------+ +---------------+ - | | data | | - | requester |----------------------->| responder | - |:PeerConnection| |:PeerConnection| - +---------------+ +---------------+ -``` - -First requester and responder will exchange signaling message to establish a peer-to-peer -connection, and data channel (label: "data"). - -Once the data channel is successfully opened, requester will start sending a series of -1024-byte packets to responder, until you kill the process by Ctrl+С. - -Here's how to run the code: - -```shell -$ cargo run --release --example data-channels-flow-control - Finished release [optimized] target(s) in 0.36s - Running `target\release\examples\data-channels-flow-control.exe` - -Throughput is about 127.060 Mbps -Throughput is about 122.091 Mbps -Throughput is about 120.630 Mbps -Throughput is about 120.105 Mbps -Throughput is about 119.873 Mbps -Throughput is about 118.890 Mbps -Throughput is about 118.525 Mbps -Throughput is about 118.614 Mbps -``` diff --git a/examples/examples/data-channels-flow-control/data-channels-flow-control.rs b/examples/examples/data-channels-flow-control/data-channels-flow-control.rs deleted file mode 100644 index ba1999d79..000000000 --- a/examples/examples/data-channels-flow-control/data-channels-flow-control.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use bytes::Bytes; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_init::RTCDataChannelInit; -use webrtc::ice_transport::ice_candidate::RTCIceCandidate; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::RTCPeerConnection; - -const BUFFERED_AMOUNT_LOW_THRESHOLD: usize = 512 * 1024; // 512 KB -const MAX_BUFFERED_AMOUNT: usize = 1024 * 1024; // 1 MB - -async fn create_peer_connection() -> anyhow::Result { - // Create unique MediaEngine, - // as MediaEngine must not be shared between PeerConnections - let mut media_engine = MediaEngine::default(); - - media_engine.register_default_codecs()?; - - let mut interceptor_registry = Registry::new(); - - interceptor_registry = register_default_interceptors(interceptor_registry, &mut media_engine)?; - - // Create API that bundles the global functions of the WebRTC API - let api = APIBuilder::new() - .with_media_engine(media_engine) - .with_interceptor_registry(interceptor_registry) - .build(); - - let ice_servers = vec![RTCIceServer { - ..Default::default() - }]; - - let config = RTCConfiguration { - ice_servers, - ..Default::default() - }; - - Ok(api.new_peer_connection(config).await?) -} - -async fn create_requester() -> anyhow::Result { - // Create a peer connection first - let pc = create_peer_connection().await?; - - // Data transmission requires a data channel, so prepare to create one - let options = Some(RTCDataChannelInit { - ordered: Some(false), - max_retransmits: Some(0u16), - ..Default::default() - }); - - // Create a data channel to send data over a peer connection - let dc = pc.create_data_channel("data", options).await?; - - // Use mpsc channel to send and receive a signal when more data can be sent - let (more_can_be_sent, mut maybe_more_can_be_sent) = tokio::sync::mpsc::channel(1); - - // Get a shared pointer to the data channel - let shared_dc = dc.clone(); - dc.on_open(Box::new(|| { - Box::pin(async move { - // This callback shouldn't be blocked for a long time, so we spawn our handler - tokio::spawn(async move { - let buf = Bytes::from_static(&[0u8; 1024]); - - loop { - if shared_dc.send(&buf).await.is_err() { - break; - } - - let buffered_amount = shared_dc.buffered_amount().await; - - if buffered_amount + buf.len() > MAX_BUFFERED_AMOUNT { - // Wait for the signal that more can be sent - let _ = maybe_more_can_be_sent.recv().await; - } - } - }); - }) - })); - - dc.set_buffered_amount_low_threshold(BUFFERED_AMOUNT_LOW_THRESHOLD) - .await; - - dc.on_buffered_amount_low(Box::new(move || { - let more_can_be_sent = more_can_be_sent.clone(); - - Box::pin(async move { - // Send a signal that more can be sent - more_can_be_sent.send(()).await.unwrap(); - }) - })) - .await; - - Ok(pc) -} - -async fn create_responder() -> anyhow::Result { - // Create a peer connection first - let pc = create_peer_connection().await?; - - // Set a data channel handler so that we can receive data - pc.on_data_channel(Box::new(move |dc| { - Box::pin(async move { - let total_bytes_received = Arc::new(AtomicUsize::new(0)); - - let shared_total_bytes_received = total_bytes_received.clone(); - dc.on_open(Box::new(move || { - Box::pin(async { - // This callback shouldn't be blocked for a long time, so we spawn our handler - tokio::spawn(async move { - let start = SystemTime::now(); - - tokio::time::sleep(Duration::from_secs(1)).await; - println!(); - - loop { - let total_bytes_received = - shared_total_bytes_received.load(Ordering::Relaxed); - - let elapsed = SystemTime::now().duration_since(start); - let bps = - (total_bytes_received * 8) as f64 / elapsed.unwrap().as_secs_f64(); - - println!( - "Throughput is about {:.03} Mbps", - bps / (1024 * 1024) as f64 - ); - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - }) - })); - - dc.on_message(Box::new(move |msg| { - let total_bytes_received = total_bytes_received.clone(); - - Box::pin(async move { - total_bytes_received.fetch_add(msg.data.len(), Ordering::Relaxed); - }) - })); - }) - })); - - Ok(pc) -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - env_logger::init(); - - let requester = Arc::new(create_requester().await?); - let responder = Arc::new(create_responder().await?); - - let maybe_requester = Arc::downgrade(&requester); - responder.on_ice_candidate(Box::new(move |candidate: Option| { - let maybe_requester = maybe_requester.clone(); - - Box::pin(async move { - if let Some(candidate) = candidate { - if let Ok(candidate) = candidate.to_json() { - if let Some(requester) = maybe_requester.upgrade() { - if let Err(err) = requester.add_ice_candidate(candidate).await { - log::warn!("{}", err); - } - } - } - } - }) - })); - - let maybe_responder = Arc::downgrade(&responder); - requester.on_ice_candidate(Box::new(move |candidate: Option| { - let maybe_responder = maybe_responder.clone(); - - Box::pin(async move { - if let Some(candidate) = candidate { - if let Ok(candidate) = candidate.to_json() { - if let Some(responder) = maybe_responder.upgrade() { - if let Err(err) = responder.add_ice_candidate(candidate).await { - log::warn!("{}", err); - } - } - } - } - }) - })); - - let (fault, mut reqs_fault) = tokio::sync::mpsc::channel(1); - requester.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - let fault = fault.clone(); - - Box::pin(async move { - if s == RTCPeerConnectionState::Failed { - fault.send(()).await.unwrap(); - } - }) - })); - - let (fault, mut resp_fault) = tokio::sync::mpsc::channel(1); - responder.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - let fault = fault.clone(); - - Box::pin(async move { - if s == RTCPeerConnectionState::Failed { - fault.send(()).await.unwrap(); - } - }) - })); - - let reqs = requester.create_offer(None).await?; - - requester.set_local_description(reqs.clone()).await?; - responder.set_remote_description(reqs).await?; - - let resp = responder.create_answer(None).await?; - - responder.set_local_description(resp.clone()).await?; - requester.set_remote_description(resp).await?; - - tokio::select! { - _ = tokio::signal::ctrl_c() => {} - _ = reqs_fault.recv() => { - log::error!("Requester's peer connection failed...") - } - _ = resp_fault.recv() => { - log::error!("Responder's peer connection failed..."); - } - } - - requester.close().await?; - responder.close().await?; - - println!(); - - Ok(()) -} diff --git a/examples/examples/data-channels/README.md b/examples/examples/data-channels/README.md deleted file mode 100644 index 6b4019cb9..000000000 --- a/examples/examples/data-channels/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# data-channels - -data-channels is a WebRTC.rs application that shows how you can send/recv DataChannel messages from a web browser - -## Instructions - -### Build data-channels - -```shell -cargo build --example data-channels -``` - -### Open data-channels example page - -[jsfiddle.net](https://jsfiddle.net/9tsx15mg/90/) - -### Run data-channels, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser's session description, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/data-channels` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/data-channels < my_file` - -### Input data-channels's SessionDescription into your browser - -Copy the text that `data-channels` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle - -Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1` - -Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! - -WebRTC.rs will send random messages every 5 seconds that will appear in your browser. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/data-channels/data-channels.rs b/examples/examples/data-channels/data-channels.rs deleted file mode 100644 index 708f20342..000000000 --- a/examples/examples/data-channels/data-channels.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::data_channel::RTCDataChannel; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("data-channels") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of Data-Channels.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Register default codecs - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register data channel creation handling - peer_connection - .on_data_channel(Box::new(move |d: Arc| { - let d_label = d.label().to_owned(); - let d_id = d.id(); - println!("New DataChannel {d_label} {d_id}"); - - // Register channel opening handling - Box::pin(async move { - let d2 = Arc::clone(&d); - let d_label2 = d_label.clone(); - let d_id2 = d_id; - d.on_close(Box::new(move || { - println!("Data channel closed"); - Box::pin(async {}) - })); - - d.on_open(Box::new(move || { - println!("Data channel '{d_label2}'-'{d_id2}' open. Random messages will now be sent to any connected DataChannels every 5 seconds"); - - Box::pin(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d2.send_text(message).await.map_err(Into::into); - } - }; - } - }) - })); - - // Register text message handling - d.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - }) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/ice-restart/README.md b/examples/examples/ice-restart/README.md deleted file mode 100644 index c3d707b7f..000000000 --- a/examples/examples/ice-restart/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# ice-restart -ice-restart demonstrates WebRTC.rs ICE Restart abilities. - -## Instructions - -### Build ice-restart -```shell -cargo build --example ice-restart -``` - -### Run ice-restart -```shell -cargo run --example ice-restart -``` - -### Open the Web UI -Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection -and allow you to do an ICE Restart at anytime. - -* `ICE Restart` is the button that causes a new offer to be made with `iceRestart: true`. -* `ICE Connection States` will contain all the connection states the PeerConnection moves through. -* `ICE Selected Pairs` will print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you start the Restart process. -* `Inbound DataChannel Messages` containing the current time sent by the Pion process every 3 seconds. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/ice-restart/ice-restart.rs b/examples/examples/ice-restart/ice-restart.rs deleted file mode 100644 index 11fe51d28..000000000 --- a/examples/examples/ice-restart/ice-restart.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::io::Write; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use tokio::sync::Mutex; -use tokio::time::Duration; -use tokio_util::codec::{BytesCodec, FramedRead}; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::RTCDataChannel; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::peer_connection::RTCPeerConnection; - -#[macro_use] -extern crate lazy_static; - -lazy_static! { - static ref PEER_CONNECTION_MUTEX: Arc>>> = - Arc::new(Mutex::new(None)); -} - -static INDEX: &str = "examples/examples/ice-restart/index.html"; -static NOTFOUND: &[u8] = b"Not Found"; - -/// HTTP status code 404 -fn not_found() -> Response { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(NOTFOUND.into()) - .unwrap() -} - -async fn simple_file_send(filename: &str) -> Result, hyper::Error> { - // Serve a file by asynchronously reading it by chunks using tokio-util crate. - - if let Ok(file) = tokio::fs::File::open(filename).await { - let stream = FramedRead::new(file, BytesCodec::new()); - let body = Body::wrap_stream(stream); - return Ok(Response::new(body)); - } - - Ok(not_found()) -} - -// HTTP Listener to get ICE Credentials/Candidate from remote Peer -async fn remote_handler(req: Request) -> Result, hyper::Error> { - match (req.method(), req.uri().path()) { - (&Method::GET, "/") | (&Method::GET, "/index.html") => simple_file_send(INDEX).await, - - (&Method::POST, "/doSignaling") => do_signaling(req).await, - - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -// do_signaling exchanges all state of the local PeerConnection and is called -// every time a video is added or removed -async fn do_signaling(req: Request) -> Result, hyper::Error> { - let pc = { - let mut peer_connection = PEER_CONNECTION_MUTEX.lock().await; - if let Some(pc) = &*peer_connection { - Arc::clone(pc) - } else { - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - match m.register_default_codecs() { - Ok(_) => {} - Err(err) => panic!("{}", err), - }; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = match register_default_interceptors(registry, &mut m) { - Ok(r) => r, - Err(err) => panic!("{}", err), - }; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Create a new RTCPeerConnection - let pc = match api.new_peer_connection(RTCConfiguration::default()).await { - Ok(p) => p, - Err(err) => panic!("{}", err), - }; - let pc = Arc::new(pc); - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - pc.on_ice_connection_state_change(Box::new( - |connection_state: RTCIceConnectionState| { - println!("ICE Connection State has changed: {connection_state}"); - Box::pin(async {}) - }, - )); - - // Send the current time via a DataChannel to the remote peer every 3 seconds - pc.on_data_channel(Box::new(|d: Arc| { - Box::pin(async move { - let d2 = Arc::clone(&d); - d.on_open(Box::new(move || { - Box::pin(async move { - while d2 - .send_text(format!("{:?}", tokio::time::Instant::now())) - .await - .is_ok() - { - tokio::time::sleep(Duration::from_secs(3)).await; - } - }) - })); - }) - })); - - *peer_connection = Some(Arc::clone(&pc)); - pc - } - }; - - let sdp_str = match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let offer = match serde_json::from_str::(&sdp_str) { - Ok(s) => s, - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc.set_remote_description(offer).await { - panic!("{}", err); - } - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = pc.gathering_complete_promise().await; - - // Create an answer - let answer = match pc.create_answer(None).await { - Ok(answer) => answer, - Err(err) => panic!("{}", err), - }; - - // Sets the LocalDescription, and starts our UDP listeners - if let Err(err) = pc.set_local_description(answer).await { - panic!("{}", err); - } - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - let payload = if let Some(local_desc) = pc.local_description().await { - match serde_json::to_string(&local_desc) { - Ok(p) => p, - Err(err) => panic!("{}", err), - } - } else { - panic!("generate local_description failed!"); - }; - - let mut response = match Response::builder() - .header("content-type", "application/json") - .body(Body::from(payload)) - { - Ok(res) => res, - Err(err) => panic!("{}", err), - }; - - *response.status_mut() = StatusCode::OK; - Ok(response) -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("ice-restart") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of ice-restart.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - tokio::spawn(async move { - println!("Open http://localhost:8080 to access this demo"); - - let addr = SocketAddr::from_str("0.0.0.0:8080").unwrap(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - // Run this server for... forever! - if let Err(e) = server.await { - eprintln!("server error: {e}"); - } - }); - - println!("Press ctrl-c to stop"); - tokio::signal::ctrl_c().await.unwrap(); - - Ok(()) -} diff --git a/examples/examples/ice-restart/index.html b/examples/examples/ice-restart/index.html deleted file mode 100644 index 99eccffe0..000000000 --- a/examples/examples/ice-restart/index.html +++ /dev/null @@ -1,82 +0,0 @@ - - - ice-restart - - - -
- - -

ICE Connection States

-

- -

ICE Selected Pairs

-

- -

Inbound DataChannel Messages

-
- - - - diff --git a/examples/examples/insertable-streams/README.md b/examples/examples/insertable-streams/README.md deleted file mode 100644 index 55c0f9efb..000000000 --- a/examples/examples/insertable-streams/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# insertable-streams - -insertable-streams demonstrates how to use insertable streams with WebRTC.rs. -This example modifies the video with a single-byte XOR cipher before sending, and then -decrypts in Javascript. - -insertable-streams allows the browser to process encoded video. You could implement -E2E encryption, add metadata or insert a completely different video feed! - -## Instructions - -### Create IVF named `output.ivf` that contains a VP8 track - -```shell -ffmpeg -i $INPUT_FILE -g 30 output.ivf -``` - -### Build insertable-streams - -```shell -cargo build --example insertable-streams -``` - -### Open insertable-streams example page - -[jsfiddle.net](https://jsfiddle.net/uqr80Lak/) you should see two text-areas and a 'Start Session' button. You will also have a 'Decrypt' checkbox. -When unchecked the browser will not decrypt the incoming video stream, so it will stop playing or display certificates. - -### Run insertable-streams with your browsers SessionDescription as stdin - -The `output.ivf` you created should be in the same directory as `insertable-streams`. In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/insertable-streams` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/insertable-streams < my_file` - -### Input insertable-streams's SessionDescription into your browser - -Copy the text that `insertable-streams` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -A video should start playing in your browser above the input boxes. `insertable-streams` will exit when the file reaches the end. - -To stop decrypting the stream uncheck the box and the video will not be viewable. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/insertable-streams/insertable-streams.rs b/examples/examples/insertable-streams/insertable-streams.rs deleted file mode 100644 index 4e45499fa..000000000 --- a/examples/examples/insertable-streams/insertable-streams.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::fs::File; -use std::io::{BufReader, Write}; -use std::path::Path; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::Notify; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::ivf_reader::IVFReader; -use webrtc::media::Sample; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use webrtc::track::track_local::TrackLocal; -use webrtc::Error; - -const CIPHER_KEY: u8 = 0xAA; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("insertable-streams") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of insertable-streams.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let video_file = matches.value_of("video").unwrap(); - if !Path::new(video_file).exists() { - return Err(Error::new(format!("video file: '{video_file}' not exist")).into()); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let video_done_tx = done_tx.clone(); - - // Create a video track - let video_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&video_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let notify_tx = Arc::new(Notify::new()); - let notify_video = notify_tx.clone(); - - let video_file_name = video_file.to_owned(); - tokio::spawn(async move { - // Open a IVF file and start reading using our IVFReader - let file = File::open(video_file_name)?; - let reader = BufReader::new(file); - let (mut ivf, header) = IVFReader::new(reader)?; - - // Wait for connection established - notify_video.notified().await; - - println!("play video from disk file output.ivf"); - - // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. - // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. - let sleep_time = Duration::from_millis( - ((1000 * header.timebase_numerator) / header.timebase_denominator) as u64, - ); - loop { - let mut frame = match ivf.parse_next_frame() { - Ok((frame, _)) => frame, - Err(err) => { - println!("All video frames parsed and sent: {err}"); - break; - } - }; - - // Encrypt video using XOR Cipher - for b in &mut frame[..] { - *b ^= CIPHER_KEY; - } - - tokio::time::sleep(sleep_time).await; - - video_track - .write_sample(&Sample { - data: frame.freeze(), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - } - - let _ = video_done_tx.try_send(()); - - Result::<()>::Ok(()) - }); - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - if connection_state == RTCIceConnectionState::Connected { - notify_tx.notify_waiters(); - } - Box::pin(async {}) - }, - )); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/offer-answer/README.md b/examples/examples/offer-answer/README.md deleted file mode 100644 index 5a95fc002..000000000 --- a/examples/examples/offer-answer/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# offer-answer - -offer-answer is an example of two webrtc-rs or pion instances communicating directly! - -The SDP offer and answer are exchanged automatically over HTTP. -The `answer` side acts like a HTTP server and should therefore be ran first. - -## Instructions - -First run `answer`: - -```shell -cargo build --example answer -./target/debug/examples/answer -``` - -Next, run `offer`: - -```shell -cargo build --example offer -./target/debug/examples/offer -``` - -You should see them connect and start to exchange messages. diff --git a/examples/examples/offer-answer/answer.rs b/examples/examples/offer-answer/answer.rs deleted file mode 100644 index 1896011ae..000000000 --- a/examples/examples/offer-answer/answer.rs +++ /dev/null @@ -1,391 +0,0 @@ -use std::io::Write; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Client, Method, Request, Response, Server, StatusCode}; -use tokio::sync::Mutex; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::data_channel::RTCDataChannel; -use webrtc::ice_transport::ice_candidate::{RTCIceCandidate, RTCIceCandidateInit}; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::peer_connection::{math_rand_alpha, RTCPeerConnection}; - -#[macro_use] -extern crate lazy_static; - -lazy_static! { - static ref PEER_CONNECTION_MUTEX: Arc>>> = - Arc::new(Mutex::new(None)); - static ref PENDING_CANDIDATES: Arc>> = Arc::new(Mutex::new(vec![])); - static ref ADDRESS: Arc> = Arc::new(Mutex::new(String::new())); -} - -async fn signal_candidate(addr: &str, c: &RTCIceCandidate) -> Result<()> { - /*println!( - "signal_candidate Post candidate to {}", - format!("http://{}/candidate", addr) - );*/ - let payload = c.to_json()?.candidate; - let req = match Request::builder() - .method(Method::POST) - .uri(format!("http://{addr}/candidate")) - .header("content-type", "application/json; charset=utf-8") - .body(Body::from(payload)) - { - Ok(req) => req, - Err(err) => { - println!("{err}"); - return Err(err.into()); - } - }; - - let _resp = match Client::new().request(req).await { - Ok(resp) => resp, - Err(err) => { - println!("{err}"); - return Err(err.into()); - } - }; - //println!("signal_candidate Response: {}", resp.status()); - - Ok(()) -} - -// HTTP Listener to get ICE Credentials/Candidate from remote Peer -async fn remote_handler(req: Request) -> Result, hyper::Error> { - let pc = { - let pcm = PEER_CONNECTION_MUTEX.lock().await; - pcm.clone().unwrap() - }; - let addr = { - let addr = ADDRESS.lock().await; - addr.clone() - }; - - match (req.method(), req.uri().path()) { - // A HTTP handler that allows the other WebRTC-rs or Pion instance to send us ICE candidates - // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN - // candidates which may be slower - (&Method::POST, "/candidate") => { - //println!("remote_handler receive from /candidate"); - let candidate = - match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc - .add_ice_candidate(RTCIceCandidateInit { - candidate, - ..Default::default() - }) - .await - { - panic!("{}", err); - } - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - - // A HTTP handler that processes a SessionDescription given to us from the other WebRTC-rs or Pion process - (&Method::POST, "/sdp") => { - //println!("remote_handler receive from /sdp"); - let sdp_str = match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) - { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let sdp = match serde_json::from_str::(&sdp_str) { - Ok(s) => s, - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc.set_remote_description(sdp).await { - panic!("{}", err); - } - - // Create an answer to send to the other process - let answer = match pc.create_answer(None).await { - Ok(a) => a, - Err(err) => panic!("{}", err), - }; - - /*println!( - "remote_handler Post answer to {}", - format!("http://{}/sdp", addr) - );*/ - - // Send our answer to the HTTP server listening in the other process - let payload = match serde_json::to_string(&answer) { - Ok(p) => p, - Err(err) => panic!("{}", err), - }; - - let req = match Request::builder() - .method(Method::POST) - .uri(format!("http://{addr}/sdp")) - .header("content-type", "application/json; charset=utf-8") - .body(Body::from(payload)) - { - Ok(req) => req, - Err(err) => panic!("{}", err), - }; - - let _resp = match Client::new().request(req).await { - Ok(resp) => resp, - Err(err) => { - println!("{err}"); - return Err(err); - } - }; - //println!("remote_handler Response: {}", resp.status()); - - // Sets the LocalDescription, and starts our UDP listeners - if let Err(err) = pc.set_local_description(answer).await { - panic!("{}", err); - } - - { - let cs = PENDING_CANDIDATES.lock().await; - for c in &*cs { - if let Err(err) = signal_candidate(&addr, c).await { - panic!("{}", err); - } - } - } - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("Answer") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of WebRTC-rs Answer.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("offer-address") - .takes_value(true) - .default_value("localhost:50000") - .long("offer-address") - .help("Address that the Offer HTTP server is hosted on."), - ) - .arg( - Arg::new("answer-address") - .takes_value(true) - .default_value("0.0.0.0:60000") - .long("answer-address") - .help("Address that the Answer HTTP server is hosted on."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let offer_addr = matches.value_of("offer-address").unwrap().to_owned(); - let answer_addr = matches.value_of("answer-address").unwrap().to_owned(); - - { - let mut oa = ADDRESS.lock().await; - oa.clone_from(&offer_addr); - } - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // When an ICE candidate is available send to the other Pion instance - // the other Pion instance will add this candidate by calling AddICECandidate - let pc = Arc::downgrade(&peer_connection); - let pending_candidates2 = Arc::clone(&PENDING_CANDIDATES); - let addr2 = offer_addr.clone(); - peer_connection.on_ice_candidate(Box::new(move |c: Option| { - //println!("on_ice_candidate {:?}", c); - - let pc2 = pc.clone(); - let pending_candidates3 = Arc::clone(&pending_candidates2); - let addr3 = addr2.clone(); - Box::pin(async move { - if let Some(c) = c { - if let Some(pc) = pc2.upgrade() { - let desc = pc.remote_description().await; - if desc.is_none() { - let mut cs = pending_candidates3.lock().await; - cs.push(c); - } else if let Err(err) = signal_candidate(&addr3, &c).await { - panic!("{}", err); - } - } - } - }) - })); - - println!("Listening on http://{answer_addr}"); - { - let mut pcm = PEER_CONNECTION_MUTEX.lock().await; - *pcm = Some(Arc::clone(&peer_connection)); - } - - tokio::spawn(async move { - let addr = SocketAddr::from_str(&answer_addr).unwrap(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - // Run this server for... forever! - if let Err(e) = server.await { - eprintln!("server error: {e}"); - } - }); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register data channel creation handling - peer_connection.on_data_channel(Box::new(move |d: Arc| { - let d_label = d.label().to_owned(); - let d_id = d.id(); - println!("New DataChannel {d_label} {d_id}"); - - Box::pin(async move{ - // Register channel opening handling - let d2 = Arc::clone(&d); - let d_label2 = d_label.clone(); - let d_id2 = d_id; - d.on_open(Box::new(move || { - println!("Data channel '{d_label2}'-'{d_id2}' open. Random messages will now be sent to any connected DataChannels every 5 seconds"); - Box::pin(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d2.send_text(message).await.map_err(Into::into); - } - }; - } - }) - })); - - // Register text message handling - d.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async{}) - })); - }) - })); - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/offer-answer/offer.rs b/examples/examples/offer-answer/offer.rs deleted file mode 100644 index 7755bca57..000000000 --- a/examples/examples/offer-answer/offer.rs +++ /dev/null @@ -1,377 +0,0 @@ -use std::io::Write; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Client, Method, Request, Response, Server, StatusCode}; -use tokio::sync::Mutex; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::MediaEngine; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::ice_transport::ice_candidate::{RTCIceCandidate, RTCIceCandidateInit}; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::peer_connection::{math_rand_alpha, RTCPeerConnection}; - -#[macro_use] -extern crate lazy_static; - -lazy_static! { - static ref PEER_CONNECTION_MUTEX: Arc>>> = - Arc::new(Mutex::new(None)); - static ref PENDING_CANDIDATES: Arc>> = Arc::new(Mutex::new(vec![])); - static ref ADDRESS: Arc> = Arc::new(Mutex::new(String::new())); -} - -async fn signal_candidate(addr: &str, c: &RTCIceCandidate) -> Result<()> { - /*println!( - "signal_candidate Post candidate to {}", - format!("http://{}/candidate", addr) - );*/ - let payload = c.to_json()?.candidate; - let req = match Request::builder() - .method(Method::POST) - .uri(format!("http://{addr}/candidate")) - .header("content-type", "application/json; charset=utf-8") - .body(Body::from(payload)) - { - Ok(req) => req, - Err(err) => { - println!("{err}"); - return Err(err.into()); - } - }; - - let _resp = match Client::new().request(req).await { - Ok(resp) => resp, - Err(err) => { - println!("{err}"); - return Err(err.into()); - } - }; - //println!("signal_candidate Response: {}", resp.status()); - - Ok(()) -} - -// HTTP Listener to get ICE Credentials/Candidate from remote Peer -async fn remote_handler(req: Request) -> Result, hyper::Error> { - let pc = { - let pcm = PEER_CONNECTION_MUTEX.lock().await; - pcm.clone().unwrap() - }; - let addr = { - let addr = ADDRESS.lock().await; - addr.clone() - }; - - match (req.method(), req.uri().path()) { - // A HTTP handler that allows the other WebRTC-rs or Pion instance to send us ICE candidates - // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN - // candidates which may be slower - (&Method::POST, "/candidate") => { - //println!("remote_handler receive from /candidate"); - let candidate = - match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc - .add_ice_candidate(RTCIceCandidateInit { - candidate, - ..Default::default() - }) - .await - { - panic!("{}", err); - } - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - - // A HTTP handler that processes a SessionDescription given to us from the other WebRTC-rs or Pion process - (&Method::POST, "/sdp") => { - //println!("remote_handler receive from /sdp"); - let sdp_str = match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) - { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let sdp = match serde_json::from_str::(&sdp_str) { - Ok(s) => s, - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc.set_remote_description(sdp).await { - panic!("{}", err); - } - - { - let cs = PENDING_CANDIDATES.lock().await; - for c in &*cs { - if let Err(err) = signal_candidate(&addr, c).await { - panic!("{}", err); - } - } - } - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("Offer") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of WebRTC-rs Offer.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("offer-address") - .takes_value(true) - .default_value("0.0.0.0:50000") - .long("offer-address") - .help("Address that the Offer HTTP server is hosted on."), - ) - .arg( - Arg::new("answer-address") - .takes_value(true) - .default_value("localhost:60000") - .long("answer-address") - .help("Address that the Answer HTTP server is hosted on."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let offer_addr = matches.value_of("offer-address").unwrap().to_owned(); - let answer_addr = matches.value_of("answer-address").unwrap().to_owned(); - - { - let mut oa = ADDRESS.lock().await; - oa.clone_from(&answer_addr); - } - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // When an ICE candidate is available send to the other Pion instance - // the other Pion instance will add this candidate by calling AddICECandidate - let pc = Arc::downgrade(&peer_connection); - let pending_candidates2 = Arc::clone(&PENDING_CANDIDATES); - let addr2 = answer_addr.clone(); - peer_connection.on_ice_candidate(Box::new(move |c: Option| { - //println!("on_ice_candidate {:?}", c); - - let pc2 = pc.clone(); - let pending_candidates3 = Arc::clone(&pending_candidates2); - let addr3 = addr2.clone(); - Box::pin(async move { - if let Some(c) = c { - if let Some(pc) = pc2.upgrade() { - let desc = pc.remote_description().await; - if desc.is_none() { - let mut cs = pending_candidates3.lock().await; - cs.push(c); - } else if let Err(err) = signal_candidate(&addr3, &c).await { - panic!("{}", err); - } - } - } - }) - })); - - println!("Listening on http://{offer_addr}"); - { - let mut pcm = PEER_CONNECTION_MUTEX.lock().await; - *pcm = Some(Arc::clone(&peer_connection)); - } - - tokio::spawn(async move { - let addr = SocketAddr::from_str(&offer_addr).unwrap(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - // Run this server for... forever! - if let Err(e) = server.await { - eprintln!("server error: {e}"); - } - }); - - // Create a datachannel with label 'data' - let data_channel = peer_connection.create_data_channel("data", None).await?; - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Register channel opening handling - let d1 = Arc::clone(&data_channel); - data_channel.on_open(Box::new(move || { - println!("Data channel '{}'-'{}' open. Random messages will now be sent to any connected DataChannels every 5 seconds", d1.label(), d1.id()); - - let d2 = Arc::clone(&d1); - Box::pin(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d2.send_text(message).await.map_err(Into::into); - } - }; - } - }) - })); - - // Register text message handling - let d_label = data_channel.label().to_owned(); - data_channel.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - - // Create an offer to send to the other process - let offer = peer_connection.create_offer(None).await?; - - // Send our offer to the HTTP server listening in the other process - let payload = match serde_json::to_string(&offer) { - Ok(p) => p, - Err(err) => panic!("{}", err), - }; - - // Sets the LocalDescription, and starts our UDP listeners - // Note: this will start the gathering of ICE candidates - peer_connection.set_local_description(offer).await?; - - //println!("Post: {}", format!("http://{}/sdp", answer_addr)); - let req = match Request::builder() - .method(Method::POST) - .uri(format!("http://{answer_addr}/sdp")) - .header("content-type", "application/json; charset=utf-8") - .body(Body::from(payload)) - { - Ok(req) => req, - Err(err) => panic!("{}", err), - }; - - let _resp = match Client::new().request(req).await { - Ok(resp) => resp, - Err(err) => { - println!("{err}"); - return Err(err.into()); - } - }; - //println!("Response: {}", resp.status()); - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/ortc/README.md b/examples/examples/ortc/README.md deleted file mode 100644 index 06237de67..000000000 --- a/examples/examples/ortc/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# ortc - -ortc demonstrates WebRTC.rs's [ORTC](https://ortc.org/) capabilities. Instead of using the Session Description Protocol -to configure and communicate ORTC provides APIs. Users then can implement signaling with whatever protocol they wish. -ORTC can then be used to implement WebRTC. A ORTC implementation can parse/emit Session Description and act as a WebRTC -implementation. - -In this example we have defined a simple JSON based signaling protocol. - -## Instructions - -### Build ortc - -```shell -cargo build --example ortc -``` - -### Run first client as offerer - -`ortc --offer` this will emit a base64 message. Copy this message to your clipboard. - -## Run the second client as answerer - -Run the second client. This should be launched with the message you copied in the previous step as stdin. - -`echo BASE64_MESSAGE_YOU_COPIED | ortc` - -### Enjoy - -If everything worked you will see `Data channel 'Foo'-'' open.` in each terminal. - -Each client will send random messages every 5 seconds that will appear in the terminal - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/ortc/ortc.rs b/examples/examples/ortc/ortc.rs deleted file mode 100644 index f4d2fa439..000000000 --- a/examples/examples/ortc/ortc.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use serde::{Deserialize, Serialize}; -use tokio::sync::Notify; -use tokio::time::Duration; -use webrtc::api::APIBuilder; -use webrtc::data_channel::data_channel_message::DataChannelMessage; -use webrtc::data_channel::data_channel_parameters::DataChannelParameters; -use webrtc::data_channel::RTCDataChannel; -use webrtc::dtls_transport::dtls_parameters::DTLSParameters; -use webrtc::ice_transport::ice_candidate::RTCIceCandidate; -use webrtc::ice_transport::ice_gatherer::RTCIceGatherOptions; -use webrtc::ice_transport::ice_parameters::RTCIceParameters; -use webrtc::ice_transport::ice_role::RTCIceRole; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::peer_connection::math_rand_alpha; -use webrtc::sctp_transport::sctp_transport_capabilities::SCTPTransportCapabilities; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("ortc") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of ORTC.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("offer") - .long("offer") - .help("Act as the offerer if set."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let is_offer = matches.is_present("offer"); - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the Pion WebRTC (ORTC) API! Thanks for using it ❤️. - - // Prepare ICE gathering options - let ice_options = RTCIceGatherOptions { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create an API object - let api = APIBuilder::new().build(); - - // Create the ICE gatherer - let gatherer = Arc::new(api.new_ice_gatherer(ice_options)?); - - // Construct the ICE transport - let ice = Arc::new(api.new_ice_transport(Arc::clone(&gatherer))); - - // Construct the DTLS transport - let dtls = Arc::new(api.new_dtls_transport(Arc::clone(&ice), vec![])?); - - // Construct the SCTP transport - let sctp = Arc::new(api.new_sctp_transport(Arc::clone(&dtls))?); - - let done = Arc::new(Notify::new()); - let done_answer = done.clone(); - let done_offer = done.clone(); - - // Handle incoming data channels - sctp.on_data_channel(Box::new(move |d: Arc| { - let d_label = d.label().to_owned(); - let d_id = d.id(); - println!("New DataChannel {d_label} {d_id}"); - - let done_answer1 = done_answer.clone(); - // Register the handlers - Box::pin(async move { - // no need to downgrade this to Weak, since on_open is FnOnce callback - let d2 = Arc::clone(&d); - let done_answer2 = done_answer1.clone(); - d.on_open(Box::new(move || { - Box::pin(async move { - tokio::select! { - _ = done_answer2.notified() => { - println!("received done_answer signal!"); - } - _ = handle_on_open(d2) => {} - }; - - println!("exit data answer"); - }) - })); - - // Register text message handling - d.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - }) - })); - - let (gather_finished_tx, mut gather_finished_rx) = tokio::sync::mpsc::channel::<()>(1); - let mut gather_finished_tx = Some(gather_finished_tx); - gatherer.on_local_candidate(Box::new(move |c: Option| { - if c.is_none() { - gather_finished_tx.take(); - } - Box::pin(async {}) - })); - - // Gather candidates - gatherer.gather().await?; - - let _ = gather_finished_rx.recv().await; - - let ice_candidates = gatherer.get_local_candidates().await?; - - let ice_parameters = gatherer.get_local_parameters().await?; - - let dtls_parameters = dtls.get_local_parameters()?; - - let sctp_capabilities = sctp.get_capabilities(); - - let local_signal = Signal { - ice_candidates, - ice_parameters, - dtls_parameters, - sctp_capabilities, - }; - - // Exchange the information - let json_str = serde_json::to_string(&local_signal)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - - let line = signal::must_read_stdin()?; - let json_str = signal::decode(line.as_str())?; - let remote_signal = serde_json::from_str::(&json_str)?; - - let ice_role = if is_offer { - RTCIceRole::Controlling - } else { - RTCIceRole::Controlled - }; - - ice.set_remote_candidates(&remote_signal.ice_candidates) - .await?; - - // Start the ICE transport - ice.start(&remote_signal.ice_parameters, Some(ice_role)) - .await?; - - // Start the DTLS transport - dtls.start(remote_signal.dtls_parameters).await?; - - // Start the SCTP transport - sctp.start(remote_signal.sctp_capabilities).await?; - - // Construct the data channel as the offerer - if is_offer { - let id = 1u16; - - let dc_params = DataChannelParameters { - label: "Foo".to_owned(), - negotiated: Some(id), - ..Default::default() - }; - - let d = Arc::new(api.new_data_channel(Arc::clone(&sctp), dc_params).await?); - - // Register the handlers - // channel.OnOpen(handleOnOpen(channel)) // TODO: OnOpen on handle ChannelAck - // Temporary alternative - - // no need to downgrade this to Weak - let d2 = Arc::clone(&d); - tokio::spawn(async move { - tokio::select! { - _ = done_offer.notified() => { - println!("received done_offer signal!"); - } - _ = handle_on_open(d2) => {} - }; - - println!("exit data offer"); - }); - - let d_label = d.label().to_owned(); - d.on_message(Box::new(move |msg: DataChannelMessage| { - let msg_str = String::from_utf8(msg.data.to_vec()).unwrap(); - println!("Message from DataChannel '{d_label}': '{msg_str}'"); - Box::pin(async {}) - })); - } - - println!("Press ctrl-c to stop"); - tokio::signal::ctrl_c().await.unwrap(); - done.notify_waiters(); - - sctp.stop().await?; - dtls.stop().await?; - ice.stop().await?; - - Ok(()) -} - -// Signal is used to exchange signaling info. -// This is not part of the ORTC spec. You are free -// to exchange this information any way you want. -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Signal { - #[serde(rename = "iceCandidates")] - ice_candidates: Vec, // `json:"iceCandidates"` - - #[serde(rename = "iceParameters")] - ice_parameters: RTCIceParameters, // `json:"iceParameters"` - - #[serde(rename = "dtlsParameters")] - dtls_parameters: DTLSParameters, // `json:"dtlsParameters"` - - #[serde(rename = "sctpCapabilities")] - sctp_capabilities: SCTPTransportCapabilities, // `json:"sctpCapabilities"` -} - -async fn handle_on_open(d: Arc) -> Result<()> { - println!("Data channel '{}'-'{}' open. Random messages will now be sent to any connected DataChannels every 5 seconds", d.label(), d.id()); - - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - let message = math_rand_alpha(15); - println!("Sending '{message}'"); - result = d.send_text(message).await.map_err(Into::into); - } - }; - } - - Ok(()) -} diff --git a/examples/examples/play-from-disk-h264/README.md b/examples/examples/play-from-disk-h264/README.md deleted file mode 100644 index fa8e53fd6..000000000 --- a/examples/examples/play-from-disk-h264/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# play-from-disk-h264 - -play-from-disk-h264 demonstrates how to send h264 video and/or audio to your browser from files saved to disk. - -## Instructions - -### Create IVF named `output.264` that contains a H264 track and/or `output.ogg` that contains a Opus track - -```shell -ffmpeg -i $INPUT_FILE -an -c:v libx264 -bsf:v h264_mp4toannexb -b:v 2M -max_delay 0 -bf 0 output.h264 -ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn output.ogg -``` - -### Build play-from-disk-h264 - -```shell -cargo build --example play-from-disk-h264 -``` - -### Open play-from-disk-h264 example page - -[jsfiddle.net](https://jsfiddle.net/9s10amwL/) you should see two text-areas and a 'Start Session' button - -### Run play-from-disk-h264 with your browsers SessionDescription as stdin - -The `output.h264` you created should be in the same directory as `play-from-disk-h264`. In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-h264 -v examples/test-data/output.h264 -a examples/test-data/output.ogg` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/play-from-disk-h264 -v examples/test-data/output.h264 -a examples/test-data/output.ogg < my_file` - -### Input play-from-disk-h264's SessionDescription into your browser - -Copy the text that `play-from-disk-h264` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -A video should start playing in your browser above the input boxes. `play-from-disk-h264` will exit when the file reaches the end - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/play-from-disk-h264/play-from-disk-h264.rs b/examples/examples/play-from-disk-h264/play-from-disk-h264.rs deleted file mode 100644 index 94e770185..000000000 --- a/examples/examples/play-from-disk-h264/play-from-disk-h264.rs +++ /dev/null @@ -1,361 +0,0 @@ -use std::fs::File; -use std::io::{BufReader, Write}; -use std::path::Path; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::Notify; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_H264, MIME_TYPE_OPUS}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::h264_reader::H264Reader; -use webrtc::media::io::ogg_reader::OggReader; -use webrtc::media::Sample; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use webrtc::track::track_local::TrackLocal; -use webrtc::Error; - -const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("play-from-disk-h264") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of play-from-disk-h264.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ) - .arg( - Arg::new("audio") - .takes_value(true) - .short('a') - .long("audio") - .help("Audio file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let video_file = matches.value_of("video"); - let audio_file = matches.value_of("audio"); - - if let Some(video_path) = &video_file { - if !Path::new(video_path).exists() { - return Err(Error::new(format!("video file: '{video_path}' not exist")).into()); - } - } - if let Some(audio_path) = &audio_file { - if !Path::new(audio_path).exists() { - return Err(Error::new(format!("audio file: '{audio_path}' not exist")).into()); - } - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let notify_tx = Arc::new(Notify::new()); - let notify_video = notify_tx.clone(); - let notify_audio = notify_tx.clone(); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let video_done_tx = done_tx.clone(); - let audio_done_tx = done_tx.clone(); - - if let Some(video_file) = video_file { - // Create a video track - let video_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&video_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let video_file_name = video_file.to_owned(); - tokio::spawn(async move { - // Open a H264 file and start reading using our H264Reader - let file = File::open(&video_file_name)?; - let reader = BufReader::new(file); - let mut h264 = H264Reader::new(reader, 1_048_576); - - // Wait for connection established - notify_video.notified().await; - - println!("play video from disk file {video_file_name}"); - - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - let mut ticker = tokio::time::interval(Duration::from_millis(33)); - loop { - let nal = match h264.next_nal() { - Ok(nal) => nal, - Err(err) => { - println!("All video frames parsed and sent: {err}"); - break; - } - }; - - /*println!( - "PictureOrderCount={}, ForbiddenZeroBit={}, RefIdc={}, UnitType={}, data={}", - nal.picture_order_count, - nal.forbidden_zero_bit, - nal.ref_idc, - nal.unit_type, - nal.data.len() - );*/ - - video_track - .write_sample(&Sample { - data: nal.data.freeze(), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - - let _ = ticker.tick().await; - } - - let _ = video_done_tx.try_send(()); - - Result::<()>::Ok(()) - }); - } - - if let Some(audio_file) = audio_file { - // Create a audio track - let audio_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - "audio".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&audio_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let audio_file_name = audio_file.to_owned(); - tokio::spawn(async move { - // Open a IVF file and start reading using our IVFReader - let file = File::open(audio_file_name)?; - let reader = BufReader::new(file); - // Open on oggfile in non-checksum mode. - let (mut ogg, _) = OggReader::new(reader, true)?; - - // Wait for connection established - notify_audio.notified().await; - - println!("play audio from disk file output.ogg"); - - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - let mut ticker = tokio::time::interval(OGG_PAGE_DURATION); - - // Keep track of last granule, the difference is the amount of samples in the buffer - let mut last_granule: u64 = 0; - while let Ok((page_data, page_header)) = ogg.parse_next_page() { - // The amount of samples is the difference between the last and current timestamp - let sample_count = page_header.granule_position - last_granule; - last_granule = page_header.granule_position; - let sample_duration = Duration::from_millis(sample_count * 1000 / 48000); - - audio_track - .write_sample(&Sample { - data: page_data.freeze(), - duration: sample_duration, - ..Default::default() - }) - .await?; - - let _ = ticker.tick().await; - } - - let _ = audio_done_tx.try_send(()); - - Result::<()>::Ok(()) - }); - } - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - if connection_state == RTCIceConnectionState::Connected { - notify_tx.notify_waiters(); - } - Box::pin(async {}) - }, - )); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/play-from-disk-hevc/README.md b/examples/examples/play-from-disk-hevc/README.md deleted file mode 100644 index 16fa2cad5..000000000 --- a/examples/examples/play-from-disk-hevc/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# play-from-disk-hevc - -play-from-disk-hevc demonstrates how to send hevc video and/or audio to your browser from files saved to disk. - -## Instructions - -### Create IVF named `output.265` that contains a hevc track and/or `output.ogg` that contains a Opus track - -```shell -ffmpeg -i $INPUT_FILE -an -c:v libx265 -bsf:v hevc_mp4toannexb -b:v 2M -max_delay 0 -bf 0 output.265 -ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn output.ogg -``` - -### Build/Run play-from-disk-hevc - -```shell -cargo run --example play-from-disk-hevc -``` - -### Result and Output -In the shell you opened, you should see from std that rtp of hevc get received and parsed - -After all is done, an `xx.output` file should be created at the same directory of the src video file - -Congrats, you have sent and received the hevc stream - -## Notes -- Maybe you will need to install libx265/opus for your ffmepg -- Please update the stun server to the best match, google maybe slow/unaccessable in some certain region/circumstance diff --git a/examples/examples/play-from-disk-hevc/play-from-disk-hevc.rs b/examples/examples/play-from-disk-hevc/play-from-disk-hevc.rs deleted file mode 100644 index c2174e211..000000000 --- a/examples/examples/play-from-disk-hevc/play-from-disk-hevc.rs +++ /dev/null @@ -1,599 +0,0 @@ -use anyhow::Result; -use bytes::BytesMut; -use clap::{AppSettings, Arg, Command}; -use std::fs::File; -use std::io::{BufReader, Read, Write}; -use std::path::Path; -use std::sync::{Arc, Weak}; -use tokio::sync::{mpsc, Notify}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_HEVC, MIME_TYPE_OPUS}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::ogg_reader::OggReader; -use webrtc::media::Sample; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::peer_connection::RTCPeerConnection; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp::codecs::h264::ANNEXB_NALUSTART_CODE; -use webrtc::rtp::codecs::h265::{H265NALUHeader, H265Packet, H265Payload, UnitType}; -use webrtc::rtp::packetizer::Depacketizer; -use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTPCodecType}; -use webrtc::rtp_transceiver::rtp_receiver::RTCRtpReceiver; -use webrtc::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use webrtc::rtp_transceiver::{RTCRtpTransceiver, RTCRtpTransceiverInit}; -use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use webrtc::track::track_local::TrackLocal; -use webrtc::track::track_remote::TrackRemote; -use webrtc::Error; - -const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("play-from-disk-hevc") - .version("0.1.0") - .author("RobinShi ") - .about("An example of play-from-disk-hevc.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ) - .arg( - Arg::new("audio") - .takes_value(true) - .short('a') - .long("audio") - .help("Audio file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let video_file = matches.value_of("video"); - let audio_file = matches.value_of("audio"); - - if let Some(video_path) = &video_file { - if !Path::new(video_path).exists() { - return Err(Error::new(format!("video file: '{video_path}' not exist")).into()); - } - } - if let Some(audio_path) = &audio_file { - if !Path::new(audio_path).exists() { - return Err(Error::new(format!("audio file: '{audio_path}' not exist")).into()); - } - } - let video_file = video_file.map(|v| v.to_owned()).unwrap(); - let audio_file = audio_file.map(|v| v.to_owned()).unwrap(); - - let video_file1 = video_file.clone(); - let (offer_sdr, mut offer_rcv) = mpsc::channel::(10); - let (answer_sdr, answer_rcv) = mpsc::channel::(10); - tokio::spawn(async move { - if let Err(e) = offer_worker(video_file1, audio_file, offer_sdr, answer_rcv).await { - println!("[Speaker] Error: {:?}", e); - } - }); - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - peer_connection - .add_transceiver_from_kind( - RTPCodecType::Audio, - Some(RTCRtpTransceiverInit { - direction: RTCRtpTransceiverDirection::Sendrecv, - send_encodings: vec![], - }), - ) - .await?; - peer_connection - .add_transceiver_from_kind( - RTPCodecType::Video, - Some(RTCRtpTransceiverInit { - direction: RTCRtpTransceiverDirection::Sendrecv, - send_encodings: vec![], - }), - ) - .await?; - let pc1 = Arc::downgrade(&peer_connection); - let close_notify = Arc::new(Notify::new()); - let notify1 = close_notify.clone(); - peer_connection.on_track(Box::new( - move |track: Arc, - _receiver: Arc, - _tranceiver: Arc| { - let media_ssrc = track.ssrc(); - let pc2 = pc1.clone(); - let kind = track.kind(); - let notify2 = notify1.clone(); - println!("[Listener] track codec {:?}", track.codec()); - if kind == RTPCodecType::Video { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(Duration::from_secs(2)); - while let Some(pc3) = pc2.upgrade() { - if peer_closed(&pc3) { - break; - } - if pc3 - .write_rtcp(&[Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc, - })]) - .await - .is_err() - { - break; - } - let _ = ticker.tick().await; - } - println!("[Listener] closing {kind} pli thread"); - }); - } - - let pc2 = pc1.clone(); - let video_file1 = video_file.clone(); - match kind { - RTPCodecType::Video => { - tokio::spawn(async move { - let mut pck = H265Packet::default(); - let mut fdata = BytesMut::new(); - loop { - let timeout = tokio::time::sleep(Duration::from_secs(4)); - tokio::pin!(timeout); - tokio::select! { - _ = timeout.as_mut() => { - break; - } - m = track.read_rtp() => { - println!("rtp readed"); - if let Ok((p, _)) = m { - let data = pck.depacketize(&p.payload).unwrap(); - match pck.payload() { - H265Payload::H265PACIPacket(p) => { - println!("[Listener] paci {:?}", p.payload_header()); - } - H265Payload::H265SingleNALUnitPacket(p) => { - println!( - "[Listener] single len {:?} type {:?}", - p.payload().len(), - p.payload_header().nalu_type() - ); - fdata.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); - fdata.extend_from_slice(&data); - } - H265Payload::H265AggregationPacket(p) => { - if let Some(uf) = p.first_unit() { - println!( - "[Listener] aggr first nal len {} type {:?}", - uf.nal_unit().len(), - UnitType::for_id((uf.nal_unit()[0] & 0b0111_1110) >> 1) - ); - fdata.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); - fdata.extend_from_slice(&uf.nal_unit()); - } - for ou in p.other_units() { - println!( - "[Listener] aggr other nal len {} type {:?}", - ou.nal_unit().len(), - UnitType::for_id((ou.nal_unit()[0] & 0b0111_1110) >> 1) - ); - fdata.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); - fdata.extend_from_slice(&ou.nal_unit()); - } - } - H265Payload::H265FragmentationUnitPacket(p) => { - println!( - "[Listener] fu nal header {:?} data4 {:?}, nal_type {:?}", - p.fu_header(), - &data[0..4], - p.fu_header().fu_type(), - ); - if p.fu_header().s() { - let nal_type = (p.fu_header().fu_type() << 1) & 0b0111_1110; - fdata.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); - fdata.extend_from_slice(&[nal_type, 0x01]); - } - fdata.extend_from_slice(&p.payload()); - if p.fu_header().e() { - println!("[Listener] fu nal collected"); - } - } - } - } else if weak_peer_closed(&pc2) { - println!("peer abnormally closed"); - break; - } - } - } - } - let mut file = std::fs::File::create(format!("{video_file1}.output")).unwrap(); - let _ = file.write_all(&fdata); - println!("[Listener] closing video read thread"); - notify2.notify_waiters(); - }); - } - RTPCodecType::Audio => { - tokio::spawn(async move { - loop { - let timeout = tokio::time::sleep(Duration::from_secs(4)); - tokio::pin!(timeout); - tokio::select! { - _ = timeout.as_mut() => { - break; - } - m = track.read_rtp() => { - if m.is_err() && weak_peer_closed(&pc2) { - break; - } - } - } - } - println!("[Listener] closing audio read thread"); - notify2.notify_waiters(); - }); - } - _ => {} - } - Box::pin(async {}) - }, - )); - let notify1 = close_notify.clone(); - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("[Listener] session state changed {connection_state}",); - if connection_state == RTCIceConnectionState::Closed - || connection_state == RTCIceConnectionState::Failed - { - notify1.notify_waiters(); - } - Box::pin(async {}) - }, - )); - - println!("[Listener] waiting for offer"); - let timeout = tokio::time::sleep(Duration::from_secs(60)); - tokio::pin!(timeout); - let offer = tokio::select! { - _ = timeout.as_mut() => {panic!("wait offer failed")} - sdp = offer_rcv.recv() => {sdp.unwrap()} - }; - peer_connection.set_remote_description(offer).await?; - let answer = peer_connection.create_answer(None).await?; - let mut gather_complete = peer_connection.gathering_complete_promise().await; - peer_connection.set_local_description(answer).await?; - let _ = gather_complete.recv().await; - - println!("[Listener] offer set, sending answer"); - if let Some(answer) = peer_connection.local_description().await { - let _ = answer_sdr.send(answer).await; - } - - println!("[Listener] answer sent, await quit event"); - let timeout = tokio::time::sleep(Duration::from_secs(60)); - tokio::pin!(timeout); - tokio::select! { - _ = timeout.as_mut() => {} - _ = close_notify.notified() => {} - } - let _ = peer_connection.close().await; - println!("[Listener] closing peer"); - - Ok(()) -} - -async fn offer_worker( - video_file: String, - audio_file: String, - offer_sdr: mpsc::Sender, - mut answer_rcv: mpsc::Receiver, -) -> Result<()> { - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let video_done_tx = done_tx.clone(); - let audio_done_tx = done_tx.clone(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - let notify_connect = Arc::new(Notify::new()); - - let local_video_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_HEVC.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let video_rtp_sender = peer_connection - .add_track(Arc::clone(&local_video_track) as Arc) - .await?; - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = video_rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - let notify1 = notify_connect.clone(); - tokio::spawn(async move { - let mut buf = vec![]; - let mut file = File::open(&video_file).unwrap(); - let _ = file.read_to_end(&mut buf); - let mut data = BytesMut::from_iter(buf); - - let list = memchr::memmem::find_iter(&data, &ANNEXB_NALUSTART_CODE); - let mut data_list = vec![]; - let mut idxs = list.into_iter().collect::>(); - idxs.reverse(); - for i in idxs { - let nal_data = data.split_off(i); - // let payload_header = H265NALUHeader::new(nal_data[4], nal_data[5]); - // let payload_nalu_type = payload_header.nalu_type(); - // let nalu_type = UnitType::for_id(payload_nalu_type).unwrap_or(UnitType::IGNORE); - data_list.insert(0, nal_data); - } - - let timeout = tokio::time::sleep(Duration::from_secs(10)); - tokio::pin!(timeout); - tokio::select! { - _ = timeout.as_mut() => {return;} - _ = notify1.notified()=> {} - }; - println!("[Speaker] play video from disk file"); - let mut ticker = tokio::time::interval(Duration::from_millis(33)); - loop { - if data_list.is_empty() { - break; - } - let nal_data = data_list.remove(0); - let payload_header = H265NALUHeader::new(nal_data[4], nal_data[5]); - let payload_nalu_type = payload_header.nalu_type(); - let nalu_type = UnitType::for_id(payload_nalu_type).unwrap_or(UnitType::IGNORE); - if let Err(e) = local_video_track - .write_sample(&Sample { - data: nal_data.freeze(), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await - { - println!("[Speaker] sending video err {e}"); - } - - if nalu_type != UnitType::VPS - || nalu_type != UnitType::SPS - || nalu_type != UnitType::PPS - || nalu_type != UnitType::SEI - { - let _ = ticker.tick().await; - } - } - let _ = video_done_tx.try_send(()); - }); - - let local_audio_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - "audio".to_owned(), - "webrtc-rs".to_owned(), - )); - let audio_rtp_sender = peer_connection - .add_track(Arc::clone(&local_audio_track) as Arc) - .await?; - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = audio_rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - let notify1 = notify_connect.clone(); - tokio::spawn(async move { - // Open a IVF file and start reading using our IVFReader - let file = File::open(&audio_file)?; - let reader = BufReader::new(file); - // Open on oggfile in non-checksum mode. - let (mut ogg, _) = OggReader::new(reader, true)?; - // Wait for connection established - notify1.notified().await; - println!("[Speaker] play audio from disk file output.ogg"); - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - let mut ticker = tokio::time::interval(OGG_PAGE_DURATION); - // Keep track of last granule, the difference is the amount of samples in the buffer - let mut last_granule: u64 = 0; - while let Ok((page_data, page_header)) = ogg.parse_next_page() { - // The amount of samples is the difference between the last and current timestamp - let sample_count = page_header.granule_position - last_granule; - last_granule = page_header.granule_position; - let sample_duration = Duration::from_millis(sample_count * 1000 / 48000); - if let Err(e) = local_audio_track - .write_sample(&Sample { - data: page_data.freeze(), - duration: sample_duration, - ..Default::default() - }) - .await - { - println!("[Speaker] sending audio err {e}"); - } - let _ = ticker.tick().await; - } - let _ = audio_done_tx.try_send(()); - Result::<()>::Ok(()) - }); - - let notify1 = notify_connect.clone(); - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("[Speaker] session state changed {connection_state}",); - if connection_state == RTCIceConnectionState::Connected { - notify1.notify_waiters(); - } - Box::pin(async {}) - }, - )); - // let pc = Arc::downgrade(&peer_connection); - // let mut candidates = Arc::new(Mutex::new(vec![])); - // let candidates1 = candidates.clone(); - // let notify_gather = Arc::new(Notify::new()); - // let notify1 = notify_gather.clone(); - // peer_connection.on_ice_candidate(Box::new(move |c: Option| { - // let pc2 = pc.clone(); - // let pending_candidates3 = Arc::clone(&pending_candidates2); - // Box::pin(async move { - // if let Some(c) = c { - // candidates1.lock().await.push(c); - // } else { - // notify1.notify_waiters(); - // } - // }) - // })); - let offer = peer_connection.create_offer(None).await?; - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(offer).await?; - let _ = gather_complete.recv().await; - - if let Some(sdp) = peer_connection.local_description().await { - let _ = offer_sdr.send(sdp).await; - } - println!("[Speaker] offer sent, waiting for answer"); - let answer = answer_rcv.recv().await.unwrap(); - peer_connection.set_remote_description(answer).await?; - println!("[Speaker] answer received, wait for quit event"); - - let timeout = tokio::time::sleep(Duration::from_secs(30)); - tokio::pin!(timeout); - tokio::select! { - _ = timeout.as_mut() => {} - _ = done_rx.recv() => {} - } - peer_connection.close().await?; - println!("[Speaker] closing peer"); - Ok(()) -} - -pub fn peer_closed(conn: &Arc) -> bool { - let state = conn.connection_state(); - state == RTCPeerConnectionState::Closed || state == RTCPeerConnectionState::Failed -} - -pub fn weak_peer_closed(conn: &Weak) -> bool { - let mut result = false; - if let Some(pc3) = conn.upgrade() { - if peer_closed(&pc3) { - result = true; - } - } else { - result = true - } - result -} - -// #[derive(Clone, Debug)] -// pub struct Nal { -// pub type_: UnitType, -// pub data: Vec, -// } - -// impl Nal { -// pub fn new(data: Vec) -> Result { -// Ok(Self { -// type_: Self::nal_unit_type(&data)?, -// data, -// }) -// } -// pub fn nal_unit_type(data: &[u8]) -> Result { -// UnitType::for_id((data[0] & 0b0111_1110) >> 1) -// } -// } diff --git a/examples/examples/play-from-disk-renegotiation/README.md b/examples/examples/play-from-disk-renegotiation/README.md deleted file mode 100644 index c18fb3d48..000000000 --- a/examples/examples/play-from-disk-renegotiation/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# play-from-disk-renegotiation - -play-from-disk-renegotiation demonstrates WebRTC.rs's renegotiation abilities. - -For a simpler example of playing a file from disk we also have [examples/play-from-disk](/examples/play-from-disk) - -## Instructions - -### Build play-from-disk-renegotiation - -```shell -cargo build --example play-from-disk-renegotiation -``` - -### Create IVF named `output.ivf` that contains a VP8 track - -```shell -ffmpeg -i $INPUT_FILE -g 30 output.ivf -``` - -### Run play-from-disk-renegotiation - -The `output.ivf` you created should be in the same directory as `play-from-disk-renegotiation`. - - -### Open the Web UI - -Open [http://localhost:8080](http://localhost:8080) and you should have a `Add Track` and `Remove Track` button. Press these to add as many tracks as you want, or to remove as many as you wish. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/play-from-disk-renegotiation/index.html b/examples/examples/play-from-disk-renegotiation/index.html deleted file mode 100644 index 77130feaf..000000000 --- a/examples/examples/play-from-disk-renegotiation/index.html +++ /dev/null @@ -1,80 +0,0 @@ - - - play-from-disk-renegotiation - - - - -
- -
- - -

Video

-
-
- -

Logs

-
- - - - diff --git a/examples/examples/play-from-disk-renegotiation/play-from-disk-renegotiation.rs b/examples/examples/play-from-disk-renegotiation/play-from-disk-renegotiation.rs deleted file mode 100644 index 503cd863d..000000000 --- a/examples/examples/play-from-disk-renegotiation/play-from-disk-renegotiation.rs +++ /dev/null @@ -1,410 +0,0 @@ -use std::fs::File; -use std::io::{BufReader, Write}; -use std::net::SocketAddr; -use std::path::Path; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use tokio::sync::Mutex; -use tokio::time::Duration; -use tokio_util::codec::{BytesCodec, FramedRead}; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::ivf_reader::IVFReader; -use webrtc::media::Sample; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::peer_connection::RTCPeerConnection; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use webrtc::track::track_local::TrackLocal; -use webrtc::Error; - -#[macro_use] -extern crate lazy_static; - -lazy_static! { - static ref PEER_CONNECTION_MUTEX: Arc>>> = - Arc::new(Mutex::new(None)); - static ref VIDEO_FILE: Arc>> = Arc::new(Mutex::new(None)); -} - -static INDEX: &str = "examples/examples/play-from-disk-renegotiation/index.html"; -static NOTFOUND: &[u8] = b"Not Found"; - -/// HTTP status code 404 -fn not_found() -> Response { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(NOTFOUND.into()) - .unwrap() -} - -async fn simple_file_send(filename: &str) -> Result, hyper::Error> { - // Serve a file by asynchronously reading it by chunks using tokio-util crate. - - if let Ok(file) = tokio::fs::File::open(filename).await { - let stream = FramedRead::new(file, BytesCodec::new()); - let body = Body::wrap_stream(stream); - return Ok(Response::new(body)); - } - - Ok(not_found()) -} - -// HTTP Listener to get ICE Credentials/Candidate from remote Peer -async fn remote_handler(req: Request) -> Result, hyper::Error> { - let pc = { - let pcm = PEER_CONNECTION_MUTEX.lock().await; - pcm.clone().unwrap() - }; - - match (req.method(), req.uri().path()) { - (&Method::GET, "/") | (&Method::GET, "/index.html") => simple_file_send(INDEX).await, - - (&Method::POST, "/createPeerConnection") => create_peer_connection(&pc, req).await, - - (&Method::POST, "/addVideo") => add_video(&pc, req).await, - - (&Method::POST, "/removeVideo") => remove_video(&pc, req).await, - - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -// do_signaling exchanges all state of the local PeerConnection and is called -// every time a video is added or removed -async fn do_signaling( - pc: &Arc, - req: Request, -) -> Result, hyper::Error> { - let sdp_str = match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let offer = match serde_json::from_str::(&sdp_str) { - Ok(s) => s, - Err(err) => panic!("{}", err), - }; - - if let Err(err) = pc.set_remote_description(offer).await { - panic!("{}", err); - } - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = pc.gathering_complete_promise().await; - - // Create an answer - let answer = match pc.create_answer(None).await { - Ok(answer) => answer, - Err(err) => panic!("{}", err), - }; - - // Sets the LocalDescription, and starts our UDP listeners - if let Err(err) = pc.set_local_description(answer).await { - panic!("{}", err); - } - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - let payload = if let Some(local_desc) = pc.local_description().await { - match serde_json::to_string(&local_desc) { - Ok(p) => p, - Err(err) => panic!("{}", err), - } - } else { - panic!("generate local_description failed!"); - }; - - let mut response = match Response::builder() - .header("content-type", "application/json") - .body(Body::from(payload)) - { - Ok(res) => res, - Err(err) => panic!("{}", err), - }; - - *response.status_mut() = StatusCode::OK; - Ok(response) -} - -// Add a single video track -async fn create_peer_connection( - pc: &Arc, - r: Request, -) -> Result, hyper::Error> { - if pc.connection_state() != RTCPeerConnectionState::New { - panic!( - "create_peer_connection called in non-new state ({})", - pc.connection_state() - ); - } - - println!("PeerConnection has been created"); - do_signaling(pc, r).await -} - -// Add a single video track -async fn add_video( - pc: &Arc, - r: Request, -) -> Result, hyper::Error> { - let video_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - format!("video-{}", rand::random::()), - format!("video-{}", rand::random::()), - )); - - let rtp_sender = match pc - .add_track(Arc::clone(&video_track) as Arc) - .await - { - Ok(rtp_sender) => rtp_sender, - Err(err) => panic!("{}", err), - }; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let video_file = { - let vf = VIDEO_FILE.lock().await; - vf.clone() - }; - - if let Some(video_file) = video_file { - tokio::spawn(async move { - let _ = write_video_to_track(video_file, video_track).await; - }); - } - - println!("Video track has been added"); - do_signaling(pc, r).await -} - -// Remove a single sender -async fn remove_video( - pc: &Arc, - r: Request, -) -> Result, hyper::Error> { - let senders = pc.get_senders().await; - if !senders.is_empty() { - if let Err(err) = pc.remove_track(&senders[0]).await { - panic!("{}", err); - } - } - - println!("Video track has been removed"); - do_signaling(pc, r).await -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("play-from-disk-renegotiation") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of play-from-disk-renegotiation.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let video_file = matches.value_of("video"); - - if let Some(video_file) = video_file { - if !Path::new(video_file).exists() { - return Err(Error::new(format!("video file: '{video_file}' not exist")).into()); - } - let mut vf = VIDEO_FILE.lock().await; - *vf = Some(video_file.to_owned()); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - { - let mut pcm = PEER_CONNECTION_MUTEX.lock().await; - *pcm = Some(Arc::clone(&peer_connection)); - } - - tokio::spawn(async move { - println!("Open http://localhost:8080 to access this demo"); - - let addr = SocketAddr::from_str("0.0.0.0:8080").unwrap(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - // Run this server for... forever! - if let Err(e) = server.await { - eprintln!("server error: {e}"); - } - }); - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} - -// Read a video file from disk and write it to a webrtc.Track -// When the video has been completely read this exits without error -async fn write_video_to_track(video_file: String, t: Arc) -> Result<()> { - println!("play video from disk file {video_file}"); - - // Open a IVF file and start reading using our IVFReader - let file = File::open(video_file)?; - let reader = BufReader::new(file); - let (mut ivf, header) = IVFReader::new(reader)?; - - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. - // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. - let sleep_time = Duration::from_millis( - ((1000 * header.timebase_numerator) / header.timebase_denominator) as u64, - ); - let mut ticker = tokio::time::interval(sleep_time); - loop { - let frame = match ivf.parse_next_frame() { - Ok((frame, _)) => frame, - Err(err) => { - println!("All video frames parsed and sent: {err}"); - return Err(err.into()); - } - }; - - t.write_sample(&Sample { - data: frame.freeze(), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - - let _ = ticker.tick().await; - } -} diff --git a/examples/examples/play-from-disk-vpx/README.md b/examples/examples/play-from-disk-vpx/README.md deleted file mode 100644 index 05bc968f8..000000000 --- a/examples/examples/play-from-disk-vpx/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# play-from-disk-vpx - -play-from-disk-vpx demonstrates how to send vp8/vp8 video and/or audio to your browser from files saved to disk. - -## Instructions - -### Create IVF named `output_vp8.ivf` or `output_vp9.ivf` that contains a VP8/VP9 track and/or `output.ogg` that contains a Opus track - -```shell -ffmpeg -i $INPUT_FILE -g 30 output_vp8.ivf -ffmpeg -i $INPUT_FILE -g 30 -c libvpx-vp9 output_vp9.ivf -ffmpeg -i $INPUT_FILE -map 0:a -c:a dca -ac 2 -c:a libopus -page_duration 20000 -vn output.ogg -``` - -### Build play-from-disk-vpx - -```shell -cargo build --example play-from-disk-vpx -``` - -### Open play-from-disk-vpx example page - -[jsfiddle.net](https://jsfiddle.net/9s10amwL/) you should see two text-areas and a 'Start Session' button - -### Run play-from-disk-vpx with your browsers SessionDescription as stdin - -The `output_vp8.ivf`/`output_vp9.ivf` you created should be in the same directory as `play-from-disk-vpx`. In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -1. Run `echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v examples/test-data/output_vp8.ivf -a examples/test-data/output.ogg` -2. Run `echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v examples/test-data/output_vp9.ivf -a examples/test-data/output.ogg --vp9` - -#### Windows - -1. Paste the SessionDescription into a file. -2. Run `./target/debug/examples/play-from-disk-vpx -v examples/test-data/output_vp8.ivf -a examples/test-data/output.ogg < my_file` -3. Run `./target/debug/examples/play-from-disk-vpx -v examples/test-data/output_vp9.ivf -a examples/test-data/output.ogg --vp9 < my_file` - -### Input play-from-disk-vpx's SessionDescription into your browser - -Copy the text that `play-from-disk-vpx` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -A video should start playing in your browser above the input boxes. `play-from-disk-vpx` will exit when the file reaches the end - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/play-from-disk-vpx/play-from-disk-vpx.rs b/examples/examples/play-from-disk-vpx/play-from-disk-vpx.rs deleted file mode 100644 index d3a578dc8..000000000 --- a/examples/examples/play-from-disk-vpx/play-from-disk-vpx.rs +++ /dev/null @@ -1,372 +0,0 @@ -use std::fs::File; -use std::io::{BufReader, Write}; -use std::path::Path; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::Notify; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_OPUS, MIME_TYPE_VP8, MIME_TYPE_VP9}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::ivf_reader::IVFReader; -use webrtc::media::io::ogg_reader::OggReader; -use webrtc::media::Sample; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use webrtc::track::track_local::TrackLocal; -use webrtc::Error; - -const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("play-from-disk-vpx") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of play-from-disk-vpx.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ) - .arg( - Arg::new("audio") - .takes_value(true) - .short('a') - .long("audio") - .help("Audio file to be streaming."), - ) - .arg( - Arg::new("vp9") - .long("vp9") - .help("Save VP9 to disk. Default: VP8"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let is_vp9 = matches.is_present("vp9"); - let video_file = matches.value_of("video"); - let audio_file = matches.value_of("audio"); - - if let Some(video_path) = &video_file { - if !Path::new(video_path).exists() { - return Err(Error::new(format!("video file: '{video_path}' not exist")).into()); - } - } - if let Some(audio_path) = &audio_file { - if !Path::new(audio_path).exists() { - return Err(Error::new(format!("audio file: '{audio_path}' not exist")).into()); - } - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let notify_tx = Arc::new(Notify::new()); - let notify_video = notify_tx.clone(); - let notify_audio = notify_tx.clone(); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - let video_done_tx = done_tx.clone(); - let audio_done_tx = done_tx.clone(); - - if let Some(video_file) = video_file { - // Create a video track - let video_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: if is_vp9 { - MIME_TYPE_VP9.to_owned() - } else { - MIME_TYPE_VP8.to_owned() - }, - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&video_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let video_file_name = video_file.to_owned(); - tokio::spawn(async move { - // Open a IVF file and start reading using our IVFReader - let file = File::open(&video_file_name)?; - let reader = BufReader::new(file); - let (mut ivf, header) = IVFReader::new(reader)?; - - // Wait for connection established - notify_video.notified().await; - - println!("play video from disk file {video_file_name}"); - - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. - // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. - let sleep_time = Duration::from_millis( - ((1000 * header.timebase_numerator) / header.timebase_denominator) as u64, - ); - let mut ticker = tokio::time::interval(sleep_time); - loop { - let frame = match ivf.parse_next_frame() { - Ok((frame, _)) => frame, - Err(err) => { - println!("All video frames parsed and sent: {err}"); - break; - } - }; - - video_track - .write_sample(&Sample { - data: frame.freeze(), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - - let _ = ticker.tick().await; - } - - let _ = video_done_tx.try_send(()); - - Result::<()>::Ok(()) - }); - } - - if let Some(audio_file) = audio_file { - // Create a audio track - let audio_track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - "audio".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&audio_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let audio_file_name = audio_file.to_owned(); - tokio::spawn(async move { - // Open a IVF file and start reading using our IVFReader - let file = File::open(audio_file_name)?; - let reader = BufReader::new(file); - // Open on oggfile in non-checksum mode. - let (mut ogg, _) = match OggReader::new(reader, true) { - Ok(tup) => tup, - Err(err) => { - println!("error while opening audio file output.ogg: {err}"); - return Err(err.into()); - } - }; - // Wait for connection established - notify_audio.notified().await; - - println!("play audio from disk file output.ogg"); - - // It is important to use a time.Ticker instead of time.Sleep because - // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data - // * works around latency issues with Sleep - let mut ticker = tokio::time::interval(OGG_PAGE_DURATION); - - // Keep track of last granule, the difference is the amount of samples in the buffer - let mut last_granule: u64 = 0; - while let Ok((page_data, page_header)) = ogg.parse_next_page() { - // The amount of samples is the difference between the last and current timestamp - let sample_count = page_header.granule_position - last_granule; - last_granule = page_header.granule_position; - let sample_duration = Duration::from_millis(sample_count * 1000 / 48000); - - audio_track - .write_sample(&Sample { - data: page_data.freeze(), - duration: sample_duration, - ..Default::default() - }) - .await?; - - let _ = ticker.tick().await; - } - - let _ = audio_done_tx.try_send(()); - - Result::<()>::Ok(()) - }); - } - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - if connection_state == RTCIceConnectionState::Connected { - notify_tx.notify_waiters(); - } - Box::pin(async {}) - }, - )); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/rc-cycle/rc-cycle.rs b/examples/examples/rc-cycle/rc-cycle.rs deleted file mode 100644 index 88ba15e3d..000000000 --- a/examples/examples/rc-cycle/rc-cycle.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Clone)] -struct Cycle { - cell: RefCell>>, -} - -impl Drop for Cycle { - fn drop(&mut self) { - println!("freed"); - } -} - -#[tokio::main] -async fn main() { - let cycle = Rc::new(Cycle { - cell: RefCell::new(None), - }); - *cycle.cell.borrow_mut() = Some(cycle.clone()); -} - -// use nightly rust -// RUSTFLAGS="-Z sanitizer=leak" cargo build --example rc-cycle -// ./target/debug/example/rc-cycle -// ================================================================= -// ==1457719==ERROR: LeakSanitizer: detected memory leaks -// -// Direct leak of 32 byte(s) in 1 object(s) allocated from: -// #0 0x55d4688e1b58 in malloc /rustc/llvm/src/llvm-project/compiler-rt/lib/lsan/lsan_interceptors.cpp:56:3 -// #1 0x55d4689db6cb in alloc::alloc::alloc::h1ab42fe6949393de /rustc/e269e6bf47f40c9046cd44ab787881d700099252/library/alloc/src/alloc.rs:86:14 -// -// SUMMARY: LeakSanitizer: 32 byte(s) leaked in 1 allocation(s). diff --git a/examples/examples/reflect/README.md b/examples/examples/reflect/README.md deleted file mode 100644 index dea22a1e7..000000000 --- a/examples/examples/reflect/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# reflect - -reflect demonstrates how with one PeerConnection you can send video to webrtc-rs and have the packets sent back. This example could be easily extended to do server side processing. - -## Instructions - -### Build reflect - -```shell -cargo build --example reflect -``` - -### Open reflect example page - -[jsfiddle.net](https://jsfiddle.net/9jgukzt1/) you should see two text-areas and a 'Start Session' button. - -### Run reflect, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/reflect -a -v` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/reflect -a -v < my_file` - -### Input reflect's SessionDescription into your browser - -Copy the text that `reflect` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -Your browser should send video to webrtc-rs, and then it will be relayed right back to you. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/reflect/reflect.rs b/examples/examples/reflect/reflect.rs deleted file mode 100644 index c01875e42..000000000 --- a/examples/examples/reflect/reflect.rs +++ /dev/null @@ -1,326 +0,0 @@ -use std::collections::HashMap; -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_OPUS, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType, -}; -use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("reflect") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of how to send back to the user exactly what it receives using the same PeerConnection.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ).arg( - Arg::new("audio") - .long("audio") - .short('a') - .help("Enable audio reflect"), - ).arg( - Arg::new("video") - .long("video") - .short('v') - .help("Enable video reflect"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let audio = matches.is_present("audio"); - let video = matches.is_present("video"); - if !audio && !video { - println!("one of audio or video must be enabled"); - std::process::exit(0); - } - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Setup the codecs you want to use. - if audio { - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - payload_type: 120, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - } - - // We'll use a VP8 and Opus but you can also define your own - if video { - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - } - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - let mut output_tracks = HashMap::new(); - let mut media = vec![]; - if audio { - media.push("audio"); - } - if video { - media.push("video"); - }; - for s in media { - let output_track = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: if s == "video" { - MIME_TYPE_VP8.to_owned() - } else { - MIME_TYPE_OPUS.to_owned() - }, - ..Default::default() - }, - format!("track-{s}"), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&output_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - let m = s.to_owned(); - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - println!("{m} rtp_sender.read loop exit"); - Result::<()>::Ok(()) - }); - - output_tracks.insert(s.to_owned(), output_track); - } - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Set a handler for when a new remote track starts, this handler copies inbound RTP packets, - // replaces the SSRC and sends them back - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval - // This is a temporary fix until we implement incoming RTCP events, then we would push a PLI only when a viewer requests it - let media_ssrc = track.ssrc(); - - if track.kind() == RTPCodecType::Video { - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else{ - break; - } - } - }; - } - }); - } - - let kind = if track.kind() == RTPCodecType::Audio { - "audio" - } else { - "video" - }; - let output_track = if let Some(output_track) = output_tracks.get(kind) { - Arc::clone(output_track) - } else { - println!("output_track not found for type = {kind}"); - return Box::pin(async {}); - }; - - let output_track2 = Arc::clone(&output_track); - tokio::spawn(async move { - println!( - "Track has started, of type {}: {}", - track.payload_type(), - track.codec().capability.mime_type - ); - // Read RTP packets being sent to webrtc-rs - while let Ok((rtp, _)) = track.read_rtp().await { - if let Err(err) = output_track2.write_rtp(&rtp).await { - println!("output track write_rtp got error: {err}"); - break; - } - } - - println!( - "on_track finished, of type {}: {}", - track.payload_type(), - track.codec().capability.mime_type - ); - }); - - Box::pin(async {}) - })); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - //let timeout = tokio::time::sleep(Duration::from_secs(20)); - //tokio::pin!(timeout); - - tokio::select! { - //_ = timeout.as_mut() => { - // println!("received timeout signal!"); - //} - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/rtp-forwarder/README.md b/examples/examples/rtp-forwarder/README.md deleted file mode 100644 index 04fb0bf45..000000000 --- a/examples/examples/rtp-forwarder/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# rtp-forwarder - -rtp-forwarder is a simple application that shows how to forward your webcam/microphone via RTP using WebRTC.rs. - -## Instructions - -### Build rtp-forwarder - -```shell -cargo build --example rtp-forwarder -``` - -### Open rtp-forwarder example page - -[jsfiddle.net](https://jsfiddle.net/1qva2zd8/) you should see your Webcam, two text-areas and a 'Start Session' button - -### Run rtp-forwarder, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/rtp-forwarder` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/rtp-forwarder < my_file` - -### Input rtp-forwarder's SessionDescription into your browser - -Copy the text that `rtp-forwarder` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle and enjoy your RTP forwarded stream! - -You can run any of these commands at anytime. The media is live/stateless, you can switch commands without restarting Pion. - -#### VLC - -Open `rtp-forwarder.sdp` with VLC and enjoy your live video! - -#### ffmpeg/ffprobe - -Run `ffprobe -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to get more details about your streams - -Run `ffplay -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to play your streams - -You can add `-fflags nobuffer` to lower the latency. You will have worse playback in networks with jitter. - -#### Twitch/RTMP - -`ffmpeg -protocol_whitelist file,udp,rtp -i rtp-forwarder.sdp -c:v libx264 -preset veryfast -b:v 3000k -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv rtmp://live.twitch.tv/app/$STREAM_KEY` Make sure to replace `$STREAM_KEY` at the end of the URL first. diff --git a/examples/examples/rtp-forwarder/rtp-forwarder.rs b/examples/examples/rtp-forwarder/rtp-forwarder.rs deleted file mode 100644 index c6e548ae3..000000000 --- a/examples/examples/rtp-forwarder/rtp-forwarder.rs +++ /dev/null @@ -1,321 +0,0 @@ -use std::collections::HashMap; -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::net::UdpSocket; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_OPUS, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType, -}; -use webrtc::util::{Conn, Marshal}; - -#[derive(Clone)] -struct UdpConn { - conn: Arc, - payload_type: u8, -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("rtp-forwarder") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of rtp-forwarder.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Setup the codecs you want to use. - // We'll use a VP8 and Opus but you can also define your own - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Allow us to receive 1 audio track, and 1 video track - peer_connection - .add_transceiver_from_kind(RTPCodecType::Audio, None) - .await?; - peer_connection - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - // Prepare udp conns - // Also update incoming packets with expected PayloadType, the browser may use - // a different value. We have to modify so our stream matches what rtp-forwarder.sdp expects - let mut udp_conns = HashMap::new(); - udp_conns.insert( - "audio".to_owned(), - UdpConn { - conn: { - let sock = UdpSocket::bind("127.0.0.1:0").await?; - sock.connect(format!("127.0.0.1:{}", 4000)).await?; - Arc::new(sock) - }, - payload_type: 111, - }, - ); - udp_conns.insert( - "video".to_owned(), - UdpConn { - conn: { - let sock = UdpSocket::bind("127.0.0.1:0").await?; - sock.connect(format!("127.0.0.1:{}", 4002)).await?; - Arc::new(sock) - }, - payload_type: 96, - }, - ); - - // Set a handler for when a new remote track starts, this handler will forward data to - // our UDP listeners. - // In your application this is where you would handle/process audio/video - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - // Retrieve udp connection - let c = if let Some(c) = udp_conns.get(&track.kind().to_string()) { - c.clone() - } else { - return Box::pin(async {}); - }; - - // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval - let media_ssrc = track.ssrc(); - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else{ - break; - } - } - }; - } - }); - - tokio::spawn(async move { - let mut b = vec![0u8; 1500]; - while let Ok((mut rtp_packet, _)) = track.read(&mut b).await { - // Update the PayloadType - rtp_packet.header.payload_type = c.payload_type; - - // Marshal into original buffer with updated PayloadType - - let n = rtp_packet.marshal_to(&mut b)?; - - // Write - if let Err(err) = c.conn.send(&b[..n]).await { - // For this particular example, third party applications usually timeout after a short - // amount of time during which the user doesn't have enough time to provide the answer - // to the browser. - // That's why, for this particular example, the user first needs to provide the answer - // to the browser then open the third party application. Therefore we must not kill - // the forward on "connection refused" errors - //if opError, ok := err.(*net.OpError); ok && opError.Err.Error() == "write: connection refused" { - // continue - //} - //panic(err) - if err.to_string().contains("Connection refused") { - continue; - } else { - println!("conn send err: {err}"); - break; - } - } - } - - Result::<()>::Ok(()) - }); - - Box::pin(async {}) - })); - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - if connection_state == RTCIceConnectionState::Connected { - println!("Ctrl+C the remote client to stop the demo"); - } - Box::pin(async {}) - }, - )); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting: Done forwarding"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/rtp-forwarder/rtp-forwarder.sdp b/examples/examples/rtp-forwarder/rtp-forwarder.sdp deleted file mode 100644 index bb0367c11..000000000 --- a/examples/examples/rtp-forwarder/rtp-forwarder.sdp +++ /dev/null @@ -1,9 +0,0 @@ -v=0 -o=- 0 0 IN IP4 127.0.0.1 -s=WebRTC.rs -c=IN IP4 127.0.0.1 -t=0 0 -m=audio 4000 RTP/AVP 111 -a=rtpmap:111 OPUS/48000/2 -m=video 4002 RTP/AVP 96 -a=rtpmap:96 VP8/90000 \ No newline at end of file diff --git a/examples/examples/rtp-to-webrtc/README.md b/examples/examples/rtp-to-webrtc/README.md deleted file mode 100644 index 2ccb2cb7f..000000000 --- a/examples/examples/rtp-to-webrtc/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# rtp-to-webrtc - -rtp-to-webrtc demonstrates how to consume a RTP stream video UDP, and then send to a WebRTC client. - -With this example we have pre-made GStreamer and ffmpeg pipelines, but you can use any tool you like! - -## Instructions - -### Build rtp-to-webrtc - -```shell -cargo build --example rtp-to-webrtc -``` - -### Open jsfiddle example page - -[jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) you should see two text-areas and a 'Start Session' button - -### Run rtp-to-webrtc with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser's SessionDescription, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/rtp-to-webrtc` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/rtp-to-webrtc < my_file` - -### Send RTP to listening socket - -You can use any software to send VP8 packets to port 5004. We also have the pre made examples below - -#### GStreamer - -```shell -gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,format=I420 ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! rtpvp8pay ! udpsink host=127.0.0.1 port=5004 -``` - -#### ffmpeg - -```shell -ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec libvpx -cpu-used 5 -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -f rtp rtp://127.0.0.1:5004?pkt_size=1200 -``` - -### Input rtp-to-webrtc's SessionDescription into your browser - -Copy the text that `rtp-to-webrtc` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -A video should start playing in your browser above the input boxes. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/rtp-to-webrtc/rtp-to-webrtc.rs b/examples/examples/rtp-to-webrtc/rtp-to-webrtc.rs deleted file mode 100644 index ddcb0555f..000000000 --- a/examples/examples/rtp-to-webrtc/rtp-to-webrtc.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::net::UdpSocket; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; -use webrtc::Error; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("rtp-forwarder") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of rtp-forwarder.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Create Track that we send video back to browser on - let video_track = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&video_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - let done_tx1 = done_tx.clone(); - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - if connection_state == RTCIceConnectionState::Failed { - let _ = done_tx1.try_send(()); - } - Box::pin(async {}) - }, - )); - - let done_tx2 = done_tx.clone(); - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting: Done forwarding"); - let _ = done_tx2.try_send(()); - } - - Box::pin(async {}) - })); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - // Open a UDP Listener for RTP Packets on port 5004 - let listener = UdpSocket::bind("127.0.0.1:5004").await?; - - let done_tx3 = done_tx.clone(); - // Read RTP packets forever and send them to the WebRTC Client - tokio::spawn(async move { - let mut inbound_rtp_packet = vec![0u8; 1600]; // UDP MTU - while let Ok((n, _)) = listener.recv_from(&mut inbound_rtp_packet).await { - if let Err(err) = video_track.write(&inbound_rtp_packet[..n]).await { - if Error::ErrClosedPipe == err { - // The peerConnection has been closed. - } else { - println!("video_track write err: {err}"); - } - let _ = done_tx3.try_send(()); - return; - } - } - }); - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/save-to-disk-h264/README.md b/examples/examples/save-to-disk-h264/README.md deleted file mode 100644 index f18102cc1..000000000 --- a/examples/examples/save-to-disk-h264/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# save-to-disk-h264 - -save-to-disk-h264 is a simple application that shows how to record your webcam/microphone using WebRTC.rs and save H264 and Opus to disk. - -## Instructions - -### Build save-to-disk-h264 - -```shell -cargo build --example save-to-disk-h264 -``` - -### Open save-to-disk example page - -[jsfiddle.net](https://jsfiddle.net/vfmcg8rk/1/) you should see your Webcam, two text-areas and a 'Start Session' button - -### Run save-to-disk-h264, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/save-to-disk-h264` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/save-to-disk-h264 < my_file` - -### Input save-to-disk-h264's SessionDescription into your browser - -Copy the text that `save-to-disk-h264` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video! - -In the folder you ran `save-to-disk-h264` you should now have a file `output.h264` play with your video player of choice! - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/save-to-disk-h264/save-to-disk-h264.rs b/examples/examples/save-to-disk-h264/save-to-disk-h264.rs deleted file mode 100644 index 238320b37..000000000 --- a/examples/examples/save-to-disk-h264/save-to-disk-h264.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::fs::File; -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::{Mutex, Notify}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_H264, MIME_TYPE_OPUS}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::h264_writer::H264Writer; -use webrtc::media::io::ogg_writer::OggWriter; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType, -}; -use webrtc::track::track_remote::TrackRemote; - -async fn save_to_disk( - writer: Arc>, - track: Arc, - notify: Arc, -) -> Result<()> { - loop { - tokio::select! { - result = track.read_rtp() => { - if let Ok((rtp_packet, _)) = result { - let mut w = writer.lock().await; - w.write_rtp(&rtp_packet)?; - }else{ - println!("file closing begin after read_rtp error"); - let mut w = writer.lock().await; - if let Err(err) = w.close() { - println!("file close err: {err}"); - } - println!("file closing end after read_rtp error"); - return Ok(()); - } - } - _ = notify.notified() => { - println!("file closing begin after notified"); - let mut w = writer.lock().await; - if let Err(err) = w.close() { - println!("file close err: {err}"); - } - println!("file closing end after notified"); - return Ok(()); - } - } - } -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("save-to-disk-h264") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of save-to-disk-h264.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ) - .arg( - Arg::new("audio") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('a') - .long("audio") - .help("Audio file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let video_file = matches.value_of("video").unwrap(); - let audio_file = matches.value_of("audio").unwrap(); - - let h264_writer: Arc> = - Arc::new(Mutex::new(H264Writer::new(File::create(video_file)?))); - let ogg_writer: Arc> = Arc::new(Mutex::new( - OggWriter::new(File::create(audio_file)?, 48000, 2)?, - )); - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Setup the codecs you want to use. - // We'll use a H264 and Opus but you can also define your own - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 102, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Allow us to receive 1 audio track, and 1 video track - peer_connection - .add_transceiver_from_kind(RTPCodecType::Audio, None) - .await?; - peer_connection - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let notify_tx = Arc::new(Notify::new()); - let notify_rx = notify_tx.clone(); - - // Set a handler for when a new remote track starts, this handler saves buffers to disk as - // an ivf file, since we could have multiple video tracks we provide a counter. - // In your application this is where you would handle/process video - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval - let media_ssrc = track.ssrc(); - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else { - break; - } - } - }; - } - }); - - let notify_rx2 = Arc::clone(¬ify_rx); - let h264_writer2 = Arc::clone(&h264_writer); - let ogg_writer2 = Arc::clone(&ogg_writer); - Box::pin(async move { - let codec = track.codec(); - let mime_type = codec.capability.mime_type.to_lowercase(); - if mime_type == MIME_TYPE_OPUS.to_lowercase() { - println!("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)"); - tokio::spawn(async move { - let _ = save_to_disk(ogg_writer2, track, notify_rx2).await; - }); - } else if mime_type == MIME_TYPE_H264.to_lowercase() { - println!("Got h264 track, saving to disk as output.h264"); - tokio::spawn(async move { - let _ = save_to_disk(h264_writer2, track, notify_rx2).await; - }); - } - }) - })); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - - if connection_state == RTCIceConnectionState::Connected { - println!("Ctrl+C the remote client to stop the demo"); - } else if connection_state == RTCIceConnectionState::Failed { - notify_tx.notify_waiters(); - - println!("Done writing media files"); - - let _ = done_tx.try_send(()); - } - Box::pin(async {}) - }, - )); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/save-to-disk-vpx/README.md b/examples/examples/save-to-disk-vpx/README.md deleted file mode 100644 index 2e18057f7..000000000 --- a/examples/examples/save-to-disk-vpx/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# save-to-disk-vpx - -save-to-disk-vpx is a simple application that shows how to record your webcam/microphone using WebRTC.rs and save VP8/VP9 and Opus to disk. - -## Instructions - -### Build save-to-disk-vpx - -```shell -cargo build --example save-to-disk-vpx -``` - -### Open save-to-disk-vpx example page - -[jsfiddle.net](https://jsfiddle.net/vfmcg8rk/1/) you should see your Webcam, two text-areas and a 'Start Session' button - -### Run save-to-disk-vpx, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/save-to-disk-vpx` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/save-to-disk-vpx < my_file` - -### Input save-to-disk-vpx's SessionDescription into your browser - -Copy the text that `save-to-disk-vpx` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video! - -In the folder you ran `save-to-disk-vpx` you should now have a file `output_vpx.ivf` play with your video player of choice! - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/save-to-disk-vpx/save-to-disk-vpx.rs b/examples/examples/save-to-disk-vpx/save-to-disk-vpx.rs deleted file mode 100644 index 2b02986b6..000000000 --- a/examples/examples/save-to-disk-vpx/save-to-disk-vpx.rs +++ /dev/null @@ -1,348 +0,0 @@ -use std::fs::File; -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::sync::{Mutex, Notify}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_OPUS, MIME_TYPE_VP8, MIME_TYPE_VP9}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_connection_state::RTCIceConnectionState; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::media::io::ivf_reader::IVFFileHeader; -use webrtc::media::io::ivf_writer::IVFWriter; -use webrtc::media::io::ogg_writer::OggWriter; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType, -}; -use webrtc::track::track_remote::TrackRemote; - -async fn save_to_disk( - writer: Arc>, - track: Arc, - notify: Arc, -) -> Result<()> { - loop { - tokio::select! { - result = track.read_rtp() => { - if let Ok((rtp_packet, _)) = result { - let mut w = writer.lock().await; - w.write_rtp(&rtp_packet)?; - }else{ - println!("file closing begin after read_rtp error"); - let mut w = writer.lock().await; - if let Err(err) = w.close() { - println!("file close err: {err}"); - } - println!("file closing end after read_rtp error"); - return Ok(()); - } - } - _ = notify.notified() => { - println!("file closing begin after notified"); - let mut w = writer.lock().await; - if let Err(err) = w.close() { - println!("file close err: {err}"); - } - println!("file closing end after notified"); - return Ok(()); - } - } - } -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("save-to-disk-vpx") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of save-to-disk-vpx.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ) - .arg( - Arg::new("vp9") - .long("vp9") - .help("Save VP9 to disk. Default: VP8"), - ) - .arg( - Arg::new("video") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('v') - .long("video") - .help("Video file to be streaming."), - ) - .arg( - Arg::new("audio") - .required_unless_present("FULLHELP") - .takes_value(true) - .short('a') - .long("audio") - .help("Audio file to be streaming."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - let is_vp9 = matches.is_present("vp9"); - let video_file = matches.value_of("video").unwrap(); - let audio_file = matches.value_of("audio").unwrap(); - - let ivf_writer: Arc> = - Arc::new(Mutex::new(IVFWriter::new( - File::create(video_file)?, - &IVFFileHeader { - signature: *b"DKIF", // 0-3 - version: 0, // 4-5 - header_size: 32, // 6-7 - four_cc: if is_vp9 { *b"VP90" } else { *b"VP80" }, // 8-11 - width: 640, // 12-13 - height: 480, // 14-15 - timebase_denominator: 30, // 16-19 - timebase_numerator: 1, // 20-23 - num_frames: 900, // 24-27 - unused: 0, // 28-31 - }, - )?)); - let ogg_writer: Arc> = Arc::new(Mutex::new( - OggWriter::new(File::create(audio_file)?, 48000, 2)?, - )); - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Setup the codecs you want to use. - // We'll use a VP8/VP9 and Opus but you can also define your own - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: if is_vp9 { - MIME_TYPE_VP9.to_owned() - } else { - MIME_TYPE_VP8.to_owned() - }, - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: if is_vp9 { 98 } else { 96 }, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Allow us to receive 1 audio track, and 1 video track - peer_connection - .add_transceiver_from_kind(RTPCodecType::Audio, None) - .await?; - peer_connection - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let notify_tx = Arc::new(Notify::new()); - let notify_rx = notify_tx.clone(); - - // Set a handler for when a new remote track starts, this handler saves buffers to disk as - // an ivf file, since we could have multiple video tracks we provide a counter. - // In your application this is where you would handle/process video - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval - let media_ssrc = track.ssrc(); - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else{ - break; - } - } - }; - } - }); - - let notify_rx2 = Arc::clone(¬ify_rx); - let ivf_writer2 = Arc::clone(&ivf_writer); - let ogg_writer2 = Arc::clone(&ogg_writer); - Box::pin(async move { - let codec = track.codec(); - let mime_type = codec.capability.mime_type.to_lowercase(); - if mime_type == MIME_TYPE_OPUS.to_lowercase() { - println!("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)"); - tokio::spawn(async move { - let _ = save_to_disk(ogg_writer2, track, notify_rx2).await; - }); - } else if mime_type == MIME_TYPE_VP8.to_lowercase() - || mime_type == MIME_TYPE_VP9.to_lowercase() - { - println!( - "Got {} track, saving to disk as output.ivf", - if is_vp9 { "VP9" } else { "VP8" } - ); - tokio::spawn(async move { - let _ = save_to_disk(ivf_writer2, track, notify_rx2).await; - }); - } - }) - })); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_ice_connection_state_change(Box::new( - move |connection_state: RTCIceConnectionState| { - println!("Connection State has changed {connection_state}"); - - if connection_state == RTCIceConnectionState::Connected { - println!("Ctrl+C the remote client to stop the demo"); - } else if connection_state == RTCIceConnectionState::Failed { - notify_tx.notify_waiters(); - - println!("Done writing media files"); - - let _ = done_tx.try_send(()); - } - Box::pin(async {}) - }, - )); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/signal/Cargo.toml b/examples/examples/signal/Cargo.toml deleted file mode 100644 index 3c71bb789..000000000 --- a/examples/examples/signal/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "signal" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = { version = "1.32.0", features = ["full"] } -anyhow = "1" -base64 = "0.21" -lazy_static = "1" -hyper = { version = "0.14.27", features = ["full"] } diff --git a/examples/examples/signal/src/lib.rs b/examples/examples/signal/src/lib.rs deleted file mode 100644 index c6267b848..000000000 --- a/examples/examples/signal/src/lib.rs +++ /dev/null @@ -1,149 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::Result; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use tokio::sync::{mpsc, Mutex}; - -#[macro_use] -extern crate lazy_static; - -lazy_static! { - static ref SDP_CHAN_TX_MUTEX: Arc>>> = - Arc::new(Mutex::new(None)); -} - -// HTTP Listener to get sdp -async fn remote_handler(req: Request) -> Result, hyper::Error> { - match (req.method(), req.uri().path()) { - // A HTTP handler that processes a SessionDescription given to us from the other WebRTC-rs or Pion process - (&Method::POST, "/sdp") => { - //println!("remote_handler receive from /sdp"); - let sdp_str = match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) - { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - - { - let sdp_chan_tx = SDP_CHAN_TX_MUTEX.lock().await; - if let Some(tx) = &*sdp_chan_tx { - let _ = tx.send(sdp_str).await; - } - } - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -/// http_sdp_server starts a HTTP Server that consumes SDPs -pub async fn http_sdp_server(port: u16) -> mpsc::Receiver { - let (sdp_chan_tx, sdp_chan_rx) = mpsc::channel::(1); - { - let mut tx = SDP_CHAN_TX_MUTEX.lock().await; - *tx = Some(sdp_chan_tx); - } - - tokio::spawn(async move { - let addr = SocketAddr::from_str(&format!("0.0.0.0:{port}")).unwrap(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - // Run this server for... forever! - if let Err(e) = server.await { - eprintln!("server error: {e}"); - } - }); - - sdp_chan_rx -} - -/// must_read_stdin blocks until input is received from stdin -#[allow(clippy::assigning_clones)] -pub fn must_read_stdin() -> Result { - let mut line = String::new(); - - std::io::stdin().read_line(&mut line)?; - line = line.trim().to_owned(); - println!(); - - Ok(line) -} - -// Allows compressing offer/answer to bypass terminal input limits. -// const COMPRESS: bool = false; - -/// encode encodes the input in base64 -/// It can optionally zip the input before encoding -pub fn encode(b: &str) -> String { - //if COMPRESS { - // b = zip(b) - //} - - BASE64_STANDARD.encode(b) -} - -/// decode decodes the input from base64 -/// It can optionally unzip the input after decoding -pub fn decode(s: &str) -> Result { - let b = BASE64_STANDARD.decode(s)?; - - //if COMPRESS { - // b = unzip(b) - //} - - let s = String::from_utf8(b)?; - Ok(s) -} -/* -func zip(in []byte) []byte { - var b bytes.Buffer - gz := gzip.NewWriter(&b) - _, err := gz.Write(in) - if err != nil { - panic(err) - } - err = gz.Flush() - if err != nil { - panic(err) - } - err = gz.Close() - if err != nil { - panic(err) - } - return b.Bytes() -} - -func unzip(in []byte) []byte { - var b bytes.Buffer - _, err := b.Write(in) - if err != nil { - panic(err) - } - r, err := gzip.NewReader(&b) - if err != nil { - panic(err) - } - res, err := ioutil.ReadAll(r) - if err != nil { - panic(err) - } - return res -} -*/ diff --git a/examples/examples/simulcast/README.md b/examples/examples/simulcast/README.md deleted file mode 100644 index 31962490c..000000000 --- a/examples/examples/simulcast/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# simulcast - -demonstrates of how to handle incoming track with multiple simulcast rtp streams and show all them back. - -The browser will not send higher quality streams unless it has the available bandwidth. You can look at -the bandwidth estimation in `chrome://webrtc-internals`. It is under `VideoBwe` when `Read Stats From: Legacy non-Standard` -is selected. - -## Instructions - -### Build simulcast - -```shell -cargo build --example simulcast -``` - -### Open simulcast example page - -[jsfiddle.net](https://jsfiddle.net/rxk4bftc) you should see two text-areas and a 'Start Session' button. - -### Run simulcast, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/simulcast` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/simulcast < my_file` - -### Input simulcast's SessionDescription into your browser - -Copy the text that `simulcast` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -Your browser should send a simulcast track to WebRTC.rs, and then all 3 incoming streams will be relayed back. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/simulcast/simulcast.rs b/examples/examples/simulcast/simulcast.rs deleted file mode 100644 index f47c7b620..000000000 --- a/examples/examples/simulcast/simulcast.rs +++ /dev/null @@ -1,266 +0,0 @@ -use std::collections::HashMap; -use std::io::Write; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpHeaderExtensionCapability, RTPCodecType, -}; -use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; -use webrtc::Error; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("simulcast") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of simulcast.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - m.register_default_codecs()?; - - // Enable Extension Headers needed for Simulcast - for extension in [ - "urn:ietf:params:rtp-hdrext:sdes:mid", - "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", - "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", - ] { - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: extension.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - } - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - // Create Track that we send video back to browser on - let mut output_tracks = HashMap::new(); - for s in ["q", "h", "f"] { - let output_track = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - format!("video_{s}"), - format!("webrtc-rs_{s}"), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&output_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - output_tracks.insert(s.to_owned(), output_track); - } - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Set a handler for when a new remote track starts - let pc = Arc::downgrade(&peer_connection); - peer_connection.on_track(Box::new(move |track, _, _| { - println!("Track has started"); - - let rid = track.rid().to_owned(); - let output_track = if let Some(output_track) = output_tracks.get(&rid) { - Arc::clone(output_track) - } else { - println!("output_track not found for rid = {rid}"); - return Box::pin(async {}); - }; - - // Start reading from all the streams and sending them to the related output track - let media_ssrc = track.ssrc(); - let pc2 = pc.clone(); - tokio::spawn(async move { - let mut result = Result::::Ok(0); - while result.is_ok() { - println!("Sending pli for stream with rid: {rid}, ssrc: {media_ssrc}"); - - let timeout = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - if let Some(pc) = pc2.upgrade(){ - result = pc.write_rtcp(&[Box::new(PictureLossIndication{ - sender_ssrc: 0, - media_ssrc, - })]).await.map_err(Into::into); - }else{ - break; - } - } - }; - } - }); - - tokio::spawn(async move { - // Read RTP packets being sent to webrtc-rs - println!("enter track loop {}", track.rid()); - while let Ok((rtp, _)) = track.read_rtp().await { - if let Err(err) = output_track.write_rtp(&rtp).await { - if Error::ErrClosedPipe != err { - println!("output track write_rtp got error: {err} and break"); - break; - } else { - println!("output track write_rtp got error: {err}"); - } - } - } - println!("exit track loop {}", track.rid()); - }); - - Box::pin(async {}) - })); - - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - - if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - println!("Peer Connection has gone to failed exiting"); - let _ = done_tx.try_send(()); - } - - Box::pin(async {}) - })); - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - println!("Press ctrl-c to stop"); - tokio::select! { - _ = done_rx.recv() => { - println!("received done signal!"); - } - _ = tokio::signal::ctrl_c() => { - println!(); - } - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/swap-tracks/README.md b/examples/examples/swap-tracks/README.md deleted file mode 100644 index 1157ae206..000000000 --- a/examples/examples/swap-tracks/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# swap-tracks - -swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing track. - -## Instructions - -### Build swap-tracks - -```shell -cargo build --example swap-tracks -``` - -### Open swap-tracks example page - -[jsfiddle.net](https://jsfiddle.net/dzc17fga/) you should see two text-areas and a 'Start Session' button. - -### Run swap-tracks, with your browsers SessionDescription as stdin - -In the jsfiddle the top textarea is your browser, copy that and: - -#### Linux/macOS - -Run `echo $BROWSER_SDP | ./target/debug/examples/swap-tracks` - -#### Windows - -1. Paste the SessionDescription into a file. -1. Run `./target/debug/examples/swap-tracks < my_file` - -### Input swap-tracks's SessionDescription into your browser - -Copy the text that `swap-tracks` just emitted and copy into second text area - -### Hit 'Start Session' in jsfiddle, enjoy your video! - -Your browser should send streams to webrtc-rs, and then a stream will be relayed back, changing every 5 seconds. - -Congrats, you have used WebRTC.rs! diff --git a/examples/examples/swap-tracks/swap-tracks.rs b/examples/examples/swap-tracks/swap-tracks.rs deleted file mode 100644 index db8cd25d7..000000000 --- a/examples/examples/swap-tracks/swap-tracks.rs +++ /dev/null @@ -1,315 +0,0 @@ -use std::io::Write; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -use anyhow::Result; -use clap::{AppSettings, Arg, Command}; -use tokio::time::Duration; -use webrtc::api::interceptor_registry::register_default_interceptors; -use webrtc::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use webrtc::api::APIBuilder; -use webrtc::ice_transport::ice_server::RTCIceServer; -use webrtc::interceptor::registry::Registry; -use webrtc::peer_connection::configuration::RTCConfiguration; -use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; -use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; -use webrtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; -use webrtc::Error; - -#[tokio::main] -async fn main() -> Result<()> { - let mut app = Command::new("swap-tracks") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of swap-tracks.") - .setting(AppSettings::DeriveDisplayOrder) - .subcommand_negates_reqs(true) - .arg( - Arg::new("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Prints debug log information"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let debug = matches.is_present("debug"); - if debug { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - } - - // Everything below is the WebRTC-rs API! Thanks for using it ❤️. - - // Create a MediaEngine object to configure the supported codec - let mut m = MediaEngine::default(); - - // Setup the codecs you want to use. - m.register_default_codecs()?; - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - let mut registry = Registry::new(); - - // Use the default set of Interceptors - registry = register_default_interceptors(registry, &mut m)?; - - // Create the API object with the MediaEngine - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - // Prepare the configuration - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - // Create a new RTCPeerConnection - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let output_track = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - // Add this newly created track to the PeerConnection - let rtp_sender = peer_connection - .add_track(Arc::clone(&output_track) as Arc) - .await?; - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - tokio::spawn(async move { - let mut rtcp_buf = vec![0u8; 1500]; - while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await {} - Result::<()>::Ok(()) - }); - - // Wait for the offer to be pasted - let line = signal::must_read_stdin()?; - let desc_data = signal::decode(line.as_str())?; - let offer = serde_json::from_str::(&desc_data)?; - - // Set the remote SessionDescription - peer_connection.set_remote_description(offer).await?; - - // Which track is currently being handled - let curr_track = Arc::new(AtomicUsize::new(0)); - // The total number of tracks - let track_count = Arc::new(AtomicUsize::new(0)); - // The channel of packets with a bit of buffer - let (packets_tx, mut packets_rx) = - tokio::sync::mpsc::channel::(60); - let packets_tx = Arc::new(packets_tx); - - // Set a handler for when a new remote track starts, this handler copies inbound RTP packets, - // replaces the SSRC and sends them back - let pc = Arc::downgrade(&peer_connection); - let curr_track1 = Arc::clone(&curr_track); - let track_count1 = Arc::clone(&track_count); - peer_connection.on_track(Box::new(move |track, _, _| { - let track_num = track_count1.fetch_add(1, Ordering::SeqCst); - - let curr_track2 = Arc::clone(&curr_track1); - let pc2 = pc.clone(); - let packets_tx2 = Arc::clone(&packets_tx); - tokio::spawn(async move { - println!( - "Track has started, of type {}: {}", - track.payload_type(), - track.codec().capability.mime_type - ); - - let mut last_timestamp = 0; - let mut is_curr_track = false; - while let Ok((mut rtp, _)) = track.read_rtp().await { - // Change the timestamp to only be the delta - let old_timestamp = rtp.header.timestamp; - if last_timestamp == 0 { - rtp.header.timestamp = 0 - } else { - rtp.header.timestamp -= last_timestamp; - } - last_timestamp = old_timestamp; - - // Check if this is the current track - if curr_track2.load(Ordering::SeqCst) == track_num { - // If just switched to this track, send PLI to get picture refresh - if !is_curr_track { - is_curr_track = true; - if let Some(pc) = pc2.upgrade() { - if let Err(err) = pc - .write_rtcp(&[Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: track.ssrc(), - })]) - .await - { - println!("write_rtcp err: {err}"); - } - } else { - break; - } - } - let _ = packets_tx2.send(rtp).await; - } else { - is_curr_track = false; - } - } - - println!( - "Track has ended, of type {}: {}", - track.payload_type(), - track.codec().capability.mime_type - ); - }); - - Box::pin(async {}) - })); - - let (connected_tx, mut connected_rx) = tokio::sync::mpsc::channel(1); - let (done_tx, mut done_rx) = tokio::sync::mpsc::channel(1); - - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected - peer_connection.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { - println!("Peer Connection State has changed: {s}"); - if s == RTCPeerConnectionState::Connected { - let _ = connected_tx.try_send(()); - } else if s == RTCPeerConnectionState::Failed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. - // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. - // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - let _ = done_tx.try_send(()); - } - Box::pin(async move {}) - })); - - // Create an answer - let answer = peer_connection.create_answer(None).await?; - - // Create channel that is blocked until ICE Gathering is complete - let mut gather_complete = peer_connection.gathering_complete_promise().await; - - // Sets the LocalDescription, and starts our UDP listeners - peer_connection.set_local_description(answer).await?; - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - let _ = gather_complete.recv().await; - - // Output the answer in base64 so we can paste it in browser - if let Some(local_desc) = peer_connection.local_description().await { - let json_str = serde_json::to_string(&local_desc)?; - let b64 = signal::encode(&json_str); - println!("{b64}"); - } else { - println!("generate local_description failed!"); - } - - // Asynchronously take all packets in the channel and write them out to our - // track - tokio::spawn(async move { - let mut curr_timestamp = 0; - let mut i = 0; - while let Some(mut packet) = packets_rx.recv().await { - // Timestamp on the packet is really a diff, so add it to current - curr_timestamp += packet.header.timestamp; - packet.header.timestamp = curr_timestamp; - // Keep an increasing sequence number - packet.header.sequence_number = i; - // Write out the packet, ignoring closed pipe if nobody is listening - if let Err(err) = output_track.write_rtp(&packet).await { - if Error::ErrClosedPipe == err { - // The peerConnection has been closed. - return; - } else { - panic!("{}", err); - } - } - i += 1; - } - }); - - // Wait for connection, then rotate the track every 5s - println!("Waiting for connection"); - tokio::select! { - _ = connected_rx.recv() =>{ - loop { - println!("Press ctrl-c to stop, or waiting 5 seconds then changing..."); - let timeout = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() => { - // We haven't gotten any tracks yet - if track_count.load(Ordering::SeqCst) == 0 { - continue; - } - - if curr_track.load(Ordering::SeqCst) == track_count.load(Ordering::SeqCst) - 1 { - curr_track.store(0, Ordering::SeqCst); - } else { - curr_track.fetch_add(1, Ordering::SeqCst); - } - println!( - "Switched to track {}", - curr_track.load(Ordering::SeqCst) + 1, - ); - } - _ = done_rx.recv() => { - println!("received done signal!"); - break; - } - _ = tokio::signal::ctrl_c() => { - println!(); - break; - } - }; - } - } - _ = done_rx.recv() => {} - }; - - peer_connection.close().await?; - - Ok(()) -} diff --git a/examples/examples/test-data/output.h264 b/examples/examples/test-data/output.h264 deleted file mode 100644 index 03555e80a889faaae8a37ab80380d30444bfae95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2566525 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

NJ3Xq7C_9dI~QWj;f(q<=HazgkCp0P+JW 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%0AjOnQ#b{;DXFNxqH{T(5~q+aY>!Q@9O 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%0AfwWd63Jj50NmM26f11eY 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

CEl=4E);7Yq8Q0d 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

tm3ibG?Qvxn(r9pdiXP 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 z

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

^}BHoE#4A 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

(>YImrvXf{I)g0aCOwfZTA)f?VRU~?3u zc~!RS3N{HBoOvt{p2A$5rG`*2aad)7ppR#SXLj8TV&hp3D+&D&Z2GMI%TC!yv$(O_ zSxTcF?NVK!(l14o?i<2p^zE-uY37L2)-KGb?%FSi>n~XB9GGvSxbv$%Jk{|?C;1+x zBq|H3hSlvjro#|k93XyNwmUh|aDL0`voVlM9t}^!K^;RH=aD^#hf|#va9KatH>zk-bCe&0f&Et ziceYGK_tn?*y<=XgDt$i?d_62M&hGKbBppaCDu7*z>Z>J8yV;36u$TWgOycP8QUcv zNz3CR`KLPr(B^`!W+;fS9ECYbdA*t4I0#C#6LRTZ`KZlosGG6#I)MH$7U7B&b0LL-20i*{)aRigj2GfYM{eLVn9Q(!?A$Z-QV2MDh$dnS zGpVBj30tk71bI@el+eBat-A-@bZTgs1Y||8x1%g~@1z4uS_XxZ(){@)Q3eSkE3Iob z=;@T(BzC3$-joCRO4{aQB@OWJ_97uvTFWEt(m)}Hu{reo?ZGfTwjVo;+IHt&=AKl| z>XHCht~pU2phlgmJmt46#wXDg7;r%I-l1OEfHWmqz~FmdK($0rrt+<#7fP1LX}

_8!fB^A(nBW)2;qYOX7awGWu@Uwny>2p-(d~)uUSX3^TXLWXHlHX{#58 z2#vDW*`Wz*|Kw0&1!+|!;d!L~DuM7$Dv2Kl1g-BTyQ1~>vTYdg#1Pd%Dyb?L)i3?knb zZA^5(22!bEnhLYC6lk}_%g==*3#7$(j90&EH9DEft#5CW#qYzw7wZp%gsNi*qP)sm zey&_0id@Wyi-Aoi#{F*rO6NcyN1#O?=q=OxOS!{@aBAdkZncB3_wx;c?zh^vF4&2u zPt4fw`&2#)xOF_vc^U5J1Wrp3U=WhSW8u3vv*Ok%>|L$UUH(nd)%&90>=>jwCnX^~>gmMDMr7?O`2DuVLJSXG(f;h9pE2U39=v5#~SP)K)qYV*{GJc z&P?p^o6-E@v~oPU*?s8?12c+7kZ0)`SQ?!|uAi<~r&Vb2huaNn@c}uR;xk4DkB95$ zEL@NLRAMEIM<@jO@&Vc!Qu$O$e|qUcfEwb~Yf8O_L7UQBXG+tR40I}U>s(>u9yiD9 za>`#>Mg7STHf)xetm)@5`%&>A5P|=1Srb4REUs^6RRNo9T^uUR1}()@rmP^_^otZH zftK@{6b>^XFjJJwC*c;-VX2;>EPx9Nm3#uP#VKL;8&m;f$0SXxHl0W zmia8^Qf}5LR#QlRC`97{AdmwTGkWpE1GQJ5M6ib;IY4dJj74(38vm6L#f_q-(E3tD z@UYoIQZ(t90sP(-Bir#|>I6E*Uz)Jt!`+dy4$fS8kTJ&+Px*l?D*FxJ1y5V6u`Nbv zPBmC%ndX~0B@=dV>2wpk6wk0I8ct8Jad$pfimzh8DS}Tm&~IQ|Bb&HAa|~-n0&(}a zYq*&;I=cxjrBq2m+^czMS5Ku>%XgmMr$Nr=H&Ori>~@Ajgvt^?%tO%!vz7R zqU4uO)0;zian==xCW}jiO$d5e;UWG^N$p417GCc!w>>p3G;yO~+mOKOW1d8Fpsb&*Vy@EU<}f>*zdz7XUl`VnV77q%~K$8JH*Dwj%GDlN+(s1t$eb zX*a*I$t>4tTf|O1JtL|15;;LWjJ{&>OheByTtcEu)lu>g=;xS4wY$rc7K5w^Z6JFw zbEH@}y#NnGB57d1&UN4dztGnx)Y3S^44X~J_@lD<^RcV5`LOmoP#$E+LWAtApW77z z?Tn~qx{Be_A9;(2xq_aqGFKD#rqnzz?(SZ^9SQdF{!=OkscKUuBmv=GBdAVM)j21dKT;f8MZOuSQ?%Xs$$b2qSe{aF+H( zBpJxS`UiUgqcv7n@0$a5Le+S?+cQ|7brwgN6D@G~;F2QhbhU?u62qzl!WZx#KeiZ-=~~F>{gX;EIvl?>xTvm)!i?i^y`IV?;_mp9u@IaM zAD@K$ibp!4ycU?uJV~LaPn2AtKTj#+pqpb=8l^?N0+3ADM+2$%o+qpgY&PrQMHZFX zI4BgCaVJ5L4ruIUAB#h3WZ&lRUrNgavOV~7!arEIGX$6BHClPC;!=dv;H0bwdaC-vfz6)cHII7 zA52uaR*k`3W5bzeR%@QEC|?DUv0i$A12F%%EF=G=X%*q1a1%~Q`}ACV9Cu(R#P5V~ zy%8ac9v2iyn6E#SIMv3xO;#h8 z_cb3XUqe;kP3&mdCk|?ocT`ANoN+jZD@e5y0_#7)ZSjFOTdZ0nVjd^w){HwMJ;dv0 zb-k(Gh}*?Y2|KG-Ps(m+o+#A^PK_3N-V5RB*%Ef85yg1JBjC>$hR4QRY3Pi=VO%x@ z?7H;;9s5d`8ax``P8d>+0LV&%Gbsw7k`+hKS)1RXa-;3ItA;$}i%f&i_kKH`VkGJD z4PJFw>51T5S!P>cUsYvd`lP5SNxew$Kb4o?{&kg>T50_mdoCG4WbPwws%-!ZA9)dv z$obA3-0dML^0nxZ3$z{c)4g`wNoa)85(u<~ga$;wZXG9=hC3ZxlBmU)HH;S#0!CgU zEpS3U<5!h#Q?_0LU5`I)A|zJ`Rl}9|ATfwKzommF;Tq&W#x1d{TrpqP+6*wJ7%eCI zvBZ>+foUk0bA=;+l$J_Fe)d&YwHW0!?7RRXqQ>W8il@h>cxF1(XBPL33bZ~zi^Pk; zJMa7hRhs15u5yD(oHkV^K6&62*QLEt@^yMojpSUKDweH2%# z4)pXG@Hle;Jt%{qvO#>Y_-PX6)<3%z;6xFroRhhgucN?Mnn!bt`in;v@QRICvC^B5 zy-vtJfVi#hfFL!(9k|B6-Yz!vAtc|rivO23@2vQ3BQ6)5U}-+%Sga0-VdH_Vo8L>v zM=NPQcG>pI3)gbiNzc_Wx}ccL2{I z*r!E|f>O4?^48iTs*Nsw<(q6TD{N0&8y$AxNR(4 z-h{JD;cA-(%gSYP1K@1nz&x@>F-d8nuN8eTT_5|kBAbYhkj(8A`G{fq>wNtCnPYho zTu)W%Zc`&?`AT+GA7xT?UJtz6gmku9cT^`+#pbFB&gUe4abN%Gkf!l6)hoH}#J5p` z&oeM}+}H>`T$?tGbk8d~d=D>VUDMe(5k}WH*P;Kt?P3)}WnHUxQwWl}kgu7=B zEHp7(7JlV^1jI=D38KmG?ah7Tcu*$9R$oj#mZoaH9r({Fhara+`>*#hkZ4h!I#zIv zz_sTWl=!5IB|a)qe%0zJYQC6y0wil#PjKGW5Z_tM8#8B~cVd3=VGz^jaG}IcF916a zwnEYed6u1u>z*!ux-XJt5j}LsLg$&@P=0BMosIeE=T2N)EWbV1vApLtY+(5%DTz{; zN{hv@>(9k`#3_C6Tr>jTWU9;b%7Od(mT5OmZ%`G?$(51!)cU>@A;xgBL8tjLG4|Cv z4lv9|lgDEZ+Lg(JcuElh_#=NV`e1hZH~Xnc zIO9Alm({}tj)C9KSK&xgo=<)79KDi>F_+r39EiF)iYoK(ffdbkSYFptd4nA8C0f5x z6fSe0yo6v11tx+s0_&7GyMv90hszB*2kp^a|_14Q;srMnk7XU4EjsKq1b za6tLWGlIh;Kin2#I~!Ls59ilTsTQ6=`f7hw5eMa8fTz~5-osI5ws5{Sv|$KyN**AO ze)J175+s@sn+tjHY230kKv#93D}@ZjzHCHfz)BNp5!dKYGm)5T)(BEZDnS@+n1M`; zR)f-JuMo@cy{!rA0{%HW0{MFw49SXz3V*=oOW;tz`AK9ej!yr;GWiS+bzy^=OOXHH z@Pw%&=~U}lRU+qiSvT`9X5^A)XopOxj4arp^CYGOIo0p`IDuZ})O5A^j)sWhTM>N; zA0n^nRS&^SEHIGAhDGouAl9rWC7KM@5XxY3sv}A;fhJCc3)^W>fPVVQJ<_{gu7`Y> z8+GpiREHN4S2&l%{Lrb%Iy31?5JngP=SvWhImdP01Ez{JxJ}3(b*U$fW&AK~$g^Ce zg`aW8Y%Iuz*Hf`KmpEzi1uZTfYF~@>8iIj97`@*y-So{@r?q!@)z$&*M^B97f{1G6 zmc7`RK@<7uaOm^iV{o{MM!c~y2LkYRShhILxg@sVcdmBza;Xa?Y$F(zZ=GE^b-Fl) zGf=-SYTewqe=|!c*oih7{U)848UvWMxKPX*$q>xD;JA+ViI?7)YbV_9iS(;2?`?vt zt`UnJiy>zY0+X*so%?dTGf|?n-F8&TWZ;J^D=Sj_ZPbb!1nRjfmwS;hu{4UT&VmIqX@^WA?g`N6riv zbBMNY0icPK&!Kq6yHtn2DmJ zet;Ww-Jzu=en+t3>O1jJa#Rw0NHm1xX)4KzvP?@@RZWUAQcoOqsN5OxH zejXdwqO19I?DvWMSe?3l=aG{tD4vi(GVjo4FZO#G9TV`Dt%K_9qY# zDd`x+tA0d~&ZXCf#{$NH)b{dNzk9S5=swf3H#^u8Q6frcaC zsff3(1os9vV~aaapbVreG-@Vf9%><>yFsGh-8oXTm{YPZiydaLXYlJ3EwyYzQfH-6 zl^YM2T?aL8!4+-7?rD6-vOdjBpohC}OL3qD_AKt&q&_L{;|GT|T>P{5;%AG{I$h`o zcV`cv#yy}j97eSHKX#G|MTxyi7F!bu+wcF0YN1W~snE2+Z0x@nh?gmydFdOo{T`lM zSj^fmQyn}Q5l^QEpakSWl2JDoKPV<|M6o=Sl==EX5!@9FUHmCdT(n5WUYb!=88f2@ zGz|#m5k_0by1*VYzG&HU{0t-*m}I;^+^XSO*Y>v8p0n<`#AwO`!UyC?E6>&GgOw6t z09X5FI$OB3p;8=BliKtB29&YOjf@)Id_EyJNcJkr&As-pv@qgAW_WK&^h3yI>Jwv5 z=aBtS9rcPoZg!}Vq)1af{S~%LgBECb9kkG=ixA1K*ly=8<;rvrHtH572GsUcZdZ(W zQjEQ#`&nYa00001L7K97L&=oDf`7eTDiwcHt)=YG>@H1Y>tid{fYO;G9dZ`EV#wE_ z={|Qd#G<`&TIwj^IdcO?sIG1Gqk*-tOS%SZRWI?}9ao zTGK~~RX^+jk&UR69MttX&5Z?s5;jG#nEVXh+WzL(0)}SSC_h9QYkR4=yOUUp;ZpwZ zG+9n_2(yDjyrC+@{F`MQ6teza;vp9@T8 z1c)NW*TJJ>G4*b$JaBs}veIDEAf?IW-RQM^L|fp&5m!)FvgtZ3+OcV*-jUG4Wj$0r z@@f3orpyyd!Ob#7nJ@AA^&-Mt_hw<0UPZ>Uj6oSV6l{5xs-vJb6!C^So>U!>&o$er z+pW?aPes{eAk>sHIv5Q`KeY(lKX&;qiM@%qPR`o1VGkf~IAxhJ{$~ZagR?!3#qv!H z(6e6+>MtGV!HY@!nmkZ z=GAvPRig`N!8YcxbIhUMgK1#h*+s*XB$!wsxMvlzC3Yl?SO`6KsQwns4$lGzHg~RH z_~mwIbq$*I+kRl-yQ6?LZ|_IigOe7I3@DC`atxtaJpnkx3?c?4o^}5z!S8wWqERb%5|r+~7W!$BEj zYYg*bav-}g)VZiT7!f{gK(zzBKll3iQXyN2(sfGRY)WX0XGT zJHGM`{(t>nQzZ$sr_wsQzU-N1uYJdx4ddUYpu}6@L_{)7>fc`?QcFr1zGnt>C<8H3 znv^)QJ*C zp=o~#P^oY4u4^6}1zJ)3mCr&iKLO$IEMBqg0(7k;ZD2iFb&Ho4hWmPpdh00UVCyqN%C=QZqyq~_>H3?$%D3n6aqt99-F*MyCm|n zmA@KTTb*&c#NHn2V0?*j_#Eu{5{!@^wz+1spN~4HA;14aTN1EFJpOpx=2RiN&~DBF zdb((_$qXU0M#1QoNpli;jwi~rv|jabTEqUMbQdYL!sTs&r~ck}BzEkLW8T)$b81TW z%a%dU?8q36!Dx=^F+W1_q)PaZ^qi-_UN!jt%e{ahZkKJ~8+bAOACmzwVV|_mrp~Dt75?H2azp`VoE=_0FdU&jo(@Mh;!FE; z$x%}UdY8VRNm60o)hVCQcEjkJJAp&mv>%U7*0&5#B#r%XZXX`}$Havu$%+l|F#^Q3 z{k8)RSGlB@HjSJqxBn{l9a`=jRv1wvUhv+lM&jJ)wPW)U63nw)b z5DSb?NCa4>?^LMEJr!R?Laj+_G{Dw|<#}|wzj^EMhU1K9Jfs#Mv?BFmkGyZnmgg1g zzyAY6Ppj(o3mYv9DD(6Dm~y4Qaq&`MoNP_XyvX4-NMde)dog&WIO%pN9cTfLC#qn6FGZAaX^$K64rXxiGZou} z2Lx)?Zw(_Ju>zq$@v(GFps^~j7fdY8OdKXHx$TaqADFN7P;h&E$yH|_RIGtsi<35H zn}?#F-#rG^KlhKOCK9M5_@|g9rT{sxCKm??3&=}@Ga?$VPs&hO*67upRR?cc-T|Lm zyx9O-YpAy^>0SGYQuy3Dc*&Gai ziM<-uqstyySX`GP@HG05#o#uS4jjQ3i#UTqo0)+I*qUN3htX)HleA1)4`_tV6dtX~ z@r$tGOY_w*e|J;i8sHxu4^-iI(dLv0?ix;tALY6$;D$1!;%v63l@JQL z6?mmcXQs5E)#kme-}r>MQ{*$crn|3+Xu-MI!PpiXHgCk~y6vvyz9w1p~Q;xIY5x4{Q5i1R8)(}Nv4=^IAXn#>Z&WppB! zV8*ljF~KlKP6iCJyS_60DWDu87!5J1XMztu?=Keuyc6#llRaXArU(b!RvH2b8+3=StbagNQKfsjOwmQ44Po4O*!!oR;Ih_6xa}P*olrBxiKSyI+u!y1VpHS7 zxcGe;{?(p8XIy4yb2hbpW!M3<%B_8a3}POb0aTUcrl=Yl>jzey{bD~c9ebk4kGu%YYk<}i)6pyEqYXIJz`3Eb10hXc;JURLW;vQ z)`@$E!dO!pIXSH{J?EqFpnmz#zIJS1>3vs=`MLdxNL?HYXCDWyj7hw4Tw~$V4*<1z zwJ4dJin-bSd?4>kYU8e;XWJ3oLD@&~Zs%4YHk%+O;2GaB~J!-npiP zrOYCrc2#@RXy^AlJJ)%kX!PgX#4?H|wK=+^44#anr!vr%BuQJicTzD8Sjd>&{NO0_*HxJqe=5K;X*2$f=JC!naZG2}3j7MfJnqeyZ zIV!ECBmS>%?8`rM(Q>D7koIs~K{{sKkS0ksbrKYr6C=Yo5_}uTtNR zC8}8Vk0!j0(mmb)$P>aU!742&*v8RLZWP^Z{NSIE*UocXG8y_%M{Q2z3XMn8dO=TG zltso1Ca4TLjiU9^@22CJLTW#gnMF-~ImEe7{V z*KapnHd$dLMXY;+@h+T52QQ~3T&r3e2EkC{YWsj;EQnmqJch2A?8Iqf#p zWEtQRx*q0G2NXT_z}Q~3Zw?U6XySnERZuP?nHJzYMD*CUIhIdXbc~Ls_)zZh3ON)m zhJa);YAWiHkKS&>7U7Gh3|PfagP^J`R3@V0^ zPhcT{rYCSpnS0X!l!?=%W$2FetV2L3i?z1DX)}yTqLW$pmPWp%b0QR3!DuRPz>{~x z>cOWpsixSsm_~WJ4y-qM{TTYs>!-K5g!cKvW0F9ZZ|WQToae}oztGx(vNNCh6WN<0 z2Huw}dCWzJLUR`l&G2jv@hkMVbv9x~*Y3tEWgs-ac;SDdlEtTbV5rrhmj6`Aj%Uav zDv8*f_e-jKex7EpC`8MlI*?~|mO&qERR}sD@y{Hq&^Ffq`&zT*_ zTU$xFDC9Ti`lvA5yJ{m)vFF1uyiI2LCp+!3JQfdQLJ!Frn%NN_Gu(;K3ySlC8A*$T zhI^rq$s2AMh4kMA-LQO;x8k_9n{m!NZO3Ks1vM>Ri>*dWaopi02WLx=GT3B*XM_fd(oRo$_QtAc(}W}tz7@Gg)-(aEs_{u$UbgY{sz z9B#-erUUJ0=*ZTivlj3Uu2gVd;4Y9>>r;v`Yv7HDeCWl0RtIVa0{FVf|a&NQ=n`MBF|)lLH4J3U+J>rDyF z&J}MNJ&Ncj41E+IZJ6=ENm~In10k(gvytUDhoWPR{i<%@Tqkz21Sw+mih@RvOjAh3_p>yU;AxKZi zRDV?)c87?73^xVjPG$o9kf!k{ws-{qOhB{0o$CTy9P6zftLIbKS@^K36a1&j(Al!3 z4LokxAS#4oxwTe-B`+|*^#DZvz~vhrboJ@uOi=39{!3`DFyJtf!VqExIA|6j2%war zvDhdw@E%sxZhxOq;$9xpOamzP#1bTv^g41SzgU^(DXxGk&px&{rdMV<<7UP<9=!sJ z3XhFjn&3qD&`4O^a!;)0x3LiYT@u`tdX~&2*SLNRv9!jbcw@X#f`MxD6KdJ_j8;_* z4iRD1JjZE1`K1;G1ebk%G+)%VnKkzB;K}}Ku>lbNPiGM}s#E1WF;7`p?F7#nZH~?a zxO>He=5mve`CbE!qs**-mVg%XWhD-QzHwLT_sMJKA zdR7mE$s)k_sA|B~rckjS}#!vYy!4^cbaI@FCUjs&e?9XtR9wz%b(c3L7 zL=X$eu9_DSpk(140mrFq7{yX|k^a@wCra(tPyB&zS6^s7Z0YquDu4yR+F{&1!(gH(Gu-D{}w^0rpG>8E3UP?KSTGOfy?Hb4f-&FUOwaC zS#GSMo{j;XXBfwM5)8o2Z?wL(ivE+w51dx@+2yz5m3FxIh+$Y2ZpW4>l;O&cU(0~| z^aVricXffh$$@59mEufb{Co-ur6~fRGCIdSs?~UHX>-wtik7kP7LS=%-Js}xJMer2nP=v(!7vC+1ybX)oO2W1 zx~-KY%wH>AFkl@2BXUvNAfVGvNOs zX6ed+>fi&N(Ajm==mis-v12N&z-h`EN?>z`kY{~&pj0a+Y~<(sC9W`_goL6ke3b5&552K>Nr-ys@%yV2zf zbsIWlUwlLpEBnhzGm6tF<5<=2)Nt72^hL-O%-PSQ{u`JMhrs;8*Nn1t8ZX-+H;JH} zGRCik1j=9VNcSzI8WGABLBx@@7AyLa?+rTga#T%T@LaS{{$!04zUj;7@!GSLl{lGG z61s&;vyawjAMPhzfmjkytDV;;%Qkvc^O~emO~A+pkEpzY8ZKH#Ad!Z2o-vb$G6#AW zDCRn3w&4AGG<~RvyvMHj_j6{JT9=@81A*r(&1XO;zaq{DdZvC}{^x&NCUJgk2P-FC+edXWBX(w=aSctf9O2VErzBYeFk0 z-&`f%Fkcx@YWb=#F(ui}tm8!r!0eoagzKrd(W_=7S`a(>xA3RxfZj&vxKxlvXa4Q* z^l3Yprv*eeFC{hx?1rJ&0XMg*c2}d53*EgwxT=y(54OFW=kHbBywDJ83RB z@v=>l@LW22A#eB(l#iFaatnC|cLuIW0T+<^>#to_YmI6c1vsB#X8bH>T!uI!HR5?t zc$LIbnd_D)S1hCSn6Wx?AHn*D(RKLfbF0QBs34DHsEm4U&e4O;$+>mQ&t@2{#SWOp zdEMzHRd8=*20=h2&|Y;0f}r4xL94_s#ReD0yy%W2>r0=?}3za5IPwVmy)&02xL z=u?0nU-pk*@jVs*KqxNL^nShMmrSB3h>Hi8aKlq?8qHHix4McjVvtE@Vyv>HccH7P z3y8udsWWKW*hiB{mdB^lOJ`xNq~duwR=>N@myAzpV^-pbSYSs{&?!=0q_Rs}-PyeA zz2ns>8>8`*?a>iGm_yBU5Co2X>{Dj)cFY(Nv<4br$-@K*%e5?rQ=q200>>*R(00}~ zbR5zYz@Yi$En zsjAimScW*om`XTx;rVx1`&i-?Gr%IfcpUm_U0x5QDx*l_F+TDCP82wc5yTSV)9dMwW9rAK3hn^Qy#;q)4>Cc5|EJ5`%xa?nt1 zg3&(MDpX{-Pye)@5h{{Cgk03;qw8la@;O>CZw$yC;(vx#)*c68DH5L`#2U=R6#gna z={$?`b7zBk*9y^L%gYW;UjZ@-2!b@s^A*Y#ENI`1;`aWu%=r>yMX}I+*$YNrg z$(7XJBxmQ@!|q&znZ$-7RrE{%VvI?q?EP`K?PwNzONnOCwH7L;HfDucHXNQI*bBIT zvkkq~-3N(Ie~lukCS+XbObA`Yn+Kte3v_H%@WRl=u}+o4wR>;V#MGp*%>RA{dG%F#MrXe&2>i}XJG}h>LA*L!S zt(zoKa2?K2kDr@T_0KH!y6Ve4BK#bBSHreD4(R7Ml zK%>YrzI_7>+L9+8mwD&l+kK|N2dzRFS|=i+#Y{CV<5VGu0PT^Fr3%G`@ z9m|QS>}H;7YAZ46P=GE{ zwkeDCX~T4seR11ylszP}^?R3vy}_RiIkiU9m6sAMYH7I}ug6j+wtiw#>HdT+JMLIp zhL0keDp*`hB<7ZSsxW@03sWNU}T28$+Ii+qT z=7mRs*{<&Boe3v>TQ89Xn;A&BWK2!cjYCWy4PA(&?aJxctTE@dC4K8-$4{*iP-pEwFyjz43hI@LYzW?!kaW8wkm8|scBJVso=ox z2pUpsRj1znb`u+dhS3V?^Obw0Ap@CkI8dx`k6U;gYTNC30%`wk4zCbg^jQnwXqg8q zK8Pl7muEd!YNJNffz+0u?+aW!Z3sP^qf1^NR#fUpK9uMN{VrMNddyS%B>1vsypnOI z0I5f0MsHiJ=6K=Y-sSOzCQj+xm4OCl2u92GO|!~(DWRVsZDNW`Kk#!R zKS_(f7LuD5D4@sIUzlof;UT*q+1~B2=ZtdB=M+lY`H#vY5GH>!Qn;z3sX+wg#qR9jyGI-y2XO%J8eL%EZwMbw{B#i|-f;qUD25$mV z#gp9RtwMUtVWB$dbOg_b@j_%;^n)oR1d_~# zuSJN{r&^*Ic}?+4+e4^uF-W*(z&@{e7ON}N320gSyrHC%Re~%MAbzyA>Zc=@U_hOv zkY(F&m9dn|Xj%9qnK5Ikv71Zhs3J{lKv}ggQ69H9k4?wWlM+FwZVPz#{e4fjw9RR7 z^^2e5hFRu;FH7%OSW$*(LqdlvPCcKPL-D{NP-EkN+u|)Nb|ZgEsT!WGTtC0SZ`R=A z39#5HWQk>%Z9vpw@xa1IBQvMuoGZ|kisqGH>cidGoq+%V00BXo(s)D3l)!?2y<6@< zjn$}HFu2NO0aRE4(}E=CaGb&b=g4`l6Si-Wo6Qm%blXI(zTa%5>mr*Z_zdOR!RnVh zd-23!6|xrKAv#_BN>_~^$BM{HRKw26c{J^Q`VZE9&-4qdgN^_xDLan~!;)waLmR=L z^oWJM=t^3KEiJ05yTILL`n1sOjOvQ0l zRdQr+`_WK6PuJeGcC`ZCog!^zWE$ibwTW*pp|_y=pGXQg-BxWU#kit4jF7%x3hJi( zZEDN|7Uc|%d^Bx|fr?g9D9}`nH?P-;0jZUBbt`@vSYRd}m?$#_6xvZQ2Rwlpxes@ckYEeLt)T@ai1a`X)U6wxuWLtopd4T zUjsF>BHD{BO$MqIZy6xz{m&w%(lE)I(+E1yDC@gnFf)ufsqa+&vljglhA*hVSC7{{ zs&bea39(=vFte#afK5lI!7q>0X`+Zp_-FesVG}sG!&scWdqZ+x=<`w2k6>*}ZjQ7% z>#~huM|7uR+O4oEMk8^OgRMrF)ywhC*h!4#t3p(K?8&Td8wnt9m zlE?i$8z}~6KdWx4u>te`3?q{Zs;J3ID-BWev)O?NihDT@o|tABI!6#U_fM*i{aN#{ znZWx{k|AIA9_(loy1X8GzDI49a^+ygoQF^#y=>Z(8rDL+GIMBulGnp)=8M_D&Y@Au z$i2!8>r3zSMh%41LcL=BJJ`hx3a(9Fj~Syu$Z8|&y*CHCS_mm^ts)V zGJtHU82`4$HnQRH%+*OZ5`PPb-u>_9(A_>0mToeNZ3D2Ivp&d|ob_xrOvU5-1qyk7 zOj&EBVb$Hjxf+dDdjt!1qBEX7s9x5G`9ftC*cWH9H%Z`1zK+$e6Y5NpPXYhEmgR~6 z4J=rF&hyEv`&XTj5=}W{Sv|O8C)>z{8wq|CndR^yz6eAc?c(UR`^-UJ(FceYDZ|u} z*@ob9Ku@xM9b~`|z7o@Cfkm16tnF3RLkSPPzyrnOHl@=m`w(S6AOUM|1!gS{J4UDr z(1qfSa@R3?6gtYk#XH67!ugVrTHvJfV=CYrZwFF|CZd$Y3gVPACk)veSRp?Wx`Kjr z`P@qm$*KzYwLLO5z$f|q5v%Zgv+#-tivI)|o;n6I;Hq6t(6N8j|J z$%(qPino(_fV+yPsK|Ue5X82Hc!t&Boq=hnXYamX)wzqJ(mL(^jC4DCPjahS6JZ_D zTY=8eBi;n^7=P#Ii@nAFmvsYp?R4U*{Trib`29fE%`?kmlJHcoZdVG_w7nB1BF^)? zO@XGG<6$qtmS;JR-F?seOX&E!i`8ohAXJfPvO=A_ib}*Z51ah4N=uf#`9$8xW!_9;HrGqbhoHkXbI}JPcV))cOwNXGTAGw>FRCsvg!IG?a@c zPp)48IS*`V%aWqQKndu%F{zn8RTR7$xblkVxyhA7G7c+s%cgvP-iZINR*GBY8@E!Q zd=s_=hXSeBM)B4wQC7sU_y?%Bq+GwSD>m2o*NBpg4w^-}FXseuW<6c1; zsgxSBt_{pOEnVx2q8bvA@u^xNMJY3 zWW#I~vm8KOC~L4ZkNXDZ)Ny^!2-y8i22m&fu|kC{rL3}Ej+fZMvW_Xtaa2va-QyNB zq(npVa8Yrca!??oZl(gq9j$ZS0%_IbYK5batXoj{ktq^!4fiKCK!c*0kP#DYh#xRP{?6W ziw!vk-1*q^|Jv3Dr%_5x)B`VLCyzx_G9QG#h=}WS;HgYR(OQ;MOQlJyq0y4N^FxN1 zcG1>4wPu4GLLH@R<=@86yo=rjuC8V`-_R-4*k#xnq8EhI2)-QA0EW;CU-?TOOIOzP ziA(7BrZEVQyj3d|_4h3|q4dB%JTKio#Wx-Cw2=O)7}Tvtd~v$xG|+CJwFAfQ_`tq6 zAKs%d^dpM1LP+cRJ+PSLLP9$pzkvj`+PU9&e?MfEnUid9pyfF(K_ui8d*;gtSi`bngkd1R+k524KV&MMQyK()o1j zEp({m%dhP*`zsbdcWc^W?NZS=3;dJ9CYyx-GcqAus>1aufSheDCkYjNg#|at#ej5VZe%vWm)28FcJ$@ny&WtE%MxbJL4r+;;^eU^R`O_v*FQzpeJpBO z-aiGae?`e|s;y6uJRdEW?R7Z-_6@`X+#9@?8y?;On{;9}39OTJD7dX#3n7Qa!LH@< z{Z#p6b+eJ6aV)@?&E4}*&)@+J?v}Ter)oq!NkT=SUofhMh2*vyWL8Bn{5F{3}3U*ps@ppQe#Rx<$5M29IL4(uK4_p;(cb$ll6sE@hX z*D1EvJaOM;_Xe{vVF>6|#T&q5Ux~sl<7{1)e;!)ed@pQ9-esU@F}X_N(fZHnvtCD1 z)iR#GJe^pq$u~Zu9wUVsh{Fyuo9d+Wss)$ZdZRTj5gq`l%FudrX!g<@dF=W%k^hPL zv3Q^X*rs!M3Hi^Q1_Rg6M4-rlY9DNZrDRf5^&&T)=3BtEmtb?GcY`X-{4tleP)8|Y zew8m+pnpq1mjUcN&b(Kk>|QTojZi#jr|&7vsQ$mVjB4&uE5`K{j3G0bRO)}p+2Jof z*=&}`g{`6gs+h(&hv2yRSF6t>KPvKOk0~)_$DchhaycmnDv9@jOU`R- zS~7s+HJKZ-ln+Bs-iR$&Qt)a{r;Q*2rajH`@!$&_kP%EbE0p;HO@vLrH}V(S$&+WQ zCpCK^Beb_h!$Z_O_$(YP(+yC1U`Oz8y07P;@|%Hx>zzOHD6KFXCV(_g7*ZU?v6O93 z?dX*=8Fk#HL)N+x5}4UFcvPF_E|yg(XCMfNZ_S&KlvUBfGF3iCV`Pc*2MD-_CR{HRBgdWxBtg<-SVW8pPZ>O$ZQ7jT!P$I> z$NPmKw~?RAN6nbUVz<5-dcV7>1Ba+vGE&xAk^^)I34GHV3-y8KK`eKGz8ZzNL^0cr;oTVI8-F9$<hf$Ch>bLDlGQ4e0k?7lIwqH!;sr{K9yK06@uyMmB}m&!dD96g6X zNh2gBHNdBm?N$AO3r4Lsa$*IiPEJ4QZKIQzTeeWuGn@fB^mgt{6=UVdc1rlxD2wMn zDxR_^#=0Q|ntoN*d8dV$n_V`}qwO-!pMWLH>+}29qi?9&I@2coa#?V&`GQu}uZF%= ziPuqM(Axziu-Nu9lB&_)$HFk_3BW zFj1W@nSX0+Yqb9Dwp*EwOY*o7#JUXvL&Rpnu~ZFJGISa_{EIfXTN`#lDc(o0D6*z1 zcBTzxdzXGic;WJ6l-c!^b}Ki(aROk8k%PmgbRU->pa1HTZjgpOgVNcKDfA&ySv_dY zPUjt{OlIX)P-&ieTP|_9q_oZ=5cQnZNHVnmXd_!ihi zeS(h3$r@4o-?&P?HHdT5t*s!gDP9hy-ZtC3L7`<=p=74cv(|E|ae8L@U?mbMB zAkU9!ha1N1J5oiAvKT_&JE91ba5WT+DPFxlQ{sb`za$O6Y=<<7UY=GRM^kkFdMQl> zpBH^a-KNoa7IQJ6wN*#Ijeuhhm2ggczXB_dGGKAh*L~;I3g|>~rupmbrI?1ojJpe9(x6(~@Va z;7FCWUoAdloQbR|Qnn1NtP*{UDVjuB&gz2xjDlEDAwCNRo9C|?_xusJ_51>}N~F1U z&ek&>w@KtMDMqXouk~n#fCuMH%c(!zv}Piqt>^4HQ~8+{bBB@#q$z^J#;fHu*)|CTA4T0U(+{8-lo zJE{Pk8|7gpJhcKY3Owro+gr;GKB)68fRW|Xd}a7+9^+xkdD7RT)6!>^<&H=0hkAI$ zrWW%w_6q6V@?Y@{3C-G>)rv?a*aWkw(RgYG52N46)dw4XG$JssANQ|>sy!LVp5H>y zAG#q)qad)7qc=fnXveP`gmRVqhd{zRjg2eVHTf`(q&7wL01ZcC_~}wohQ^3clClZO zaXc5(#qPglqP-MtcwO0GjkgiroJ3uNApveAv~#zg)6SycZ3x8&X|J6!1!gECLRpqw z$_x77*_y>)wGIcQkSDL!_>{*QSR`s55vLP&1x@`IFB+|h)Ge7-H`ToDB@rO{?4w!( ztAkXoGfwiPy#rsKhmEp6tslvUO5g>p6bNV>js7)@z}ot|Zu2#fS+#R(4y>>TVy z>Wv`qwc}BbhUPD%is?Q_W!>qQp*QisPpVFtu6Bd6+v@3_3two*O{lfkCR{YQ84OQn zS@fgs;|T2^r~HJm@Bbn(!3j*f5sLx8MW~v#pkk#ep(%T!)6Rt!rcLRNZeC5vbcXCV zpSd(VL+#&wFu!G3mO`1Qgl@04&og^$C(U>7P!=Ji> zi$AXX^(2Tr12#0*2w7XiD4uF0dsAo`js&Q~`mWG`*lBKdjvBQ@YX(n=!MeIimU{I=E6SV=EeUJXJmdFPc6(wS zO`t3<)dZl33G=)3==^bIRi1Hs;*mGx)jB+avAsZ236guHCAogkx7a$?J>kt~q*YEg z0`A-|qAH?Vro1+c*3lt&H0G}*YaN10a8&j$qBP{+pLtDX_}QF3Tzt(aT(nO1SfOsN zCF?h6FMVz6vDn<^hz9TU%!NG@Po)PRxIiRV;MA+(l0=Bn{v^swLs@kp*Wrh9w940E z3LGOt6e#RkFEc{-Le~nEIVKavSfHn)yhw?6oDM&dQ?BYuZ$P^=$h-;ck8EtmWT}(D z76+3SHx^9cPhAS@JVgQ(1Tv3!)r&R=`4S#lWEJ?2IM9@gY9JcC4pA-mg`e4Nnyst> zj1ZGU$aeeQU|MC23`h_3qQE=C2(UhQZnlyi+wEm`^0hZsxzV_XH^r&f49tmY&^Mk8 zF0R|DdawE@tp~5A+c`Dy3Lop74tL0Ok#rIFux@^x=0!wQmzlwS=0&IvlMT7-RXH)i zzZaCtP?-gOFSu=$S8D+ERUXz4-?efR7(d4RD}FL@NqPNC4uIZAVfcPmUS_i{4hIWa zeV6alP~ZNq!OvLdDbah)qICTg(9xd?)d0(i= zohje+T0&Dz;|o8fy=Q>%I;90h#zs$G!r)1H%;d+$K`V20 z%a<014#G5`!lh?WF6a59OIoP)wdzOge-pFNGb3*5?oQprXsvtD;ciIewLME6X8lBY zSZ)LClJnhb!tLztxbw@`ob$W8rFn!xw0p`Gzl}=83`%W&C1|;iKw>?WtnCws%Tn)& zeoz1OHN%B-7TlM2{DLu?x{T^*YU1)-%uX)}EqhBYM;*fC@c`LZr@KqC8sMg<^%3A? zAwew)f2(uAM|nw#1FDv@9h1kxp3c>dihhMG4TB$k+)MC(ea? zgED}ZzkVja>{TQY{Qjp)Q&Whlj^wO(r~h`^k}xTJwxMGTp>rU>WNPox*EY zL}#-`^Gwc|(gIm@i^RQD2_VnHz;w|R=|is~Bme!)7=!qi1aAR=z_xSmbdp_%JTU#T z0>H9;ZFe74jzC6qsAk>0_XZ-3Jmekioy00#A3aiS+{7V+L2yOR-z)zH^wU zdiV0EG4!$dUR1K>YYiyYJ3plCgMs#dLFJ3k*GJp(N{*W_X>-Jr{ZI3$CT)f?Z;BwN z2v$UcgROY0yYrSsVgY@f+Z`$9bOc1Jazg*Uz%vwK#{q7qP#e(_2)8y=9qvp)1XAtQ zVC9F~PLN+VOrk_lD)^K@v}O5o^KpUlv+#5?gFp?S!q8hm6neR!lJs~wnQl#(;BzS% z5I@9Ly@UMd;6yA2z!v}4Nb*rrc?!#DRqy@RdU1mCYid?GeIJ`$X1g~2m^H02dvV7b zfV*v(@_!#MwaYW7mK^#yN5`!}rgrw|^t42LBwDBKkXbwP)1uYnrbENhCfs@fwEXm3Rq4wC2&}(zs z5JqGn9pibZ=9I?f2Z zwK4Aw&=%$4TN|2JM?6G1vxRG_viN;NEV2I(07d?Pd0@{5pfr{H5pkoJKcZWU?5izJ zX0CYkQiTXZGgNW1&o1VM+8;jQv8>bDy#Xvm5syGRgj1Zx32>_rD2GQ+3 zJ`q0&#WeU|Aolw(<&2cjuo9= zfuMZMHy&4UXz(FHA+LN)8)3Z+cL}Jd8=T0h(IbCEiia;ic;F`4G0dTB82(9|-Y7uw zHjsn+3D;FW_EIkr0V zjQzvV+bOOZR@|X1L7*({h-r0OPT%pD&F}oME*s0_Rtf zr%7E&Uk=0&%AFr8ju_um+L^RHwP`5t4?{A!5;bYb`JdUn)G|M0`=b?4P;>XR&gv4J z^;21RPNvh-&@D5M0-J*}yg(?P4KtXeZoyI(MSejC2G)gK)hD?2VOr3(V|et^oZo*C zA~wA6Jg#eb``>Z&Ju_Xpv}kjo1cojXKCd%2lC(vYDronTU)kpD8@X~mRd}MAzn{*T z3$blo<$qCdDia2E_v_ugYLCoYO;cLCWV7>qLp8ds=+p^Kc@H# zJ}P~Pyc~-<`Y@oXyr>mJ?!i|e(SaUF&Owu-|EV$LaBjMNE|MGg5?A80LF%b&#Sy8T2 z4Ge`U1f=y8IR*fRrK}oG!bY>JM&_L?dl4kM$_~~*FtX|*8u2VoM{GZ6teiI)7EVje z8TIwCcrFs$gepayWd$YV?f?XQLm#oLe1F)4U>WaUWj?hgenuogPa{p@!(^lbm%vf@ zV`lj9WW@b%5l#C|2#WIqHH!!|qJz?)T^RgT{VYDojV>GHq7-D<#T^srnFPFL2kh*Q z4RJmX1H`g=7wVRkSMH*9=wQCp!}~ebYz-+CXba>aEI(Z!?aU+C zE9qw@cd`^=BJ-#=@+@!{s0|w|TL`M6JOOeWSuccqMrWMxBcU$SQ-alVwj1WOgUDXg zHa*efruEyuSHv<3W7^5B-xLZdKI}rAW`81k{Rpvk71lV_>vlV$j#)4gPb}dh^r-s& zIL!X>{#gxdB_$W2zoEW2kjbZ;fFfrR%bj}I@m_A;U^ku%erRetSTruG1?P=wRE0tR zot6GwjnTSIJdGYZi8gWQ3d*$3wb06vDk?_Fkd7$G(Ke;gMKe&e(nLU_uS&Y)_bB5T zcmJuoO!hG>i_D?id4eQUaa}h0)t%!XRRN-+^ioTf&Dp&muItDp4NZO z=Mar{hPQjyXW2-H8ohzoaEhJBT!{^)?&-`V;*}b|%Ey$;g(h+pdN& zd2f`>{qq5ew6peuSo{*%(ek-|<#vkXmMu9%W4r#0ycUF}tSfHF?E2QG>nK~cEFjztAuyrabf$VJ2QIE`a27xxol$-t`z8^7PIYqP_S_mMWUfdyZm0sQ* z6ir*po)DyvztgAK4XrY*`f%j{Os*jyim5!V zCq&U3FMQ!6WFB9tDY^EqQ)@G>`>{Evb7DmRH#v5s$h}dQ-ULVq9&roXW8t&eRsX+t zP_HAMVHCT6$ZX&6TG`6^FDundY>oq7C8|{mU02;^j($-C2I{Hg@45}=%I>hd=UXVf zkPzjdobo}Z$WP0lmgIEM{lmu zdLP9(FXCrPzp#bo2&|By6K^8hMLRA>ieIKZFJM9}RjwTu2(Nfwo{qtoI={^hFvJ^z zEhoruROp@ua5q?DpGc-<%1C3im$M=yQK#E-rNGlP-w+yh3Z631&c6T zm9i>7nZasHco$;2uq+y27<#L0DndfJ zvR2Y8SbZ#3%QY+KQ*5Msfb%)fE>%+kWrR1zH+(=}df7L(M1qpLx6PSl8MZvIbw7^u zJc@kRN%r!GeKNYE>ne*QSe(>>8At0Wd%e4)-7(m)V#Ec0{tYFohL=Y&k~^lAdSQwP8{v{JJZ z=?8nT260G|wy(XsIG=N{2Q*8@K*KNqNFPxv-$<{R4h#FF*t}q}fCHjiKZgZ;MAA0iP zvoXp8j|n4!`)e1=4=%WEc4Mc!nIh8%qF|lFrS{!@jv(bnGZ9Sq#^w(}vMymHUg-mo ziu&Gv-74=fWi4joZ5fzJC!h|lf6UI>`0#Y|n7{pM64cv)b2I4yIPE5^M94fCOl$bn z1ugJrB9g`ztf(~4!2!NHh(dc3O&Vh4^2Eiq3)EGV(07xu`S<&i-d%tfK5zIspEpks+%+&W{Bw#)LoUzHyCwE%j-Ou=gJ3Y@Xwki7Jx;zsOsP&M}7-5QZ5X!jF;aOlg>YkCLVNSmUBHK;~pB%^m)Le6qnu zys$L3V^7pTo8_zLmstf}2dsbg!d@Kq!Xc%SX%7*`{)!se@F*dLc{3Fs5K->mDbCQ= zYnncVhaL-66?uQqQCM@A< z7vr0=XJ^Aoe?{1m$jc}xQ3P9v8^$IE5G&7r*z2>c_@O6TweJ`|ZySEdCg^M#^_7%U zrKYQpLXzR%Y|`)Twi_{{VzNQ}vKPO$Yj_CVY~AfM!l4}HWaHWotL+Zi4tDat zlErSP49;!oGInK@fwv8I%h1X({!3LiPEsAJJF6Zwk6lTgX_3VlOgx$+n()9o?`atr zavhtA!zB9p%cV-i_?f;R!_BoNvgW9;Dw|z-l00`Fx*Fxcu}8CgGf1YW4(--S&ciC< z@LchJ0AE0$zuR=CFH@qno6^-=iV9P?OJkrYI2bDYh}}91d+edh5ty zph~}6>o+Lx=g4K4W2pKi$XC5xszauptxW;9ujk>Fsnr=K71W(#3vg$V)&=a{S6}W7 zq09a2R_I`+UOlaU6tef#Ic~VhiC5K1nOwtWhA?ku0N_s>``vXin0YDOtK+;9Y}z8m zVh=cY64g0|X=H8W{RK)g8r5l5g%a6Ypma74@5e#cDU$6V?Z$PU`S|c`ygoi?#11lj za~cv^MC$6Thm^kTis}!ipuo<5ZEMsijfVNu<`2jnsCrF|wn}NX^jp)fsv$75r(!?P zG0D+!5T5 zFZX>V4%9T2WIFNYsEzWvii*J=83xnRan|6zaDbH*SeciIiVA;vK!bL-pxtoS$03{C z7uO3qz2w=(G8f^85?$rv8z$oP$cjJhM`BcYb!|%Mt}CkRTc&dbVLIf^QFY`RQoQY( zE=c7g{l8;1h1<>UwABQUV)%uIKkz_*9k+>%4^NMrNpx!JJ?NV&EVZ059NkcsMAJjJ z+-uL!)gj+V3B?ZSy z#QUHGD_JgLcdeZl;Y$So8+UElhTPY!W>`x*zgoVtp|x3Tzd#|tCEcd=x$2mRI(0QN z4WpYSRUn!7R>-fStnV}QwDr5!X`Ra`3}mx>y4z?2EjFA?#=tUA9Xqm0Wkhu5mp}bi zL}yGLW@&Qw*H?%2T-@n)NPqmXv`vG*{|4APzp^4$o&VN923_K0~X3GLm?Dv zjzUyYQEd(M)*rZCL$x+BC6Y3+!#6Y}RG;I2%0gM1aGAts3q?1)G+pz%nC|gXaEs|M z?k;dQlI`odC{UnSMVnap&W9F={&J`os4P)OVcy~BPqNLGhGZ}=C0pNJT(!GGlv`%Gar>uezHrb#nP&#qbrM+j7w9(w_&k20_DJ zEsewE3eNtE&w;(=;#eb;7#@pam&Y#++BjVn9Wzhihgegiu^=r6R@J=BzGx^gcyKb0 zPP8;y-61UHwuUBO^FgLzR&1&kMuM6s&$Hx@M2Sq6>q!N*JuNWdAqO3;tyetSZn&!{ zg-m$`BILO9SsA+JjgMd5L-EQykbM{J#4KI+GV-=b3b^m@ zn@-WQ8wjhwIbY_0chq_$a7yE-N&$hZ8bSzkOJP=7`Ea_={tImSomcDi>2&k^^cMx- ze$e10q@c{${5(76a%)~#FcqtorGg`9gE|U9V4B!Gw^^ugxpL8;k{4mym#gt@|4)L= zr~K@&csx>Y8bah7XpePBgn!f6)@&Y|hJTyI$ZqK3`2yeCYX%QZ*2vY3Ah#!@fCj2$ zw^oW_;YolEYlUC~5&ah3$iHD6YQS@~o}zmt7+hpWw91-RZz+P z_ApfIKZ)bf&59V)OnkjKrx8Q_ig@*Slx@e`tcPKUkjtC;HkdkT7z-!zXS{>FbNk-d z?T`llO-eV9dO-HBvzLPHln}jlxgQeqXgUiZ!+We9q8o0-Miyy;^qUp+w=$A55=77K zb#| z`(1l%c8cqB0L}nDJzosE(5FJf2u6PF^8`^stU)4!pNn@&7OwX)4Ji-c>5-Q-OTZs& zKTagS$>T)VG&CTK!B)B8o$HhA>L%yENPf4-1pR)4 z!u=ozIYg=)KL?_#4@UH1Fy62!f=#=8UB#7MR`r7z026`Vu1chU1 zn~|-E*8iQH0uj}BM38fj!2^gC#Qt)RiHJumDh0?XCb-?J%a9u#22k z;yp#4Fk_f${xHWF9|J^Ad&Zre3UJ2|+fi)q(Hg6R z-uo}{o67`D!vjtH)5I(YN*qtOp54)p8;TkMF_)nBhKH8rB2T_dAXE|uLu-ZIopFpv z6+S`&5gj~7B|JWW6_Gey*BGVGD+(!zPswfpuRS>lva)LE^n(`*@TFm!&{rvVom(+I{r+#24 zt}&PNv~`c;^N6IjgD+EMe(nHOj!UtZcuN5xtU@xB4M#DF;-ti@E1X*N02eh-33AuL zu{Q5-*P^GSqw!w-;EFqUgamLT9LDpZsY#e4kPwu1y2F zx~V3;RqrFt4WjBJN0*e^A`$25D$o#+GU1AaAV_bMbyBR4C#un|sx+i=bpp{Xy<}qu zo)Xyw$pN)9yS(af)0GJnvyYi0RO^#+;cW*^``P%K%=pci;Gogi@NXq5(hyc%GlP#;)!S;%K95=dA0l>3KlRlP4&l zoAh5&%ni=J^%l(@Y)EW5cPOL1p+Qpt$I5rr(NvfniD(>KlvL0b|sQ5flrf#_1NJ0gL8 zMehvx?>`WV2c?hWhIfW^Sy1T<^?UbIy6eSXUrTnUYy6ZFq(Yzf0lEq&=90~Cu@pXV z3gX%hq2j0~m_ZNOQH}Qx7w@CD1OU%gd1>=~<8>~~DgNFn;GKL1w&&7XnSTib2m%90 z8X(K<9zgtIYgRB1`c0vqgzsr_=CS9L>K;P=T!0WV3zEB(v^PMC7H)e&oSi5$cr1yl z^Q`PiyaJ{?tkC@X{IwO7(j)S9`tCE+>P~m0!9;plkF}>*LrcHG zt+!)EAqWSH3A&UG{`=OeK!nK{RtSSB?ms4qxX%W~sG^u==eXkES0?`N+Cl!09W@P@ zqE{b%FWPWQ;{%H1xmoCObwYCGfvy@MRPQC(r?=9erLKRapdvo8k6W<|wS{KPXMGB3 zsY+#XCP{v7i*Rky5`D(9hrr=4ImBpiazOm!-GQ+vw{0NjF5uh z;bJV`gE3L(@J!oqeoh{|YwuvBEx;<$vd1vhK4y z%1)yzXIXQ@QxK)4x&CwhV3 zZje6mGwL=J<$bQ9QOC98)wMnJ@8jCz(AOyHfG4SKoY5+hP~#_2H-NmZHCmqRidZGD z!!%woOnD>XDbk^g`7=QUrVMG9ZiazvqaY3_m5Yhoi8tx-(E0K!+yeqK%E* znK7Ee-WZ!uuQ&-^w*Vim78QfxWYt=yHL%FD(O8-NAv$!sUTqA7jTD^cG#(LD>E|yC6$)By)*E7*R6_3|^ zwZb9=m33-zN7j5P-;)*gy(666-y|7={I2%%3BuN%(IP|xt|EEz%-|?*;y#)DvWJ#L zl6me`{rZ6LpLyZLpITD44xS*W4dm;VFI&OkHMu%_U5An`1!B_8L)lvw7uTI>-h!v*@9jKDva zDCDSChA?RQd9E0blXKfTE0rau&i~UTRu4aKI5I+#R8*1Y|D|@xd1ajduT}o{!a-(? zUT&f?bc%g z`acjatVi55el&CR1u~IXGG;x@x z_aoE=%}nn$&!z0xdawTl@yz{WT~u!cyf9qdn5Xj15`Lf0b<+rA`6yh6je3z?euM3S zvhBzYovACk+^o1@%!Jpbf_Yn^cakKcx~^T-<8Wu1Q1-jPA&>#`4iguOB0ufORR$Dv zIoZ_($iJt5l3p*j;7;x?xxy`nSJml!wsB}kt1U!sFd-;R?i_`<=_ot0+oX@b!6C-b_GfVo8ILz7~DffP)H-_+Bi*LTsLCNV#b9FaKyo+2`y3>5nutB__Ks0h}$N`nn}JG zOVXkk`1>&;o|cJBQz*%lz1G@@XX>dYy{YF^m1vhX!oe$e>@+n67OICJ(-HgYh`D`T zTNcJ6@}Fle)Eit*1CO5NMvi+Oo3|{D{A}SqrOQ9`~Yav%sr8(3THONg2uXS zOi3FI3GspP1J(wS6U0TpE4=P2J9~b!{2w#dkFIq4YGX?PG2wHCV*9YXT@#-ut)kl7 zbBop`U0Mc@fM|lidslq<%u!UOVkbCb6dEy>mpDUjQm&vl!j|VqM?2W!D-!dk;L%gm z?J)-?5=%pd&4S^)htLy5zP3qYs@PS;uE*OeyLnHvj@N|Re}Owk#Kl&9)`Ej6SDSko z>iTUoCSUv;AmA{`FODpVmh>H`4<7wq$dFJl6|6o7^ycJx3AK!ovdfbju!WI_Ea#8FU4=w&8t0*scWQK)PoN-H^2cP zkA(%=Bo1iX6ag%pEe>)o1w*a2nS`|=VP{kZ>G!dU;Ua4O>-YMhaqY{-!YqQ17K3~m zA5Xzkc{n+xq4Pql>M3*7taS74O`F>UivdtHh{?VZLs=zHga4Jlo@D!zt9Hrf$hFa9 z0t2SXtRdN+@FI9Mxy*C$J~Pdz+_jrr8~VYdj{fm6uLt$*KQ?5?P#5*bNBO|XH4hndZ;&Q0I`nH9`LXn?@C(+ z#|8H_I3}6&%FqrUX81{qEBe15*AT**QMdc8oP;<%sJQ^h08vo@)a^O9bRo3;r91t@~x4>(1wVHYgW zoXy<8!dx$_y^KO5j&;OJD>-`-kh}X1p>I_y7-W^qS1RXA+^8iJt~*GREi85U4pAZh z~pdF=odRVO@ zy1$dJWk6WwRI%dRi()Ck*i!@sXUA=m{<%!OCCFVaDb&bnL2x!O;^|$n!CYjB6J6SE>7fr`^D#l3CpE_`8DJUe7)bsD7vpHx50us>jx*s2!|j# zh2>oKlrZ_b_0!zRWj=!zB8J_5g4ZaRv$u`sL_~6HQ2i0MD2fVL^8gr^Md5C7U~bbq zNr{~}U_K7XuxJaVqq92hs9&@rXm*}W)wT8P_xAsih%b;E;ZjLpBA{sP)iA((Jljt2yfvVSimEV!E9CmmYd0ufQLrEVxI@ zME9bjD6!3t$m(dWJ?b}ZYEYISS@60@01eN-jG$XxUk1hGC&m>H;zxT~kSSOr^wuhs zX~LEl^Hjqy{j-t^)PY-|W`wTyeR|!G=+p9ABF8=pxjBd7!d9YG(Nl%wdG$(u>o5oB zJ-FzYTLk|_Z?nwp{U8c9?nyA1UqtC~-|md>fFfkgCcX}@6f@57Rh`XW;VBjnW0gmV z^LN5-E-okWx8kezLPer*MXp)=&94ZjTuOLStCuzQqT+b19{XA3)(83V_%iTAX5JO0 zUN_x>g(nES8+3Arz2Rm8rz(KE-6|5CQEOwL0B1@yV|hn&YZrR$vK*wVa@KFhmF7L^ zCc!xbNTT+NFCu?ron~kbD1>4BDcSzmyTWT2j?KE06c?7XG2m2{GS_vdJ)TDO)9XqiQNm5qdN|z z{W%jVl;l(q?SjM27XDa2@Otjd2rW@G2{S-+pN+-MdGiYQzY!>Dd-b~a>Y;bPI54|< z0U-Yfmxl$A3b*G3kSmMd8?mr7BW!1%fwyq{zHjg;k4RBx=9Vn}`1sy_#CWDiu-0zX_ffhu)4x4Q_CGoZsHBb?ubBp^5mn_$e;Pb=%F0NmS5*VWNMoad3%)qyfhKhgwepNQZ0{GoP*qH@@js(GncE z&tcn>S~&g58WCHhjHt)}aM%a628=KN6{0IlzEO4@YLfupN`sqx*#>v=1pb5v`+V=w zMs#09{ArtLVOHd2{BYdS$^aEiqH`k&@mVJ~FDua6tb?dAwU_$@x_tmcd)OD6oie%w z_b^tWBadM|wQK?@>0t`I z@%t0y4@>aB{;(Y)80bRXoSv7hJ-1m&NglkZm009avWMuZ+t_~NQb1Vtd&DAQ=#mwi z<{5iL$*F+FTgs1+k#L~_`}?=d$H~s*h4Eg&=P1D>R{_FLJqVN&+Bh^4=9k02CC$^% z_n1-yzXlq_pu>=zs{wn9uMBf35w(req=ke<&Iqu2@l+PR1}KS=!~5hRAz~|FAdpI5 z7(iX2a7YYhKItMNyBX?KT2BzdO|S235KC5zQ&u8VFj@WhDazj5sukaZJ#h^5xL=hEtXu9s<9K&VP3v$tXbD@y8s|1b;LD84q zO%=pk$Er))=k)Hz}I=d}Mmx z`1U2jx5m{}WL8mIOkIoPFil+lH@s&i_W4YHgn~96iiWG?H@aG6znsYhFK%wW26kX& zxQsOL_vyxT>FckfyTVCuX413ZLegW!YL(9%c83MOT7Q=kEo;pch>Cq_(&b~D&r0GA zsdS2YiGch}B%L1`S0)Q^s}R`m%Jd-cC>B}^lc|P-1+#)9*DJ1rCTeVLCLwcOyjnT( z7TC8S3-aZH{JReBY+kCY?ts@?-|7{9{7LX0RS~5!(MSoWpk{krV(Kpak+ma2G>A24 z>-A&69qC z^p_u>oRwM+u2Lf>7v{~>Q@M{Y zuq-uaoL(5}Cs-Q%J`#({SKBFuXe&#>?1F#b0BzwXE|5XdKIugo^!gAuJq8p=I3#nj zU>s9squ)vncZDkzH~kRMG*cjb%v(nln(Ag1Ic=d7IK%JM01vEIs|rH=p_E2=KFjpx zp%L)2c*_)nJ8zw9k*U0~^77(*7~`}5<5ILi!8~!a(Q6PAQ3u_&vMGThJ?l*Q70alb zjwEYKwP)B)O}S|TH)J)$h)~Wp|5o0OfE$c;m`X$g{Y=JkaFVb2D@Qf6dno-e>Bc=_ zd1;|Hzktak(%Og;;W*)+?4yTJkt>E;aa{rM*zY?C zM~u+L{^6CqUXOerZ4A~AG0aO9&=Ww8*xy7Kvhnd95TSb~v%F~LgClT|eE@;ct7Oj5 zFFop}miCruyDIsHCx`+#GM23nI}ibjv>%zZs@G2Mog_izs=s_7pC$A}xzFkMGL${7 zEOFaMsbdClH2-`p$k#3qe@Cx4^-S}w#x^>H;pMq+Y$&xcLiNs4DF5+k1YCE&y9}Jo zm1F=%=s^l2^fOf|;5A?RlVS&q4+aMd)q6EjDk@KJxZfU_MS83>XVZ=FYPpu;b@0~^ z*TN=^yZ6!my`h3H_5Jw8m%wWgR5Xm>Izf9h_=L?s8C{3c(av=uRsC7&lRXtif_}V! zKJl7>>YaNw$b??O8;fd17(RljH{V87>)C&@SOl7X}n`ldDJd}l;Z9M;?skW1=9~Qc6vtjs#$)QKrKMF2k+zLl4-p~&@ zN5T~^Uy#7hh=>;jg1kvLYcdh|_=O}X9(xqxnsw=N1R+IO{RT!TmJ5zs&pbXW38D^T zz;vP5NSfi7PB10`Tw?1Q8haXWFsnphK%0v#tRge^_DP=Hjk#W2dKI@8j+J7p@&j;! zt+giT1_*`cL#6S9;&C5zQTUX{x0Seq*63MYI|L8KY-8tDgbchd(QMDxR+uW6Zw}J5 z`>nwP6S5RavNtX+P#zP>WIvcf!8)2L(|v9{MYz8#HFvb&hqxDlMG`2ouX`v>R^Ka6 z$7%ing;1IT3is3|H3=hhBeLyFWd`J4 z%BWA2Qvh2!S_B#M9fzpIb>bhQYZouI!-cLDSi5sB zaz@a$)4I|0H9vma`{RO#%jL#_uL5x3xJCmMuF&?2NQS36hqYk;RA$Int!ax2+n5W03JR^SB0}^N9-%CGLOJE;O$N4& zGYkr^Uz>kTh;-WWeNy<%+CN8v8Biyq)|*q?*1xjsn%m|J)4O# zpTy`t_osbe;g%!8rCMD1xvTLWVio-tPG%mh1-=Y1DOF+?r0K9bT?+he5+tEj^b%7w zG3g9@ytOo;pr?Vcfb^^+=RZVp*-arCTX8;AY})I|@TX-LS~8M+d{eCzoBsi-0*isA zgSICi*S)_JgKLf_JC@QtK2sETRTJlbbw~tcNvHp~nd&iYZQjEWY~~J^2Q`|7ZhHrO zLxjIxPfOw9;ek)6cL7Qe?{)~d2WEM1J6#r}Sf$95m;)vOWzIJMM5`&lJ!afCUMMd; zs77*w`rY>OH?#R&uX%Iv1zY>yRITZGysum=WLdz#Xw zRY3`wE_)Rd=j1D+i*q-k$%sv8bhx$D3QVpX#1?jIjI|2j%7Bfo9~;6cen@(=cv7Pw$dC8%Z~*EVBhH~hoRLWEdRnV$ z2g~4yiyVa(hOfR`;B0ROx~6ivfh;AqZa?gm3&}o$Rw#;F{DNn-XFU4L;%%N<0~K>k z-VZKMl}2}7u28PSsN!8JsrBgo$5mRDJ5|9oasy!?V>^Bv)_O`1Dz0GtiL zul5pc;2huN|E+*%@r{q7~*5br+ zOf5<9M%4dDW4@VzX<&>H3v8A!84IbB{NT zCVkjgj874^KQu*~2$_lTRE!lmIBc&xnUX;MEQD5)Pd)CgCg*YURh9N#jQj2{&WbKz zYxx)hAKP#-F(96%Dq;%i_UAlw6Xm>3dj7_v1X!<*)Iy!>uHCi7sd!UoKbB+8NsO4$ zmr^%w{4&4-!?lTNT>3KGxqWycLGtes!uRfgGirS#eq_JQaXZP-~-(|loRP}m}sX8gmr@DzHAHE{W8F=)kJV78^A8~cS0xzwY;!uT2 z#^XlHh?{+b7Z|C)_#^BMYwGgm3%*<3Q{2}BK|bYYN1@}Xzb_E|8tEJgKwEL_6y@t< z+s0RNEH%zDuixr7D%P5!A!laIy-HSE+LL0-j)_3!2elR?wWHv(jh1?v8)M8RyI_A538Fs?Cp$`iv0EzBylWVdf?c~KJ`Vc0t zth4iDqpaQ|p2p+$7LD%*z@HFpPT$kwMS69NsUPIH)wH(Y<60&eUI669uWxf!@z?p2 z-T|Dj>4y8_AJPQaq-px~Ybu29V`tuvd8)kPCI~O`((8ot}_W1ILUhY*^!`g3}zc zVbtk67MlObhujLBbX&uTgHyJ`x6b7CEVH z)3-EO=kx2hCXeb=##xj(T)-WlneA9uz^tGyUHt|>#X~;X3hwxNv~pM{^lZbypW1-$ zwC=(zBVSQ`nfgd&ji>5s>%_Hx2i?hV^d;*B>V{K|ogyb{^fN)Y;sTDZ-|p5F?h(J3 z+n5%%S5G2!50Iqo?hJFyPkMQ>kSEE<8cpH`_a+$&)NjkPvq`~wj_U3OrdM};XcN3y zd5byu3(iaIR4pigjqkz^I`gN2SdMcd2*#Ij6G*AzW4bg$E;#Up!f_8srWq@|xLr%n)6z>p zzvUC=bHuwT8>gNa!m8WBY%xKbcpyd5*QvA>#IHmFPJ^AVfP}e}qkP`kh23RI2&CC? zXbqV^NI}D!R(kA{tt-#47x)ZAqTJH`gr>^VhJUA_WlB)KNvHU*^cpBoyIm?QLedXs z-08lYwB|9L-$K}AQ5(uW60#INqJ~^{vWM?1%DpRRUjEEJ)Z3%9Q~zogzm_UGBZEoV z(q>~b8No8pLqo;n68C)LL}Ai>-YeO6sEhicv#VAI=TIo?Sfmnw@||1w+fQGjGggri zXrC4A+Z3m+Lg3Wb;FX;EREt3zPnc7DLcMV|jF>cQFA2}eETI%00H=;FqAnuKgQA$K zM6Du>b-OMWHu3mgBR8aaMPWe?sX{~ow#(bHx22P3BMwtf)*gAyH9ogmq{y+yV<}pA zDu*Gt@_Cp50003&n=*Jq$&|o?f4y7+zP*%19#5M*^$daGu^$#jteQ@2d8EN|a!Dy^ ze&r!6ixW6=R(YCX?-r!aVZ5r~q<|TBICJJatFVVH;OCxSMseSI43eOW4T+Bh7TiC$M;%;++LU0EYue+Jcm*Me0fI^QU z*Bs&Z#fqOZ;GjM-#!NW`eji!m&E*~t+syATCc+r+5GJ>QgXan=Hb|9!a|yZWznh$k z)km|dl|VMON12 zijKXNJt8{yd_PvB;*+~wY7MJla7B5r0s75t z9o-6Y(bd;i<{>Pf6z%-xuV(LFj$kH&pZgB%x4wm6*GDy^qW&~V5e!|bq|l*PZKi7% zC%e;oNQ+AoorwKMKL-QWP@4(H_Uzd>s>l&$<|veZVxrGx$E*}e!cT1D)tOqf!+b#2 z)WolvyG>lZmazTF?+pk57$E`yQ_ZVRbQ}E5^eOReF3)MOSl}Ii-2TsWV6MldHc8x-_?+$PQDfLAx{=2Y zt06OE9Ve@{o_mzGH9y|2wjyEGBJ7#_b8&J-9dB&0O;yqCszF@mBe5%vo)dXS z+FYVYC-ODBjNV9CLLb*60`2Lw^opr3>ka4eb~;D-&~*)u%HKDVBh|?`A^S>s%iQ~2 zsa=A}%4JP_MZTUF6_^TCoM$hO5@+&@lN5LsfVk|)B_kM*W0ZU)#+@6ZUE50`p0H1M zxaa(4!=aBjxnF+HZIT?*DJ~7y$qW-Dr+rWdIIvPZy0(*kxKxgMZ2vXGM1+68w<#?X z!GE!c%fpxGNf@w`XMR1KC!)dJMjPMn>@et_z7$&0f8RyA874n@fC?ws1SFL~!s%Yz zuIsT9BNlhzH8GAtg4Vx-D)fo|GxCSUvJ&>GR8}U&ge+lfx-B^sOnZ%al4qu+GAr7N z@%{gRyV24D>$8x8xk5Ix2e>YHnS$2w2|Drs1&7W3?w2P#iC|AhIxt- zhC_TkDbBOGg>#8UvL3DO%ECOEULmUeX*-R&dfp7P+0vEPNEVF%X$}SbRfr3zzv{&V z!NmLbtDFz={IZK2^2%}g#-`Z>^X=7>D(X$w%!sUG=EED=X*)?coDmXG z6qx(5JaL%nGPTud@{(Nq8pA4l26o+{R`#%#Ry(8^qse?viMQ`E1gu&oE;$nQ4UT{Y z&8`5jgrvs7{MKB9q%w>1tV+R6j8ijpbQQh#jP$@`KiJUejg*4fZ$X;*_0$Vom}6=+ zIkA(T);D=aitS%iKaFlXw$HE9)716EC`v$Q{|yPR{qS&QsAc99OWk)a(kVO3&(!@# zW&PkfZdhED{#xNdlADkn;yesXFarT2%aIGg*|uR9O397qx9k~ipz1{jx8NIxrib-4 zWWg>i-5qJC?!ml?83!@R$R&sgKE2p|fcvHwLfrO`JBb12-?jcD%niJ+((8S44Tfqn zT*LPTx=xL~ofu`^w^52aGSa@iyOrCRCE5G!O?y31obPh0AWzPLvNeu&;qC?%udbUg zLHpK_vJ4-9MeA+W$9q{E@zr)3^e@X-5O?g(3^CTyNmEZUZ-wtIQa+K!Q*;FZMK_8^ z;*=maOo;2>!yiXFYWvE7^QGGED5}};)aGF@@flhI@7SyG_L`@@QO`X#t1Zb)0xX14 z@z%9EIpEq!X;0;g*i*0y&LczY#_SDT*TI1lRG@r4!!RsUPX9Itz_`eeK%flJAc#FwC!vLn z`ZJOsvm6zAVs|}9am6`V(mSIzD>~FWK8v%r$XiJ8^S|l3;k(ROYV8k|3wNJ_rU8#* zVks4%7uXEoI;!xT4>QBMVyHAHo?n7wc;xmp9%vPx{075Ztf;{L8A)SJ@Ym@BFv@DY* z7DbU#PL#Zm?52wW^`ym!EK+mkPoX`|{h?#j5oP}f##ftAm-2E1b0zVSf071?d?H*G zFB3u`C*M`vfBa?2SYw&^VxZ9Lt@`{1!M^9L-|)W+H;AwH@|8nlsX zyk{QxyWDCU)<2%dFZFpU6dn?D+2gvYBD<~Gt1S#HX(Kn0XNUr_n|OKF!pD-Q+LSzh~hb35oxlH9Oi_g zvPN)dfdt@f{C>dmob*m)%b_HQi(7*LKAhi@M*J4`{_8`)R0oLs6>23xIdBk2)&z4A z?lxC^xk_VVkgaaQ-TKHy2ZT!VoVH&DV?XanN{hc(OoSlAwnw*|p^xUpPpl2R(-|JC z5Ju%tG(JHv)4}?H|D$ji@43KJyQ&P3n?Zp19+D9T2 zNLj2fGQO@nDW3#!Qi|NVsc08^0L0eINier-XA^Dpw$U2&B{p16Sh5I5>(OPg=#yKT5v1QnK%@al_qP>UV%b0SLc2XE8-)l0F5!|Q;k%yh-Pz<; z2FRA{@rU>?Nr)}Emn#sB@IQvDG@@m9HD7~R5pVb9=Bs5c%ayVsU?=bubyMtPu&-FP zs~87-YGK`1?eRp9!;FPtF60Fsgs(RvC%6~kYQ$ePPzVn8?~{r zKGj&@2hs6nKtXsB{9w4_aZWs$K~N%9gVDA}+}mda0XdkGxFG_M}5k z2!W#$stHmFA!AFd7w*PqbWY-B7r;+=>1cSt9PcIOcL=aKDFO4DaFSpec*u6qQ=AkB zQ?fYYXb!qWtBUy=L5A3*?1+p1t>Aem@05)R;l7ML49g0y0~4i^T!jz(UURh>oWdt= zv2A?tpgNA1G=JO))MKadQ=R5;U{Ms@ug2X*i{6tZ*M>HV><DKTy}{C9g1>^DDqI-l zeLEDQ_!3$GW7u>0X|kxySA~e!+zynJ2`jlSNT(P|;^K{|DIRHG(=h#gCk(QnWfqaK z5}OqlL;*F+0k6;lM-hX>`^2WJEdlV%7KRdSl#Ri0sU8BdW0H2g(xxYR^BhI#@|FRY z|GNz`zsSjUr+wWy_w}?E<|DPg>;64U#SV;1SRLwKE$bXfnZsf-bKs~DXBfMt(_>4- zRzTP?aG$&3K)mPtcEJJ4cd(D{@-81b*ieg5oPP)s=e8J!*s^u1IJN@fnqMCPn5GF` zv}LK}8Z|7gii`t6a@8tw_GzI650njf@G#rR z7q2&yZEAx?mXbABPCE-HCo*|GQ~%dxmazQm&0_6RGW{6F=9}1_5liLGMV!KIbLuhp z1;nFHPu2mydD6#YKiFbo_=ac4o9^$@1XN@*TZpgZ*vf{iBl0i8?)mw>x{f^Mb1!T1WP3q~jpt-JX9?RP!NfZLx* zJB>3v3-Eo^4W@2|&&{u85z_Ls#YTdyx9a&}3LoU58Fjc=Jlt)Y(6bLX25kMY{&BHj z*hZdH5W=KxRPFQtlf_mB4F);qMuI+9gSD0Q%=yw#Fw*T(8%le`I}jPoEMCLfinqC{ z$0PZ6Ukk@Ay#En(_(hrl*SioM0^TLQDF$kPao;eeF}enP*mFH+R@kHFQvZKKA#f^s zj1aZN09HV$zjv0b#ZuL*hf#wzTv9PbmRfxrdOkh5_(4DG@lREUNW-A9K6V)l$sKpupmz6o{vsdNP)Wc8_N(l-+e;svFrPE>(J)JDd zETV`U^>3%}10#b{{q6ygkrlhE3AZjweZWlQ)49eQvSNP7PF4@hO{0!uIa#ID(c@0I z=SRYngh*wdw8%G%C33QllP{#$rXk|jVrQn&pQD&OT*PRw7ZOAmu=-R7JzTI{JyOJI zk;Okz@LlFxXk4rahmQ1MkgWu#qnj{R^Ubyf5CiPZ74>bBwYERz3}QtiNSGZ*nY9#5 zbZ9wW*FPT7%*E|m;JNSqFEmWOHpl3D3c)s=B?GhpsOwD|MF#9tWicr{JW1{ z$f>62x{6VL&KP(L6*AVQu3ew^E#=hcj6FzpPjw|KBUnwhR(Ab%~GG3=^8mncyUQ^E%Td%+(W@%M2NX2 zH$Gr`n^TUNhQ4YIVxxf_5Lj06#sW+!fVP~G&Y2cb%z;^Qyc{_qcL5*zVP26q<403w z7;J7yH1yd=9Z#^xgn!U}aW%z|U5C;}{9GM4FaE8)RfQBt8n+W}r=xGe#R^3ylJNTF zz(aVID`hG=6K3?3V~A=8*(&JKjN<5;yJ)dPkV+iPO}TIva3Y9E<7KehX{5A071k-_ z-*2=bRA+fsBPeOsDlucW9m4Xm6P68&mGXQrvHmk}hlV;eHGm50EybNV6zvfC z1gjx576FWA=vBpWo^;GDNU%0=3vxD@=SFzkAMrSAl0N>Kr4Ug!kYXK0O9vuPLo_1% z?pw(0rC`$#-RXLboU(%32h$m1BAb|+gNo#&^POGQHN2Bm|LL~xP0hrdshCCn)% zOwqEO%oy!Yk9lU{wtmBIpdetn1rImSChZ4?D2p_!7L$QHg$D&J)Hnr#1aswOo+3F( zcYD;oCOru59UgJ?WMkUZRUsO;xSFx);Um{xaMmIpWdMf$0zjquer3qK1BMz=@Q<>0 zHkS-8^TO0Ux0hd)oqYww%^TZ?kuUWWYrYqZ3!S(-_9egPjlrg)jXMc#8qtC&1RolQ z*zn5~-aufUKS}5zP~W+oJ665`yb;jXLsOZ6PEsfL`+2(-vE&bS&;sgd4u0~yqCgyk zOp*sYoCgoT8b{TcU&XonW@&bbtBkxDco&iJ`1Ya1HVVvcih)GSa7}66^Z+c59&+~( zMi@JyIb-|ER|%Wdr=0!qwBwF5Du3&$-JinxzT$*FNB6}6?xc4{URgS#3v*wmJ_41F z?7}oKl*N3_TT#;mTe<&i_k~pk|0bf@T%8~Cqg@ye&wYbq%C-J!I5}VVDZ6@8Bdp-r zFJu2Ixp_l+)hfzD%&j~IaxBDa?koHc;2WLW=~!I8C@(1)Au!Z{4O_`qi}F8;{&S3x zaCnD@(CVu0e&=(42Nag_4CAaM*4qX^GWqJR)j`&;we3eVseGX>NP3F_PNq!-EZ)Ac zp6*;}HGVkokw6wZFI3mAjODUCayR|^o!23wq*S6{?%GYFc&9?~iuMLyP^Ak1&|*D>g&n z*v5@uJ5M_urGD2O+_DKdDMIyO4KTKe2d01d;-w5-uOq=^k-ddsz)GHIMUk(NvRLlK zOM}zOXIvF=LNDb!$rV)`IW%d41XD;$tw;nmyWn4=1WK6fp}KLrwlB)MV>=5ZRC>nM zoBoFfHp8dq*tWW9bgnj2Cy>I%G8t)_lo~S=^s}2bJH%zmb10J9+)>>S@Ci&m1W#OceXCDa22ZnS+C8*=|JJc@L;RAJhGXUypmtjJy=`H0bJ>((GSWs}OUR}@8# z6>qX7nS`hB8nZ%ab7CVlu(cpW<9+oI_E6>NK7}*l=qjKWb?ykjl3p;ha^DqnI+!`s zf+!gRTiPMW4~FHXe)jV}y3Q`#lMn_qc++UR~N996=?`GvFDK&E)VT2DQUzf-P zoC+z~KRVbv|H#auc@c%zb7MC3v#&0KhoIzQ&Kzyv4Nd-!R-2S;0^QaN4>c%tLiv2|$ z!^y&RjGN5GWIz+GufF96>+^pcxr?ExJv7VsX28Il6jYElPUgLreIbc6t=@$wg zwU+TE&meJ{{aEpXbf&b*|WjbRWk0TEc z&ZAcbV@c{q-dJfej_&v*yf}#l_(F$YMjDkwENIdg`BHy)d!PS~Z0#PdhfsMn?C^y= zV+$jzXEv5MMM%u(cvvgKl&#e7h;Tt#PM^2qoCAL*P+cVBa9nm>?{t0EmJn_kQp@=% zRMl>kKt%4T?=g;s<3~hMwy?sVrW1Ta9}KC<7Jrly>R8k;HK1XvBP!{_g$H?O+`G07 zajSr%?D6#l14TcOGD1E+t^Qdb0@nLkZQgUJisF5mhY`U&(*fjW{~!nDH?!>ehh6(`xp)sCEEcG0Pr zP$A_Gal5aXBqyj(gL;v(s|4?#k#Wccjs&7p#W&RzfnsozKn*<92+aR?S^cW|LXxM8 zG-c30vPUr+AP~fbC4HNeK7o4SLn?NXt)5mtV>L}aqSEW}foVDpu z1Q)7&uB+4M8ZY87>tgwai7(0ZlPI(OsGglu=F!WI+D->E% zaKXBH-?}5$+^1A|xrfN449xB5TiO7%;5GAH>iKkd%~ChWf9Q!l-MzI%6F9mlM@M+G^0W49Xdm7W> z+q;i^PJm{VPUVoKD_g~{PE8U<)7Em_4#!h{W^Kp+m8pC1Va z^S5u^z&<}cwaQhuk0D5wsen2tJy?xb4LbY^DPb2$qX>oS&r;}rSI;ao@v8qwLr!b8 z5%W=7fjaPD1uk{0IKV-YJ10rZwnQxFTqM|pwQNZrRg7Q!&evDz2}``i;od243Ub*U zM##C5A2O}*Yim+))JoG>7|9O^mTc~?Z%OCg1tcaO5Mp%`eJ6+(<1LgF=H*>~viO<_ z-Ec*%hn?kUD)82Eg^$ErTKcEvVxya-^NI$&fB5rWpm{Xl~q*R&EMBq|FR z|F-*-YHLiSJc8HBtJVzv12`3P#ptCD_KqPbCp<(Wf>$=2d_>@E(?LNy9n}q*3XJQR zo5&6wOy?a<5cCC%^=MSn-RPtqT-?8((#QJ7@3DYXnaWL7FR{jiR;yL$;edNsIYehA zf0WW5YIdb6N9$j{=G3%EN7s(pXh{3jeYo&QV>E2%7}wRj(SCoT9{@J>ZUwrc<;tht zw30;<0tf4E4;ZWtl>JyJ=irm(G^(!l_@Z&hKUdp!@;t}R!&a{zD;*D5fTNe=9~kyT zd$B|i1bTVQaxw$ez$GSnmqpoN^vHnfrCi zVM^Q)YhHa%W_i@y5mG!|cK$FWGG3`+y39_PzVam^*r~DN{Na9~hsngoJDQc|=LH(t zbeZGxCBIY$RcS`@!`N2qLGC(Og{U+#CmYI(pfd7@z>3t$IwVt0?<+^%vRo7ypr#|= z09Nrl3vz4SxnDi;;EpnS3MuE3ZeQxp$y23Cxx<S6-0T68bFbNpS>k`;FHfGw%Gm?fc;5qg;9#>`57C~+Lx@xQU^m^+N18-Nmv1i zYUTfgGschRb(m}_Vgob~Sp^$PxH+#|BRrC z!9|8SPw;B6F8`uVY96HpBOTSFMnT(UISxq#I8_BKRja_@Vo(68WQ7`hr$?bzj+m^V zP91WK}w{sQSzbV3B^Nf$H=D<_E;tb2L16RZOc zp4@Fzko_1-<@2FLByKh$(Ds+Dn>H(a4bU@k@_77)7^ivV+T?eTWcZuv(i964Q7zHo zt)T^WA%kfj{rrnKe4&o-?9 zs+zW9iG1JBpQ;sigu4E|&-2llkxeP(*FOn~pwyO)ge+!+h=CjCPb8L-d{xQNxz|x3 zait>}TJ~jlez+uXCu9*oQdP9lARmP1*c4S*O~?r5(zeA@S-9sn9S|fJGz{YLKY>_v z_=Gh>s&4OSywFTWN3R`KOt?hlQqqEZ;+`_|f~yLt*<&Ls-t`g*7uhJgc(k8_?oS;3 z*XoJ)60-JaPxv3QSFrpt0+$r}7|w&kz{JlJPT576{d1ki2wb2^W1f`$O+HDV2{oH_ zT{p&)%ZGB|SP?@jh8qZ`s;6T1f6G3v&z>hB6W!mAL@vm;8O^t?g7xp>T}kk&ki$Xb z?fvZ^Weh2hO^rLI{_C&uVTk?)v^q`A-grE-r;s24DO%Y!8SF!j_Q1+QndlYa#X8FK zo%;I#u#d9=zXoNw?7@|+iuqprc!K0ei#beAuytlLuB8!hJ!%0|o%Ct7pZkE&iu0H* zA>pR|^0Tqbfzje>sQ0J_e=}Wv?$fKvT}`1C6*n@-pXXNV`JQ~g=fwPe%tx4Ho(Qff z-uV&zF2FC0%Ku5dR&B|`gm3yR2)CN{v94_o$4gUqUWg(~eh;x{N~F#~cr*|%?*yzp zgp87sa1NNr00001L7P%|L&=oDf?tdJ_o&vk72) zP;Q>CgbpA~_gd9XHEK&GP^S*!i&r?YY0~6RbO z^@~3VRf;&=9+W?}i@RG3cpj%A#U^;ZIUZ=duThq`fJ9{hqfB8bl4G}o56S1PJtXSi z0=F6aqFw(p#Cx8{1rn+srB}An;VJNf0Te#r5K9isbKy4Xjr?EW=7IDSm3wqJdQ*rT zZ_IsMSh4o*>a)4;Jn{_th~7CrG1~4BWDIoO_cu8S8l6}xZ_2{8s)MMltPFpC|4fL; z9oRgA-T`KmVa7UH*ZhPh!jXm|^vVwCclJ%Jt%5qL(7uY{+D8^^m{Qb(kBeVq)c9 z3}Py`!pFKit@~+rwPN^!s9uNwmV%N6{$Y_DJRlqx^t<8RU$F(BK7yWe6iXEBKbqRinbFsj@c1<8+Vz0kudqc`i zLU$-(Ly-57yfr54vJ*A}gE6y%-f`=%`X0#aT-{@z2s-8n%Y`^r`%s7G9PQEqmTn}V z0FqDnJLIBQeQggt=*LD~$j7y==^RGuL%MwTz!~^@5XnrVAquvptnqELC|yq>OfPZs zG=RXdP^E4M~=A1SndGfY@P zs-1h5<7@Qqkhsz8kk;ITy6C)hl$*kYb3@k!I}=j0R@}=3j8i_r-R7>}`)fJ^leS;H zfbTiO@lg=d!DS30KHf6>C0i}*mA-gG6nvTaT(>+8{o;~@+NXn_{C`TOYV>aK**O75 zcmM2V)ODtGs&y|4$q}A&Blid|!F=}uoY-o&zIz3uz?PKTy)^ukp|;xIF!yASTb%CT zEybudhTgf}xYWBJQhTx+UED@|73%&CD%2qFYSGj*56R5?mlCsGOHp)wmiVxsG=p)! z%cY*g0uDR)tz+6&IAwKu4lAWRGElHBmjCOOnfjG-o)H)kTI|q%RVRu6`?`Jt$}SRg zzmurWu~=&UjZL`T5xRk)Vj^)JI$YG{ZGXN${TAMTug=6Oo31YB^3*ZhrU))t-qP-aJmV5)OYZ=ui-t>V zh;WSqk#b<{8eUR3`r5@+&#OQ3V0PWTUd^pS&x5{ceP5Oj5!qND_!jt@FYsd?LP&xa znh>K~zW_=J1p@H1fVi^KE2%L92mZtwyi(j7_>|xA5RSuAG#jH5b;Ij0z$%x_9<}Tw z?tNsy6RCTj7sKV2JuQ&|=woR1pfDr|xwW#HhbVwmb_U*C99xC^2M@gTqH0cw#Zn3b z>S$WzPDV?k7`>*D_CT+J;OxYIno?5fkr}veTq==ew)o;NA9*w1q}IJfhj59?t4uB^ zGgld|*WN|hGy;Wm!!3BT!aq855Mr~c@3(|hW{*}fu62mc-Q0FSgiMl-+IPZKrKgeR z5Q;P^B@JIr!v8X6bQ1~yhati|vptoBO!i6uN<-9PXTp-3$!M2x2AKn47L-q_&bo&K z-OWg5Z?0^cQ*uo;r3Lp$`~8bo+^@wJ$f@&!+ngSD*cF_v5(yY%X_NWfm)8(u!XFeW z0GBF-hvjroWX>u5ez%={w|?xc%p?)BG=dyG%&f9%7(+Y=11DQ5aSZxeQ|C!N%;P~3 z@OTeKkfD!~DPbA&($f{hcE079;Fhq{96=k~?&>#$eMFk$Z^JpAfhV>~>ih~Pf5-BZ zmGj(>(a?GZGd|8$J8IwzVRLziTjm)HSQ2#Onr#CR zY{0`z+lsklG6n0Ne6rSzuNus1wi?2;mtSl)V*=jY2nyBVlUz=A|05G)1d(AOdt7qw6XSB5SF#IPjHRmHBITqQu-T@EvbY>Uc#-$Lg zYRWHIxnd`~yjlrzpZq7JAm;)(>H&6tBOrC)#dTD6V=mY zhm0wChuuY!>`#ZA)Hytr{;3|Gf&ce+&<%?6v@0%}G$S`DVH?J`7wlxE={4;7p&7w4 zldTW3#*I~$V&1$qDGQ{A-^d~jDH$3sg^X`>%vmoNjyF|=(}Nd#(C6r z#LT-avuI~{iDmlurFz#>&7wzZv6f+P26j|7iu4_|D$Xt_gtCVYx8{bHTTZseqYR2%`6Ed zc9q!U)M!l zWS)UV4(Y~2bPBs1;SR931l${ZKHKvx1>=k~?V1fs$ zkoAB+6u20eOzi|{+p~q6C;ht-RpyUQWCAfoNa&Ua@(k;u$;kQ^r$v$-4Jqqm=0CZD z;|!cl<026^V|~^*iWaRRu`t ztqQ4uUWtLER)@v;2z&R8&L%DQuTL{S3Eo}0p7cCd(RgqR0cCTpl;c$fb?!InF7t#s z$%A#mYGd}!YCpZvHgqCQCYd%9jb{5`&mHHcVi%w3)hA@c(7M8WR%c<2;?0ckcTcL5 zx3Ukja+%5#DTb#l+hBFI(N$w7%ZJioszNQ*J-{g`tCs~pz1=v%;n@pKXR(g6$6N{ z<1+Kf1nBet$xKojU$`y8L^Bj&&nOgcR!3Cbjl80*->|&=;N9Dsvz9&kkc-u)&Gv(N z^k$)nk6Y)-E>t zg4_4;UTCpZl(but>;V=I=*vi*&-Cg@9u3ww!N$)T(2-)1#1@ng$t+HY&(}hPc3$2B zbto-b6v9To!3>bdLN~cJ>2H5wz>O&av^C^#F?_&up5-5+2Mt+grnKXW{mVECxiKzMx)%cqhs%6n$U(yc{+{ijT8)@C4c9#Er1RX+14$#3ACNPJpBr` z2#?l2)@c(^GaUB7MWzaMA@8;UUJFKEy;Wxqw6a~}btK1hUpbr|bH;c>d74UV)xz+J z_yBSSi##URP3WG$nBpGoYR6*)CCRG}%|ld}%oOsEAL7lk$`#mlng{-R?eHK!$lE`kl!AKTL;piCj(QojfU^Y=uyc zyuN+Z=>$rR9?g^t+Q1r=*VIgizS*ECJ!cmdAZq%XJKgZI(jo? z#K>$;)85$usTPf97eNkxaNA}!?Z5RN*l0N9LM$9_g&1@q*;^2veuj!IF!ObT3zdKu-1b)y}A2#?jHynlR+g zmr-WKGhaPkR*Md6pVhQJ&G>2QVFHo!)*>GpC-IiPhYdPskcjHZ`$ovFsgk#1Y=*RdCC9d}RBP8;OGW;$J z9|Cftsi+=%6+(4CnjX_f5YYV!@wo-t9y*2_roeQ=E!?j)JX^Mp_JeLy zY~mKg_xPK^dgyvrSQaOC9dp(IO;U{k5ohjpb0DC<%>REaBwLsN<~IMOv2xOJlU%jT zG-7s4t4oN<0SxT0cTq(9K`b-or@B(J8^2aOZCMEoly@(@QA}TCT`k1a)a8fLyQ*M-MvEClN$`U5T-<+ zkK_Fsl($4ik6gWj{Gg#G(usjCDwRYvK=H58Phn7!Po`p1!_>*C=}MpqAU@Tm^*!Vb z;W@G-!?z(P0r4oXCC0P`p48D43RNISaNyIK} z*=@++JM(Y>2VEKPAyD z7qpc1PR4g9)y?AlM#AA04`#p7vC@SXAuG;YWso4)Du3YP?mgOGJrL5hXE<>e&y}CI zL@VU1!`c_cGNdTpYwq0g@P~;h3NXrnXGc!DN`|ix*q8v_s(j5RQxj&sEEr>?`Sq(S z2h%iFcFy0SA2Eh_Q9&wT|Az2MHn9OIX}Y3-QIWa0fmp@);^mQbm5$yePnk?L5WTqt zy&Ls6xg~Fd>Baqnn=;pwQxGNAr2q)eeX+kJXT(b(*7d@r53eS(uRMir&m19N67BCB zy>5~Zax8>ndyPyFd`@Jb9R(*s>tlY@a7xpd6utQ5|B&oKU;FQ6yW?-HcZdvv5@JXQ zvbQ*k$JJ_}zUW_`aUiJbyrL52p=Jv)xiA7j!}P!%lIgs_g9>ztm0p5*0VNq39(B(b z@65lmf(k+~& zcP~&kA5o!bc#Eyyn@2ntzMuMr;J~q{_gBDcF8lxN}y&CZK$r zOG)1Doq6pbGhs)*_fXPoDbHr*iT=_xtIrbOv_^?UZP5R%bkx)kdJ%px%=I--EbQ7ZI*qIg%2HX! z%Pr{tSJ5;h!IjRJvYe|5BB-Nb(7fiI&;&I3pR0#FH@ethjm{#4#sd3wc2-9gIeNdP z`K*2qO>khfO2=h5O(2)~J~NYQ2@KCVHzze9*F<~PVPLL;wU&3ut z#Do>{7hm02OJC`iI5o|Kvkl>G-VPybm#>%L;0?#q_}aAq=(8T|vjf2SX?dYsT|u<; z8SHB*5C*&IUEr2KKAKgHRA!p*nKWq4jnQ?&i}Ri<{2rN-SZ?ySmG)}pZfECTW0iBV z?d&ZWclsx_v@?_iy?D7>)}~petJ6ROKa3}`3#1rUc{H~C+NpIj?yxgZvb^_Ut(-WhY-L_K2}!b{4QB^aKJ&9%W`Wvxp$?Y z3To{}M)2-p34p}CgNz0f{8>h%k_1rBk+tSxY9 zTp^=)moFdcN7cBovJlJ5^)#cMp&=jvZ`8|B>)-HW2mTyFY*+N;DK@PeC}lU*`r(Jt zN23%*d^F-?Zx0XqNNKC@TI$1ApD6aYcq$-%frG84b8|a0ZZ?U&47a2Ajx8eSSC~}i zRyt&kaSGXMabhW=?9VB?)og9+1~jk@N;Yv$41HLmjdXsgz8M9ZhQE06adGp@Hu_6u zu*Qig2VLe&&m`F7XUW45G&cx=A*`u*f;OhTCet`2R`r5>j#b=YgXPfZ?lZ7GMlKEn z>}{er%54TTEhtb(j>wmgUl#U(q z|I2M_{Fd{}W%8(K8f>z$1@8mJR0m(QJmWT4ht_YPx|bSsVvlFzXa=9p^q|KV8}gU&0Rio5 zmZ<0WImMPsBE(=?KCuy|x-I{Y zQ-m-}tbwX=Cy6CNh1Qss2#0clgSX?x07TKIc~t6_NtHY_R=A(V)Ri$)VAe*HQWI^Y z_d1e8n?|=nmBdhm@2-SH4ri0x`?x`eXHZ7gP*F&14{O(K-=Iv-lzw6`_qv#go5`Q;D_^u+kn<%;~i`c5h5_LRv0E@XgoApPL8%C$hUN zCG@XO1^k|t&r_8$G5 zZ%bG#Vp47Ec=FrS!-c5j6U*YxAxojhI!?zVsKZ)?~#^x3T{?k5SsipbQlaVmn7%Z zX}2e%Fe<+mIpc4T{ghx^8xHsun{&(huUXFL6xe&svv!vz-!tv~*2gSv@b|EgOf9)f zV|hIah;N3EP-(aCIhOVA5kkp`X+3ra!a~5(51p+7mN3mv<5bm#5ltKRm=KLXwtVw# zd2T1pfcF;QEPX=#S4yZ8$Azx=q(~CwH@{r&xJ*P;8`z!FF_J4NvqGx=g3{sQO4iTv zr1+93lrZ_*Z@!!~#vHHS^cWY;z<(II>p_9=v?5$oSggnKLV@~aQBTFzb$ zI8^cKCS?nk&5vji;+k3~FCCIq0^{7rvLpG7=|)^v7>2pq*smfDWLe564NM9;ys{k0 zy!w}=Rui5BerX5_$w2=N=rpMzYDN!v={IG8Z^GckOsV)y1TJ*WcqF&@RwDh7knJIe zT&`>FV1lG_EeGy~!*j+4I)iYep-b_52%dunPrbb3DLf3}5gXNKTy#M_4AFatX<3<= zZn#^GL~vq;nzUs>L@L>O@hF?Sd2UkHdRsgrz*o;&0t8XQVLWGap#Uea;nVUBur2Xe z<_DA3;Ire3Ak;H61R_0aIhO`JXBFQO7^-6ekSVmto*(^FWPxCBhfIJY4RtyVp?L?7 zR;1FU{FA{lmNb<#K1yw}h_Zb_@&i-0awh*3!1%WbO}iRMNMyLeKRR+Cz_8Yv*Q@ zGMJ}_@%D9`0%5_vCVLVKbNJT6irn3f35!K1`DnnG~X{NO9z8)Vh zERRLIP>MTF9}G>8A(xqPpYpIB$urnQ0m12|i+>ml$bEnIYDSt>BNKOb{7eX>0?1`L z4|aXDsbmu9TIQutv{HJlZWA=LQdiC+MMriBQHhl`olj&qMX&_g*bcFok(3D161mYR zxd4d=(S(QquHdT}1OO(lbC0~Bi8Tmu$ zC)#J`-1f*pLaO0YupICK!2xyY00001L7Q@TL&=oDf?vCdjb9Zlxd{5T>;DELVpoL& zLKTdO$ouXQ&UJyV(+;bIVw4M-hF|OvMM1+47K9AIIK-ri9osZ44FRg4WfCZTZp;?* z&=1z&#yLL!{WXNH7S|!V%I)g{i7rx+iUv%;w&H8B55q> zx6w`+L1-&Xfq+Sc4a%q7bYo+-2+t>d31D++g!6qIL%bn8)sBL4(S)5=4eglU{^Tgf zQ8ncTKTu>2f<^MT5SO513csJi$s|6aDW%2&KKP8P>(Tu%5bKHMQ!a_IO9lVNUxh3d zaL0`9L&qEDX7yDuKOK`t##`%Xj5DGY0aD`Oq+<>6IrmDv`ci3c%%*?y`9u8iA&@}z zOmb<>PUQApkdADvxh>30cAha)48J|eb-K|zV|oczJfGVAFNA6&0DIpt+E?3Yen0P8 z+M*5JK#J?!nd=UFv8Vt^9{7ppOSNHDre1dsB^%u1wB4CQnC6gvs-=s((Q6Rmk$UZTxwOrGghm3Yn@@A;1!Vt z+Dx#pisFjx*mk#BS8ud3v@tv=_tpX#@1!<_BZ)BP@|eO|=;>}{c0ef63f1fac3?%T zJjm+PATHG>o-QL7jD3SI>X7-4iwsezD|cH7Ncs2c#sFDd@+tN@U~UeyTkRDaFgg2X zbFhI!4~YL@m$EM!{jZb7Xk9sVS8qVn{LS)J{B?LBL^+s3_eo8NDZW4+d-nL8v1yp{ zd-2&}>Wio)nN$UulJBYa>u+oZaXI2ARqg3Z^vu)6Rs&r4V@7$7{SOW&A0jKSRgaIW zpVpx0r0MKGdk~XcmG{{@z#piOWE(T32-GdEK>0TFx55;O{(Z^C$mAK?|BMR)jQQ&P z?-$DvVJt0akA@5*^pAN;{2$KD6Bci4CqpG8Omj9U$jgHdZTW(nt$skAhQ$eR z4GOoHhul=VjR!+Zq%T#+wdJ`L$-Ij?k=7Sjd(==+LGH>6SRMUibO3;a_(Id+&RtwC zt(As-H9^gP)&b5%oZa>)P#l3%y$^jUkS7{RCO;6pZ!H{p>B>w5w2|?6^L$w~dcMR=~WTp=qhgRob8dUq1~4XDqmd&N1ct!~diR-{FG19h|j}4GMl)0qT|bl1 zYLw=QnNzB%3%0yqMXp^Q3(_m=o%%`XR}Rh)eMyd+r}WhH0;Ek?LFf=q?}q74{~}QB zAveJM=4{M=KBe<<28dQO%Rv|pq}qbId~CH!vTsR=A#koamRDB!bMw6`p;(Mt3z}tS z1pJZ4mF#pzFEaT+(yAT>xplDU=v!kSbL7{cVlwhCsm@bC92go0c^Ll_doN5i!%P5OgmR2l5zuD%y-n}SoV z2yS!=ACQ2G#5y(IIvCTpSd>I8Mjp6uQqVMj6K|p-&cA7K3o3+iU?ex{ij;4!E5D4V zSAq_uN9rM~Y;#x6m!p^ZC#eFSG=4hR%Kp!8dH$aA*`i}77?yL+zEG7`d0F1TEwoh( z1`wv4VZb+(DP||WIyONz_Ei5PE6eCm;^uN+Gnr^OvLfohT4@1zC+-hF&uFMSm(uV7 zH=v{vcV;)L+q73d`8hfoleltMU+b6(93YZ8K&j0A6tQS|IJ&cp!(8L6BwWkaG5EVt z{~GNF1DvkKKcqImh}b=WjMg--L`YB#ax}oajsHNbE|*tqh!x*uJY-t%%@*un7@NA# zev!&^R2Doe`&~1rncm|2CJQ5843>GD(Qf%ev#b>8h7Omc?K<0++Ivh zRc28CYecxpqyD=Tyf|a{+j2iedDL+kh7``YCIef$#fenhnR^{1dE?2bP=N#) zv6Cqx&U~-b^*Oupfu*y!~kshjeQ$Kh*t^Ihq&@t z$K}SpCzN%Q9hb9`*WIx~t|m#SX6$RKVC^aZh!*_=D5rE(N}U5EDbKw3qKYdIFMvC_ ze+-CJnWCJw?#0w#bICqDcvDb8{b>9LJhxAM+b?*O5P5y{cB0PuGz>|Hyo&sXfKZ~4gwOeu>^bTatwtd!Do{B7*i=T^pGkS0xFJ)u#?~2`K z55<6DV87;cHspwXF~?-vG0&A;1V@HT2#9zHd#6v{ z6mL%Xpl4~?IcnP1);(L(Cdc6|dN0@^{nz3-Mhh9Ovt<Z{iQEM|N{N00kY`ZXLly|xvx6Si@WilvWq6WVKfe!S8VM4 zBHYmCr%&CxiAgV7`me6EVCpWbL5a_dCD5^*w}G&hrkasZegLfM%_~PcD}}V<6O?U} z;oC3w6PjwL0t)~@9iK2q@pNaZG1R=#9H!i&&O1)U8=*IkWqL@LV_1+{y5gaPYckx) zlY^tn>)tz;-ydEM_+1u@cHV!`ml>j<{2gKce}o4M(%ne4EL3keE0 z6Ev1%1#hUIw~3o@4*@41!@EjxB+SA12-hu0{Ik`$F@k^HO0xna@EmaDXzW-^vY6rjuVvAZ|4T>Z6TYZsAYq_jZXBS6E~TiJpa|Cl)N**F&(@ z4jQ7|<=Bt$iKrADnpppSY%QR%aOC6vy)%}ez3HmfqZ;sWGS`PHTq;w=qQTMju=Gn^ zf&<`}c&Iy|*s#zak~k&jI@P+4_B;TvGt)-Whk~-b597PzIWiMu8Qi`+>B*xEL}<%2 zau>=wCoAwp^ufKp!zevNEUGz|_qBDrpIMaM$T8#l>bOqNY1APrnJyL*FgQI!CN3iM zz8pK~&>ubGkC?hbpj~e;fLwm|jdv03urrT`wKE}VS-QI$`kiClSxAZ_Owo?4 zt?;1uW8+K9Q!o3I^e(xVFAGK0_%fg+9sb{XgAdRc_;h1rhtrPa|0j_!JjMh3>a;1`4Uy`rQREv8sJ`J4_Y> z%Kv!w!g_kAPA9gPa~T9#{9d?lq-IHZRzmw>DEAW6b0XxC=+|nC|DWDUbj_ly0Em2T zTM(VQ*S|OZ$bJ$=CU!uZQA3xLKH{)0qsjDrP9-bX=-1aW<1e|QMgK6D)0Tu;KV34f z_9r1eLy1OQg6IX!LzW@7s+`C-_KnB**O%>cv@k)#Qtm4o(i!?V88!mGwdHdkC;f9P z>P%xB8^fJ{md=s{{&B`{C#{(COq^!WXl?BHeXTNXS~74__2ZjO6N;NU71rd3*s#Ip z)jpG=pYVd~Tbx>%y&r0B?ppVob;(6OVu-b)4EHKnskm#{2Pzftarp-+BFfJxSoA|* z!=}Nb!R1&-0SerQEC%-+*!)S#o{M`#xq?(f(cfb|e#t3x)Mj?d%5JouK$d`rO~18R z7$oJYPcRL1qYJ8wSqG*JjhXIT15!uUQ(6e!aaY45VTB{Vwf1^kSY*FPQWH(uMyzKI zO8%Z2WVM658I|xZJ^>ZOI9Q(ig_?f_zwHIZ$kSWi>{VwhD2wwrUMuD60cv~pGg8a- zUE{56fB2B=u5SN4Sy;FkIadSj&*Y&{E6|2E)5f<8)6Ix-A)sxT$0IFSOMQzh3oizP zUCTzx{-Y2057`}l2SIC(>|S3JqGvVY%#XP_pY0)~Ktm`OvNlHQ zcdvAh+9mQB5|1-B_UJv^CPH{0Fiq~<;2r4?#Q@SCUnrYzk!V(`G?a%9SOFdCK(|2H zlJ1-yxmx&VS?&9qUz$z$jkio-@xR5_7!TkcP?d>22nKSH%JEhKv8a6UA+5HCcqA&X z{lD$*fNEcMFS|)05yomFsQ?-)YD@*dL1QPYi;A6)h;iBRRj0!*XKpMz->y3s=y z$~#mg|2?4j|4ihH+Z8vW9J0?W1eua&E0t#0Gc`0RP1s+eDN(CR-mZ1Uzzp{4o zw}qv&rUS_joPdZ;o^S9P=^zG+Jj*dAWOB8o#C|JrD3?>bD|XyE7X#4{p8v$9?03w| zdGA&h{Op4DA569g>_Q>3F>DE@=a_*gSnin0D3!f{_CD%pXlQ-|9lx43+)Xm8@jKP` zKPGtX8M0#2wN$;oXoNyAko=!@!fB}eQc%T;ukAf)cTQvsQG`ejCI|ql6R=|SWtW_D zm3$qVXH)%2fCuc?5jBJ53B3*#fG=sW2Z|D+`ZS}vK7@B|C`RRM=?E5-jfIFJV_3q^ zzmZUG$6Ap7atDVKx~nPcHcgTsu+jxd4f^7eP4%j5^TyN`5Mg@Uy7;tRqtUz}a+VxF z?CP>SLo%>XaIR(O`vE7xFaO4B2=fKzD|O@~@*T#TweynXGpz^yh%80Q_g1%y)J2Vq)|N>MnOZwpy}r!hna zk||d=Te{wTByBqfToL%}P|%VW%CQ2vrgF^^`9s+kJE*KK${ioHhUubJdD=E~!P+Ml z!y>d-LfD$f6GsYRu)~ZFSF;=xrdl?#(fl(ecN9hvt1T7D#EeO6BCBoqOh9M?;Z#2_ zyogcsYX1Tb$(tmt5;)yf3o=;eu;|vyK?BM9$QaS$5M0U7@3- zD(!A*W2qY;BcA2fqnCKSN{d72{dPs3$N_PW`0MYIIf~Ah$vwzjU%$O5{`0`FO`;XN zDIeMN_p}8aqH%Xw9(m{}K8v(|GrvPVad$T#J7}t6?apSEO+gy+0&x_A!Ux}JzPb3} zTObnttbR6oQmr;A7kqc2iWddb{U#|A$kpC&h&H)Vhi#WC;1GI9P6wD1wZ$+IVYtjy zZD|2u+it1e0#nVRlmu=dExAd`^`FkNVAJez8D`>-3?i+=8|t7%kCN#p;sroBahjAZ zP4l29_JP1}fj(_r%z5Rc%WA9$jLLNoLrf%NQ?qHQ$sCRhc=4{GnOSs9AVi@|%wd=r zJ&yV^;{Sv8{;AoHeJe(Y&*r+X6 zdhmqyPZd#|0eb{W(gLIvc|zA?wP+Ru*pH{<6QbvslJAGWwT$SbXQzK;? z#dC7MA?g-gz0y{sIlN)v*vFNNfirkBH44v|jg&9dx%h_=O->%&wqv<0cac@(V?=@Q zv9yu`;p_^}HDBc{l?0%&G<^&Q2G?xwJH*XkMQ&Kng)4P_P2zdF$uNB2YPIAC6c`f@ zVDhyLuxvZ&$Jw%4CQXX8|7$G>K3=|FEnbVc&UR$%>1k1$%G(k)3L;eq6fPiO_icf% zj9@e>4ZpcffKzTy`ywmDFX=dgdKe{cz5gB+-8S9d$i;$hhL1Ghg#GBs)M(Xd;OJjZ zAYM!x0YezMjthATIbLHA^G8{v(eNdR>&8F)C`tJb5NCbtwzb@aEdOv@W|y;WCzSr& zk?`Y9OLka((t=bir^@bZ&ji6LVQaHlVWemS0KoDuC80H?w%@n57(bwlhzHb+8@;Qt zWv`jlPJoW5s__F>J($anLy#>JHzlT#^zVORQXcIx5ss`ph@@f^4T)Sc-vO?PJ7deA z-6P(!$PjT)smTmWcTDImM?i5YcUPILY2rqx5tb(2 z%t;T>K^K%oZxU}HPqfkEv-k(=tMU7*e;}5|Ms`}RT6kfcrYCm~2pW;RQ507WCDy%| zqvUF&Vvw1)7T65^VhBaqpa0oUyB(+Rg2)vD+1Iuvs1HTv@f*YcA^<=Q z!O$5NbW!C2(tq#)0)M83;`^y@fBB={JH{);-ZtMs%FKCsw9k#wM9afwQ;zD|Lh9R6 z>aAdvEW-HKy%)5a08i6kp*!sG9O?l&qNEg7nLPWS_mVcS{chJ^xH}fId)fqO6c&%S ziT8uGPQOwh8!f%{n2Oi5-# zMyempt05gd=s#)5jDS&=tOjK9+A=-M@aWd#WqaPgC19y<%Ze4wr1uI713F~7PuGT4 z8yyB0j!+XVk=9~%0s)&dH-ibLE7<5azUZgEeXrnm@UueTS*C~w@Lu9{-BZywz;v1GqN}yTzE^aQ*R-8F=1o=(iqme5vFv-g`0E=yOKtM<1L`t6Du0pyc6 z;unAY#j3#I4}#?e0bd<_0_s#QmD1yK25k5___XAogu9o0mI}812OqqJvjY5n6X7RD zkkb#qrj6ox?ax=<5oaJchk=WZJiL zxD*AwG*nIi_~DSDIs|U;DADIB)OCB%vjB zbfpKFC(CYIHXL6)fMIr1=9*~;#|Ch>n+RQFrqejYzex1rFNDoO(uwz>{w<_&pkIC@6L5tReaO}`P>=dF0F!ls=e z31Wh1^Zo|7PZ`J>2joSBXSnp?%oBdIWw&cSAjr>+&i}fJN3N9z+#Ph|Vw-C>)m(uW zBdiVBAbPilmiuvH_;6^k^5t`4tDvcRa3IZ@kS!zr?!DF2nf!WJHXV6~!F{zOzygHq z(o{PZ>F`P$Aq(x=yTW2uT=>-)tL4lWQP1AuR#S4GY@oGZ@!fzjcaR?pRe)q!(b?)1 z4r;Sb9!v-Yxb_DG#Ae(f(S_pq>`B~mcrp46tP97uslT>jo{v#-jyP{-nM73?y8NB6 zTvl2*)D0JpGwMz)9jOmu?EU;M&4tJmLm9&H568V<=4YAH{_QKY_moVI zvB@YB{uRjx1YDwZpn~_{R{H-Mh|KW?WD%azLq%&@@~fiWn1C#u;ubFguK5|(HxkbB z96wG8xIMppR)jVyB#~i`z6nH8D6OYyCS|kW@lgE9ldjbGd+;_8O-8noD$vdk;(wPOzeBd#18T$~1b#s|wC8YIB3sWjll8GWDS=injj_OW2_6C7uFp1g~;s;<1BlA8J{t}xw0l+$nRFk`f!Ewn}!LyI)3Q*&q(lK$kHuxDfhkz|9BGj)?Krjsu@(n{Y~R3WeV3wm?v zZ{4i$5!dExyo{sc6(yJq39^sQEp>N8Yh2!<9)5@yQxklz0+RMPDyO|>`*HtvkcF-> zD#D<7Bjy`*~w}Z0b;R~ z8xk`rMA{R2h7_g*|Fs{%^Y)dUL&fabeK;F0k|F6aIX|i3xgNu|ZU6uP0YRIRctgpQ zz=D5}5rGK!u+ir(dN}8Rix=?CKRroag zFg3h=;-n6EquCOVrJuJcyOBTnYCpj*nXD2dZEzr& zxOUCyo22W) zR_9RQCoA~YbvaKU1L_*Fx^}T0;zY#>-9FJo5GMqj><~Bs6^3__I%?54MoHdrE~{&5 zqQ(L)h8gl4&F&drFTp=9q1}wqp!U73GmtGgG&!#mEn`AR+OY>y^!~;}CH>&?d^AWm z8D#fO#~Dkj>qT6S(Rk;XgtpIhi$gIDJz5#lZqaeu8!)Y>_ zXRopOAVYQh<(`Rg8D9wH24l}IMadUQlWe*{!kbuHMjH|!%^kbz#ICu4A>DQH6&cvH z7h{KJSfK9(#_Eo!hvkep>WFB%WH_qlP6%jkA24JC9I?lgYw97|UgcgI!wgZ>B*^tx ze8g;HSBHb^m|%c8-86-%`aU+xpU?)(*%m zPpeI&CXMGn3S1LwXX$+=x8h};UXYGYWIthm27izJjLlW9uTpR0zyP^nQDyd$=BjA(0he_3F+*ErSuit$)`q6P?;1?siN>0C(`Ou8W}ZkH?hm~ z8)eAAvIBjnsox;QHbds@EF3yTZ$jSdX6dL{u=}Ln%|vwtE3l2dY{zGTgCuxHBs2X_ zYusdEdOaVMqa04%_BhJlQMdWJ=8!*ZNqbyaP?vN}YIDU#>R3mIYN)Zg$6>6htaodj zJl+RzLwU=rPWHdBJWb=FP#()lSas!wM}{tZHcyq43jm>!hOMp#W9zrWnAv@9ZgLR` zXLp#L5-)~ojz1^pTkevHlP@vY8D7sYLbqfX5U&Hkgx7v^6!-S84e+X!hw39k2L1#+ zluW0-s0neaV9Ky2Gx1XN>-*K;|8ewEiS~DhJt@d*Lu*3(18M!<|3o5nkK*w<1^Jbt zA$V`BtwifJI3vUBZ!fd!mdT4D4%He6Y)oZJyptuz?;nW)qd@@o$6M&WC|3Jb}=>y3KbMlGQG(6fk`|6<5>lzBSF_4 zDVdC3P{6K6j$!LsmD)gW$jj&o*cCvaX)~D$KsHorK!SUc^#J=G@m()`wL1FQJb2yB zIortULMD5N-_XJ7DAjtbZw{m*86`<-xlO>p%_(j+)BNT69D;9SnzULZ$)BmJt7#;s z%UKXV#Bg#Vy0r5?w}cdSQ5_jTU;Dm;DreOfNLaFSPK!gEr z7L@2T2t)B5#NUTLKusIDGpjb3bv0~GvbG3?iO0aH6{PkN8QiGApM`o!Jy=-qAO=+z zW%fXz;tN|OS(r^%nB=vIN;1R{y7Q|ZER)D|98BVIM<7yn)1MCxgX%&r^3(vQ!_^K` z+Jg_THKlQnvjHxzyEI4DNxL9yhd6M(-Q=IJ=IDEz2quXYzs8Q0N-4=4d;xYR%I4LY zlDqF}Z}+5(+hEfVcj{eegJaDi)AjzbX~eg_@Z(L?QeF7_c`cosnLyf_sW}PlG<10@ z)mN(5iAea~T2zSO4GNremeqKyq6s1Fi{MokEVwPIr(n@IReWP15-1oJ_}*pp93*S3 zbKTzexR?%5HhMt0PT7C{&>0sk64XwgwR1cE?}*G~BMI4Y%e$li zCv7W3%t&HmH+%-P6XmSJ>9OP zTf2BVu#fDdth7uJl9<%gZ{)n+qjf3$RVl6!REf$L!pzvSc#dNz?CeW)YX0TfJSB|= zT7iuHvFf^glp3iq=t1eSG0CyII)lg&O$Mct2ym0>STxn)#<$XEx{f@nn6`4HlD>{C zC6vP=@l};<3`yt{DG7Qp-DBrCuc89*LopPrynK;a386mFc zZ_Jw7;2PaHqDT~p4W2C?11d}&{Iz`7UwA$dwPs@fGh5x5hm9IQG;H?Q9t5Er*1#c} z>oLbI^Kgi+RI6^%y7WG>2yD)uGu#n8_l8Du5Q(ZJKGwheP99bOG1uT++)x5 zX#xx9{yJ;I^XDJB%e7hc!E27WEfE=B_5kD)^?`p`#Xw=R=0!dr2ZZzn4UY4Y*twA7 zH~O3)B@t^lU(3=DpPjV^N9|{B#?r{7~-yib&wySjs+W}^|T4110j zHLy{j$z7qZm)Wnu*t$tHVGTXWE$SlU2-@4Y8IlAu)%eOFF&4s^lzPYI%dN70*%tY) zW>wz^E|X1?F&RJ8ydcI{`zMZ_x|q;^6&R`(3PO zw!8-|PNsbR@y*;=i}OfwE+pp`gt>UQXHw@A~(v%Ktc|@TWD~jN479JkP=_K>Y(7l>-6v>#w2DuS0vpR*>eTc1` ztrEiID<0IEnQaTFAke%}OUf!YVHxKDLP)3t1WwQ?dfW*}j*!-)@35Kg-6xVWK&suV zSQ|gfK^LlcEYO5(Awx(bSMzVw-f2ELtaL6Z>SRXAQr~>)PJ&do+J$DCe$);IwRihW zw^^#@f7AcGC^pj5$qY zglxLLCKl#*-;MZtfksM237HWbqUT&UL4dvZ(O346A2H9QV!r6DW;Yv`DGKY5kol_2%ReYa{6&;4HDY%LGG4J&F3(NGKj0ADE-%7q;7NF;+dC+JB1eSjcjXx?@*K|K8kCz1{52sNUmfB82m!~h zrqMG7!4 zefZU`M9?0vDEOB`2!YwaMq*dyvWZ*0uA;8bKIDX=qiO8wiHhN&fvl+w2=-YI{uf#|h{tUciuDSY!^o)UfDNPFiC zs`n4VQ~0H$m@p>ui+iCV44h zO0Mi7T2-fa18nQ44?*I^d}I?`80%a=4*Q%OHmo9k*Q3^cTBQONd=Ni@4hJC$TSWW3 zp@Co1Kj|eBO(r7T4P2=b7M2a3JsC0EL}l#K=#hU8QLH-IMNyeJPDezI5rH zkg*Qx7_5()E{%q}xA;D$nvDy{`N>L-yDEc<&?? zg#{YXp1cbPn1= zp4Lgi_6`1XmC{K#t%D(e|CA5NO6G)1mMU+0bBfI80NEOud~U>zjH4U_DmL-w{+J?u z`@_UD!cJ)nQ3&4UZ|3L`aIix6i=j);YEVtJSkF35^PIs5#gKueVB(Gwu21c{q8(vw zcRU|2`+v=9Rh5EMuc>^YtNqyX9*2$uyk^S{FEOS9!ybAa4no%t*?Jd zdA5q>EPQ8IpqEW-$)XzK%K9bdeRQXbI)UI$%8T!;fR+JJj?JoU)OfH;jN^I4nb5Rt z)xN&|&PUCK$tVjt&Ig-T0;=+^s>{$bpX&Oydu2sBjZa&6&Np#K0sC1S!!$OrBKu{) zAJjk)_zR%n=z?&K&6_t(>AUNOGv#g6IW(ykv&|ogY7Dp+eK+Pr_p30MEN_zUUrQu0 ztrdJCDrL8;GJtGm1T+EQ-fTSl=QTF<{n+kmgKB!Qq}9zVvn0m%Tcfu_ZmcXY#WK98 z0-HD^VQvj36*88Zn{KL>y?0!q#X^#3)oh|3H+n~lQwKg~AXmF>B-d8?!x4QPbsJ%p zJjM*jD|Hrj<08B*?z_slPReda`Ayf$Tz5AP^K23KxaHP#ep^%DLlpzDp{VPL_2rbV ztk;{a8?JjAEA8t~-y`^YkkYBl9%yp_If5l3Bp6oO;yvJ5+tEKsd}pqz(+Yh7ukKVQ>F9NIZ=fW^K}9c(jHsO0K$J6<3*!@$yYx+l@{d zZG-g?rJ5hOQ^*zev~sQU5e+KN*WLxr@dY&EY2+3q^J;qczQCozUbd)YwymOH!zEJk z$+o(O=WZ?_xoXiorc|#+`#uxRJ`l~>FHn$|J7yup&5qME;rEXpgthw%6IOtXY|P?* zQxS}vZDYd9-}s=<6k9Q2iLP{kdN%<-KKObli};(< zVwSox`WR8n5CRJ)b=d#5&Xmobmxfv4E@xnX2Ac=S?biqn^{)G!# zv%p94(;gQHfR8BZU#jpg@+yXyRG-gbtL?G-h>BRN`#6Ro^>Z+FcEk|=J@mgBYSmI{ z)f7;T{>%&@eU*9~`98$ec_^RxzLx&$rXVzk{&0Hcgk`Pp5u`2G%X9re6Wfpfb@oFmtSwmZ zn^dikt5bd+XsAAUjEv_w5{IehlYK-5tTq!Z%WJq2uJzEzuv)j__@|Sg0@g@ur+r zny-vtjeH(XaHT6zb(2r!=mEpjT{}^V>ni97wba2*#z(0t0Uhujm9~QIl!9b3Y9x?o z#hAD=*We@{_a>tP-q~50foX10HK)u1ilv8T7Y&Eh>0%|=H*UDnLD0pO$BD^_ zwn$pSLwXvG7%atws!*M_OS(z(_qFBXe!8dVMBEOkJYuJM(6YW7NU9U`^47$Z1Nc&q zKXf_2`W1@_tJR?I^Cv&ry8&g>lj?KWfA#2 zLZPoW4ow5ibvn{O9J6*x9IXwR8n372qNi?69!y}+Vc7yJr-%gG4gWgjudFEfemyu9 zJ|Ox@Kog3T08Z3mU!G@3`Ps*um@0ixG>QEo&!?!{J7al;SQ?eo_9aX_j6s{Mj~0QmuI(ijSXYM=-;?4-AQ=1tT6e@H6BWWpv1Q+4=}iMdS%u43DRcF+FfskMt0 z2g=GwoiV^YUOxcf4$w(A2_Q}3H%F+>v%=tyn$?Bm#+k5;g+{lUsQY3kp>BWK@+ma~ znqkjm!-a)PpuUU61$cKYTT4TT87(0-Z1`~82Gu2|)tSFZy-;JI(1a?G>I978AYT%F zfUPR&%bXIEtA2VZrzv>|VYOkHt;Bmq#gb_o5A@i{TBu8aB*|advgI=9 zw>f^WRUC;4G7qpOSBt^4$d-|R#N9raew0&35nmzg&7U()B>UtOz;2DCm|9B2FV*%K zhc>XbwvvVfzr^$e()f8G(uN%Ue883;;h=bd^_U6ECq)TBcmo^W?XDB_3*F=7&_Asg zBKMvzZ}H9Dd=yKKJ6` zdPZhi)Xg6)cX-Xz*x?Pfj`-yHcH!guGnPGRS|XHTxm8!qYlNsd?~0kS(`0=UNut4& zhM_QObetkqv!VrEcS0@!R-h$ue}VlFLn%;C)sZ+z@LQyEC!h_Cr3$~+xE*ac&|hVo z94KP=7)YzVIp@EIX(N3RDT`MIbeZ<>li#jD(b+>kcIM+>6$TU>o=|BtLYndXM^Mvp zmO(?*G<$gb8rO5d^}1U*z>&YeMivgC2%p>`IX3u1+|_cJ|T9Fcv`V@&zCIW@I%AtZYrFk0v9?@-0B<|I4( zy$E}_^H*2`wiuFdR(a=P`Z|ziOCJ~D+v$5ux9xf^a>`ZRnfw*+xTXWxcoOys@d}o- zlBF#te~6B`T>L%e5Lv3#I9{>xt=oyKIq5OIHXCEIF3X}dI&ct%>iI-#ffZIII>Pw? z#R-AyZcE_~2M1A<(grUZyy!1fOFBBt=We5A^F+#1>&*r|R}r(vs(Y!X=NY6=RL6I` zcWkq11S}id*^`9><}>>GaD~H>sUw(fc=HGlsEz38li~mk;m<-k`eaW7joHWUj#1_j z6>hLEv3o1Xe({Qsz48OBfxgh?cwFpRSU%?0IIUcxb}c^|wII#Hhj*n>vAw*0%q)MWe>zZuJw>{x)Y=7CM>S zshiH~Ae-`4%O)EGoT;AEHzh%|F1NG;Rq(Yj|MP@@jmsJWZ*ZH$$kQvgk*1=$^H_~u z)p|)XQ(j+J1F-^=Nfn(7JDvXM71i1eJia!9)aGyBSS6pO8TkVIUkrXt1aTXWyjzIC15YIO0(lGEm!r8 zZpQZ2!t0tIFT{?z=#Og=R+2H`jb-|Ii9lA|)nVWsqC3q8Sh@r|O^u%2-Vry_{Me+o zDbDG8uG2$S?(1q@Gjn5LPBy3LI_7CXU-4!m#`an%^n!<~>p%Z;t67}=_FkVO_FSrM zc$uL>AVOa&@lPUXau&opX@@Gh+W#9jVhg0S{J2o4W<; z#JgeYu?%rW7FA=;Fp~{Zbp6H1dv!gkq=uxRhMe-So3wcO>5kjs+Z0lSF(M{HF-P=Q z5YpOGRN<>vl|KkRl0sikeGA$E4+6%S2myV$@;cw_(wN%$=GOdXM{q(Rl5(N`d$qL) zO{>gI(u->qJISdqz5Yo|wjPkRh$Ma=&V3%&xf$dPV!VJ5`+O*h_w~#=bhs=({pU2G z4j%y%EN+*vvqi#2@BeGzV+)0^<5YvMY%FTwDKmlJyxhO{4PQt*Hf2@GMMS%I&I^%6 zdkK>Mw1&mKHV^IL6L6~9Uf4t`h$BA7FGyMx#oV<5`Av3uI*=YJBA>8E?hx^q95-%p z+%W&-%~p!Uj0zn#0-Gk_d%^*oGRr=D$LL+>EJC_pP>Vy0yt;odmhllOza29yUw&Mc zKyj#<3--z7s$d@QU88nwtZ;v@-rUX6CceIz-;P=AI8LaFQGR8afacWhEe$@jAB=~b z{CY&h@Zv*0Ydy>xdlFGNWvsJpy7=i#|IF%JcLy*JD7ajc`tiF<!}8fBFvY;C&gV3 zh0bOXnWXbLEE(zGJanbQR+_xlnjGHW6$pOd@`nS(hDJ>nm6Bj9L_!4?HtW3!yplDd zhd?PJ1iW8lmp_kgE#%NCw*|Ag+M{)Nqm!yJZfp_y6~vJ_O8^}~vfHKH_wm87o@ZXQ*x6g;rh_Gp_A!RO|nj~l%)^Hd=qKrDlEaEfJh9Nd8 z1nKcU5+;2%V#{U4c16XbobA*mEcRge%AyoYnqWqKn)0<_U=gB%f)T6Aq3F3%Qo5FQ z9kLw>a~~TUFgQFx-7B<6sT-IUp$H09IdsI)v1Z_<=&0#tE;KD~i}2qRjQUO^lDalCfZCzYy#` z{`08Q_WSFEiLh(;hHo?kiZS7iO@!=GL1Zb-iJNX*@tKa$iMs8gvX(sEV@irR8rB+f z(`IF+v{WO0yE5I^vi!&+!qc8%v+fK4gdWpmijCXHW6ZW&li`I92HBW^00001L7TF8 zL&=oDf`7e+LG6u_RuFa;yrbji@oTw|LhOJ^>f$4Pj;y3l-$C3$dTnZ|1En1Nu5s{dAv>|=J}jDxXXt!TyylUe2h zvSlZ*zilsPb#xubGc9X$nuj10Bbc)DuylZ42X(ZTwh3m^XhyGTXDvATAVUKoyYo>A za;aX*4+9krAcVhvEfr)aVzZj~?+oR+xPrWgq(wKi6N)G?4;GSa0JjNsbEcBs+nW+% zoPrrd>ejUktbVpWDqqOB0sSecX|8iiXpn^*7ITT#8V8;7(kctxgYPM3^a+fTvgMre zBHse(9iu=Hm`+TIrC`z1%5Z`f0m>{JIE<*2)(6|m>ZT%Q}veHPeQ=0}4B zqW_y_T%a^7;N5)G>fx%XXSTztyqA~17dzBAr@pvbC>Z`NsDU~oZN*3jj>75r(Glp2 z9Y!p%4K41Lo%KUExofGD8*m9~ zCUh?GJ4iZVj_6PzSSA`1-3?!fG+^gM@d!Cah)()WKu|DdS!Y^5gP z_RvM}9xN+OzHHP($a?ZU<POM_h(>42F**|kT{o_F0I{1Uc1X3L-m zKlUA1#Yf>`0=6~+{zf&zFH+{}5?vvH%{*hyf3lj(eypsj3#;LACMatPAK?kniLfus z?2-k|G>8xIvWW#(o9>h!<=oIbGY-;xCwV2OHP;1QTZO!Hwjz>#fjsUz0O%seVLJJ0q*8gP7+a>a#;Amiaj)Z8AGl7lHNg3 zN0&8P3kw*v!T|lTnEiuMLITH`f#*K>*W5er^gy1r3pT3-=O}Xw360R>==3%BG5W3) zeh_GXl-PkmZ>o6+6nn@ymW1$bt@w1;w_9yr13xv5N?vn`JGsxd zB6uuQc8&SlT>^IaaRX!h_xGPs&W@ecm+<4`fPqHW5!;$E^ya#qCxrp^>Q`Q+CjxcF z?*~_5guYjya~#U_b9jQ*x3Z^=1Y+jZb$cF7V~b?*`?WLE&IimeTXzAsXk=}}^;6vC| z%lO+-wD*eNqg?3eTaZlTOOj9ATK*)_OdgN1$iYBxA2AWTkKEcPP2nGs-71y4MfzS> zT$YrxqY})4qMUQAxvS3aWV~+~8s#kv39f@i(mW7)>rdRwux)%cD$X|0b)Yu`KMFvwBr)wwGmi*xb&1ckKzvgLTfb zKduic>0|KBy}jtJfXx@CGK;b?@!7;?w-WarhSsIzG&Oyt5#xXSt6uzK35Vl3%spc& zv&+X_65YHB{sN@+AznCr$PFn@ZM{^akDD~uz{V!#wU?XnQ~4sEJL zI%o{2D@9N~%>k&3&EXR$+Ww57Ge||P`eUn&WbQ^{`dBP9vn%{X*GJ;8QfbBh5h_HW zUak)ze9A*5_FRN&&k8-VT?Vi{2~X1e1cPTuIxfq+1TjtfI|#De4)!3!^Sh8*oqtS= zRQ4nFbo>x397jbFUa7Q*Yw5KiXKCu5cglJ;wMIzyU2ACu2YhQpWWmzcSzdIV#mBm`%eZ z(8r)Sw0Eh> z*~0`t?y8?PPB*sd%47;9kzb$Sjhnc5Co-wV_64OSC9m}C&Rl{61Z1#TlG|okZ?k<< z91g1Ttip5mDwwPCwHsr57{CCp#-fdk3{bT;`*h`E?64aUJf&*A`?8>yN54z*-C>X4 zd!IOY&3X#tIEL!YQZ*GJq~H%n2ET}OBJEh|>QZ!djrx!5^P&MG?t|vYxjplqnv>V41wRzJ(&^(+dsdX9 z(~P?T56TbM0_ZaQkR#oZLmu0)oi8$rzWt?T7so}Ev^-W{C?0Yy)|6XIe8j~doYp_Q z0-##vWUhF_nnr7~cD#`)0{-Al(VKHZfTpPW_xB;0R(@+m7^jksDge$EX@9+31v%W^ zB1FHG)i|KhLSw>bM4MQ*b1m5F22T6kZ>9MJ->V5%!2D6J^|4!3;i(2=I^ib~_~NlY zf!$Zq`eY3w3Cnylw)VuWE7&!XgOLxw?F=g_%(S3@kI`$r}YCqiIc%^h|k8tZqBl`lJx%ag5 z6;bOuecR5?u#I^aWsLE?E3*CvSzo3;Y@c)Z9y%!ddP7>rytP6<;sjR6=I$JaY00_f5H!kj}A6MimW`VLL_M>h;A?!xsL(TNqXlG40z zywbYPr>lNeWSe*G6-GBTpMA^s81l-*N;wL5|!#54J(I&v?;4?Hf1lK|FM=f zhrcWfliln_a1sKFU|XKNco~^ULjniT4U*pn1M8SQL|}v20E}HplvUEG2s+bxSo?6$ zeYw@m5^^XTp#>dxo1hZxCR0$L51lv<)->J8{}OA<@Dai2facjCz(f5n5rvJQCdJI` z_tl4GLUO`zU?D7#SWS?NkZI)%A@r9Z0*!*J6+RKEWH_>wF!fG7KniI$Pj##oIt_G> z`izTtbpZ7Z*wy8DSb%nT?^je6^%%*2qcm&h8WK-q4Ye*c-Tn>J zCb0c{+iM7V=)s5mjJMgTs$C$o!}7lAK8WiwobtGnYwF>Oh32vmGYe4FQF291h)AhO zbU_PBv|zE{IM5_MN#y;n3(0as@g+D>G*fFxva`EpU5*_%=#yFT2c4Izz1I2#k@j3P;7xIGIY?(x|gxs>2D7l(tkN{LbtG`%d z0rYGikLMI2IZ^b*9V6j!y>eEJ&hb@kQCK@%`nVF;P|LLlwoWg>i5m{GS0OFYa*#6c z8tkVHNff9*-VP33e1yCvWrxKYXxsQ#|8R92@t|Svp3DYEhCYk&jRd8~V{J4m<;(_U zVS0R57+?N>I5C0p4h@;C`PSzDywDD-&rk;XpU(F3gSw#4Cf%rznotnqkRSI8R|pdL z&n#jCTT41iJ~Cs=4s27BcF}EpheT5>7}WAWrQkRMc48(PIV|Zn0XTNzGs#1sZRpti z>3B0u!Q?>C?>WF0G;uf(7U60Rn@^adQ+wcnlm3>D)FNv#aFj2hZJV4DQK%#^yTOG> z{4P`mlf=yYR1GR)U_Cg%Hj{221}LP*M}!lQ#hh8)Y%2BFPngW zL(mDX>IW|a10ZOqzx+0RHn3#Sy$j&GYqVXu4s-GSgN}08`_JRyBbIwwpt;H2kMmo0 zWfR-!jY68!u>oS`K4}_*e*>IguWlsVG5I&Os0A+5NNo7@L=*jkr~&hGwDD$9dW*-G zoOGEMwVQuIf5{FyDt~et6xk7*yo7BfoTnvebq%V% z_-5GOP)+yC)pATc|zo`bDQcpK0gUDxCLjme$Rf+iQ?-N zrILbMR6^JUY+uLLN^l{bLLWB(N)nhl?+!&|dL;w1k*5cC^_!n8L+9H;7cHa%Zj2+# z#3L4Ykin$6iOkvsgZ8BY(Yi$GHaYf&A-6!DN)FyTXkmYKJVIGv=epFELFZ5oSinq2 zvBB-{-NBpYRcIdv#2U{Yzd|46LJV8qHK`S2UT<9$XaL^$ewD9|mG)|h4#BJDqWE}= zXgh1@|G>C35B9C%7X0&e|GkMNH_kC`l-d|_PzMMu>DBupi~@T5&K?f9ER{_9rMsVv zHwlGbQl0mz-C4N^uy!jTJX5_Ay_{g6I>QGf?oxjbd;=InTcjy=9YERysMgFgr&*KMMnCV*x6DM*{M^RAPq;>+r3c>Y> z`Q?KOZC|0oq1lc|XoN%z1RzB*&@yF#7C6FVzQI=yh^Ac1zp-)Y>xD3${GBsoqKB*{ zm&58{9wadKH8*F!IiraOdzLyxYtVPn1M#;`YM9eb?FMnxxGUTDrB8kPWLd^0+}H79 zpM;b{dq58d2Ay61GxdP}-%tN3qA6Cw#hW-IJ#@6+g!04b#7HfUayc&^nAB;fljKjW z$sh5d|BjpVMiYg5xOsfekdwFf^IsgF;SG5YH8a?aM=H6p)n)u@`=xFkA-g3tKS3G{G&8I=)hnhYB4}*8pVon-isLQ zJGzd*!ajz+y@#v^|FKIAyaoNDyqYDhI&>x+4tIDAhRuNX=8rJXSO}D$Qc}iEv08#+ zQ@UBN@`r?e{VVO=r0?~=DFZpVnf=(K5lpeFrKU~2{WBcO;-!3Pje5*#8DpsO zM!AeS%*IV3zL_#L%2@7R7&FMm;3F7L-J!PVO-_3c*&W{vW$y!Cn-M594wFWY3|*UU z{0A~VvslqQIMB3AdiRm(3@~pAuwq_$6@z5<<@4UjVy*s0`639pR6I^pA33AUjHeU@ z@JGAu?{P8ou1yXy9j5bk93rTPF_WOetLkS?{=(&Q#A||kR2e5j1d*!KK&Fe~m&y&69-G!2f0Na(6c8tH$ zS?Xpn5*?Hcy%FVs_Z$uNbt%<4VzrctidEysD4d-`+ZMJ*BTwqpEC2>&x<*xX`J6@1O-G2 zLBM{nzFs~md45tA+n@v;twEPELD;8Z^j#LE*ZDgzrL6>f^{vW%OR7pvDc4jU-JF)`f&SShqFh4mW4LyZ+^Og{IYGvDHV~uWq=1k`FJuqQF06#NkLA5T zr<7TnR=%S(pqM1%Vq;q1;!hIvXe^xvZ!c}RYDl1JiYDlT0!SW zFmzI8Dc_b*O1c??tQ!qJ7cH#_9?x~cRYdr9to5po)v8b^t=(R_ zwGJ@2nXOdrwfYm+t@Q?uiJbl^z+}S&z~6CIuAMK#&2)h@4HW< zB2`9Qi#51Gm+GY$R534h1I2r180ppK5njActn&sfLxC@tyJcKk$L33@1B#csx@Z*a zECSL~#**Sw3NSVpt9n5vi)wT?z4Q6l4n$t~3Rm+p9_=T%b}W7YIxOB1BDfjM7wBff zdz(9reHOnu0p^-#vWd=KJzh=SvND>Y+4%W7m!R=sRQlaiEY+j zF7#y}ko}L7vVqmajZ@$L#FsX_E9+yVCn4dz5=^doga$o@kdln&9LPq$bLvG=(&yB3 z#YjwMvR`AV200~5PN0Gjh9-$3fI9CU$HCMu3Dw6sv8tLL@*$P5dXxFHf?xE1h<@96 zE5anx@;O#hRzj?gu!f|i9 zr=!0WG(>cqaE1u_(+!Dd5+46?AB{%doK!RTIqa~d2^tm)qw6S&?BU`t*H%o_LNQK@ ziVfw@caTw4!xuW3?^N-@Y|HhA$etn=3+ ztUXbAERj}>M7OOAlknQAqF%*YDWMU~O)V)|d_N-_eZPT6-ox3eH+vOWzZtq0o{b;B zHr|%)k%0#zR=LGrc$-{l&rWj7Rf^-lY*K4LG_9#*!CCVy(h+g7GE6AciLl5(OyxjRNO@;@1^!T6~>NN>^& zt$FWP>fm#zdRS0{8_6NDwEV?Cm$aK4TP=gRM7FK9o%ey+>JJrUZx~c$i%zRbWyTPa zbTGtxv@|4**%RLYWtf6p*tLE8ui*)rCX6${U7;&}2*~Lij3fgJG%d@*-|MRMQkvjq zbYxz)te+!DToiKDi7!(X-r4_h3VmJzZqn@?ZvSI|9#B-%L^Q)-IcpvFo+h?&&HknJ z*s720;am$#aNtvhsp#R*TGnRZBXc_^(Xz~4CQxF~{+eoUlk%XgbR(x4F>uR?UHOIo zDFDu)FAHntR56ohv)ypjI1L4E)a@0`Jd;8N*#!`_+|W%8EDz2IZ!|sD;?CfqLQ>?I zx^mT-c`vLa-XFTb)7UJ5!vi!fXAigk*Yd|ut8AoAtEe_Y0MlAE!dgHo62?VG(rq=| z=|;3tFosKl5hy5ZKUU_D70U7NYTg@-+54p*eCe+9_oqjO@&$o?ki*2GoQ_Sj$txDn z#v(*gwyfi{^{(Div9AG#G-nCj(uOtDK_2yKVB%hBEya7{FV{$WBt!7tT`L1%!;VUo zNe+5~sLPCk_!Q{j-%@?&C7a=$(b8V{8hqQ3x57>fOR*j(JEg@^{w6tNH#(S5m-RmYPn$jeY-*jOQB@5+H8uD)3N!m#vw2D(cN! zUJ6fW!K|pdAXcel-+%Ct51($7Wu#SOxpIf5oC4w=T3b5Dib;NNq$~vcchU$u(Wn$z zF0oiD-Ar*m4zR|4qs);noVcPr?kokb>MNhv`&~Udg)FY6s~dzAfF!jMps9ErDH@UO zITw{Mz!>Hn1jXwd#^btnXlfmI=^{8O)jZsEUO+Qxf_(OWlc;nxw*8jTb4xitlI8(# zbIp#%g5q#KMjN!uD8E>H$6z(_xPiJ~5LgS~#p)(f1&V3KZZBg_M0d&Q^?BcTBtf|y z(ym}Yzf-~=^_26bU%Mup7ipHF+;2Y+;!LWW8Mvl}G4wjXknAGwV(xDa6W}T@y5~Nt6|Xk5BTKAbMHJpTY;C>{@h(og2zL8nu^mN=)g0 z1NVqRze&=&g)Z^3=a)Zb`9nP-oN25Xw)o$${AbCc?Y8S1^Co&-Tf4o_>g^|RVichD z;%zK)O7&z-`skt!c=oo)DEQr}w=`2{=O7gil#_2%g9}5mKO;l&;}{Or>PJzx7@Zu1 zxTID%lX*u~eO&q-wBk~%{pHrbS|7U018W4G-|bI#arjU#hW)wiOkIW8!^GW-QgW13 zbbO&_H>wTVZcQ)XjTr;Qc{lxBSYLb-S)ppKuHj;M|E!4$MLKyT+C93#cyu_K*-+TX zm&9?U2=A&;RwvXZ9vFNVR|q6k-qd|&nBWd%W`Zkva(pRkFzP4`TFEdCMp zk9CB%AcV0mMcmn4!$ykJnww&~$kv&BpCO>1N7U<)5<9CmT9v|85s>RGW9$D(K~_Ot zG=`J;^)Cw+3|j3(-z}|~|GnLm0)0^3^8|AvfGdh@)~CP$|6%;cWd7XIpq_*&+0Z*F zcpEO;&Y1`byTuzW_r3%0Pffvb1L7a&BvBN?h_7Y8Gb^rlN23KQh3)CkKeI=CU=SpY zx+keqcHQ$;`hg$AHDZ_ka*j+$xY~&8!1(QapfIr=(qqj`wCh0@$eGz z-2EABeD!4TZtMSE@0-BV>9!0W^~2(Ck2g2ySkXj1AJYrFNx zv?G~iUNTGtuUDBF_iQPeFR=PFTCI27RfW0ME^@5OOMQ4vP{-t0Z$J^0weiH7_)%>j z_CdDT@Wz_>W@d;sG*ZbXxZ0V}{J>K*&dQQjGQY=yQY>OrbL`V$e#mm$OA;+@#Y{Qp1O9c7|v7t0NXmW(pK|l|C!CDz6 zd9$P$2=br)SBmgKq&zA!-QoIIB!vO1=Qjal4mH`Xb0H3E1X8b*h>8^c!(|XC+%TEL z_o*v#l3?cms^q~=qy9>sI4b`}SEAz>?~^=b{Q*TR7GRTLErGXh;b5RtnLpD{MiVqi z0q_oK7d{Ftaz0?}&{z8aUOjQ4b5vCc|gYc5G@+C^iDmIn<( z*yse`NB!}~8O=erRHLA^80h9NO4@>7aWugjw~4t*tsZCHHr}?5LYrPd^y0mJl)We? z2F9{mj30vmY%Bw)&NK6|1YB`8xi+gCS>}bkaXK_0NrAK@W zv8VN_mHRj<0rKHgW<_;kb;Vkng@qZbWAsty9s`z__C%MP1f&0y>_Qh6uAg_o==bda z<4dZo^~0bu&va10{#z^?*%_jAJ65)YVgUVhG$(Ubuk^Nf1g^D7Z2JE(M7K4_&gJte zJphup2?By#b#wjlCwnAI4tX&%SG=s&^Eo6`#AscVI4Z~Sg-266a)>mEk2cE`Aa>2; zFUy?=kY9{@9kQ74v(nzyathNksv`-Tv27hITyOAE3NGwX%CB`0vihdzgK?zi9uyNu z)I}@Wht3qLegvDR@3~nR@&}rybXTMiV_4VEu6iK6LY^b&5&WzcN!m<*Qu4u>p60~N z8&;2-Sikd=xw(_Z$U?eM@((D&hr6L`dltT@RURrk*#P0Fu49ui-m$kt56<1rF*_UO z+qZ<}ruQ5q)ZPyb5THxhWHl66{41e-`#vr4WSSP%| zkr)~(hP3B`IbY7&GcRB)h5*ygtO05Icjv;O4A`NraNT-P&J!#)*KCrc*?(lmuoQ$fP~h(VVt8a(I?cgGx9eOT)vhh8`IHM7Zl&bM4diEZTJWP;E6;h z9KvNc;Q#;t0YRJ6ctgpQz=D5|_zOO8ZLn=6=fDcEq1Y|lhc6Xu^bIPCOSQT4ekyz@ z*IfU-L2=jvZ*TFiE_V6!D0xJ81fKkRCA}4?6yDa!t9oRCq_v%r;rCydwU&-VDK~ug z+t#>8p)VzY>rA7&yZ2Y$rTOG7n1$`YkBuVI{4RfXGz6%doV5~uTfub(^4GL$ff&aN z&*+n)e!-ACLxV(mmN(rZKDJTul8}-Z5vvozf)5$yf!`_Wv#ka{r~MkFN?+b8jiww` zOmcHlbOyW`%`q6OIwPFz0~^SGI#2(~tqXNnN33_?+-TdPiak$^@-j2hu0X{g0B<5U zlffSELNSxu4l2h0u<()uqI;hQf#%~h4)e!^al+4HZ4?^U&hTgHIxwX^q4L8)6pr210sN0RE7Fi%2t3{xPhK& zQ>cxEhDWj*0Z2KrC29a|Va)tjI}a0N4ZQA}b=WMtQg0^&HyojS`JE>@Hku|+LObg3 zwAw@U&th;O!GIcOe!qu&9c}+}74D&^;fNn+-Vat&$hQzAbpx$Et}STD70r}i(RHTY zaOUu`NRFUA5b0;B5(z;j`%BOG%wio=3JtN4$q*=d?tTUIPyt!P9!3z+Ev&LOfNP%Uz?> zaJfAC%_rL51!%|(!yuSMaABpAq#P_R-{ZN6r!ibQSzz`a%j#rSzE!8`n1#0{ZVyI6 zu#Ex21gSgGg9;EvT_8j&b~eIFJI7ox)sGf*(t9S*kq4Fws3#gdXZ4cSYO6X*7{^^p z4YB|CXQ#Qg>5k;@?1MXYWB(@9bJdJd);$@ zf-7F~C--!g`$Prp43E92df(g}>2_>(*cyQ*uZF5hlm3o6GA?Hzw=Ct-EO#7fJrAb%q%LRqMOiKFVyR44fB6z5D;YWm;$JLaoqXEzbkpoMx7)IdfD|?=ffOuNk%|5;Ll@MWW=L_YnYo4Z$G!5 zpi7mT=gwqgQ+|2U0pK~P$;@=UjTQ501z;PVSGP@lg1bv*lN&zEPrpziGw9BUeWKB5 zd)zCI0fE3@|8jG%$M0SKo0H%eOni0ti=a{o1Up8YR(l77=N0`^8;uRO{8BhsBC-Nx z3O`9K4lLbtgkNH{O0efpd^BhSB3X}Z7wnQd^GIkikLmQ1IX4laSkTznAI7x1!z8V} zgIt@))86#`51)A+WGZdAVVFzXdqo(=Do_e&rdxbyQC8587b@44ldep0>b2xFPLoFS zn>TPC@@>J!&s!i|?|abqCmHeDlkHLoE^{Z5uRoJDjd~kTo40(Aryp=yWpp$+SiReF z?qt3qX6K|O>w<`hDYVs5uf<1pj|gN4?{8VsyY*u?h&3cCB=pAC*s%^b1=UggI2BFMWYzAI~_8r-djkAMK$+PXCeK5+gLmVG zv>jtAANndWiYHD#wcI1e7#B7Ky;Y^(pjAbXm)BdP1cVH)0uZMnbFQZRp=7AP@>Iu@ z&Pw37c~{k}x8bc+jfya$lo)1wijTxutg&crDjz0Xn3H6MiBW7C4rdnG!rKfGAs>E! zFlA&W>z6+~KL!WuL7i^s`u0zu%=b@wuNOa;(dOrkSj+pMNYX_-oTv}@1h3fDf|J(T z`9bPd(d#v!&X`;Pe@Uno{`D7;;w8MJ_+Ny?>k;0%9{xQow~Jj6g5fO=3e@iO?LU3) z4=>y9IE}qAIap`ZqsKFl5;qFuzzqcfC8&r%M5~F($6;iOBPIw_VW3#9+x}8!$<%!7wBUi_u9uj zT2fA7&reD)A%!{SsE*ESHF#1Dk}9X)6mPgyTX1Lh;!MxudxX4c{c%?IlbWOp=bg%P zH|5;;Q%`mQ7?#7Wl=ZeE^>xDgkL5Y8j4KrXSqL!jC}&dKB&NX#=USJuQ#x>#ecBE& zDx|N6Qh#txZzmyu_HD|}GJG#uAPo(673>j64_kwRv)uC|4nL~j_aHoFQT!plGNMKG0_0Eil&A% z9txX3MpjU<28SO|_Sxh#0|gZ8YlF5HpIDm^Jn1?*fv(*f5w3fl(o&}sms4kAE4G|F z=z$ejyesP0N)Vx2bR!?ETCGB8*RR$&82PaZjj$dP%A+A@3zz@;HS+CPpzD(2XdL?w}=Et#QX%$h_YP*(ll+UN}L~(R3P$N%2DgC2~zRKChmdGD`^DtGIr0g@v6ib zn|7(kvC6)5Tvv$1>kFR#KJi`eL4*5zhUI=v{qDH@ln3Pb^XXZ` zr;qIWM47Tv+Ehpp$&B!?XpZDKv0;gYm>|RA#E7fdJaZe&A4n}&@U%C>k-DP^hQgig zU^3iVaU*@emg@tN?Y|3jsxzy^z`Uv=<4CU@N`Mg_$Y$)~>dz3s{ zPW+{*d!*t2^_QF+Cw30YJ0|fKN5iL0nlaI|NuW_siIohG!br~H85>nG&`w3KWtx%` z+01tytErX0YMB5twi)+bteL~6T*viiH&@(-@*Mx>u0BlAp3js@UZ^$}1z^luLucFz zV>RPdT0Z+AtFKFL>El(r?TQ}g!20a|@Wrx|taE?yRim|vvSQ(~D6obe>f$t#Mn53N zm>Z^*`Icvp7Z%NA=Y`4#B2L;o!@f^?3>_;Jx~!W&8iq@D`Xn~$v=i1MW*5Dupc|&` zsTWR%TwKyoQ4dLbnkq(zAmK@JH{cW78^iPg0Aym<$v7>&1Hl`e+4Jd1qgt~8cOt9?f zRN543jU0)ZsXL1|3bPN#u{hts+Td>Bp{|@-FJF}3$)*t&h~rai7?|8DK`C&FR4s5{IqIO&Le0yGe{pf% zmf%_zggkcC#iLH*;GO(a?mx4`KY(<>EtlMS35=D`UF1j*qaq$Ttg3HSI zdS@L0H4qKI{Phys5yH7O26;SrM(Tnu%fV-oK z6GMrsh*LSR_8UL>EINAXUv8DO1Id~o%K|JNDwC!xyCsYc8> zF#iwDCptA$fpWXDNj)Z}qw-D9KzxJU2H!;q1x=Zj5XdYA2Swzq59P0kI3`m9nl756 z**ptH>Hz%P@}1{`XBAPq{1{Sjkubf%B6VC74-0(EJZmNECbtg~sldCYaxETI)IGr) ztkv)%TLUnxjyx(Tko`_yB-k|sSiFvDDD7%f<_)qBa15G^5Rlf}AwxycKn5dauvgMu zzHlhzw8$W%!b4TOMK;WI$thyW7wfSCWbk9V^aHvU;i}$3)tBOmXdyM z2v$reL77|sRd!L6Ts2WBS|D252YOaQy&wZ~Nkh|;iLjZVgWagIASJKa#a}<3Ns`yp zBVC50tFcaXw;C9_jLZ-Tb$_AyJuh>&_Q`WxsvDxC_gKXkRq#bf&W$`1d_i?Ry(3h6ycmocQUyk&jVxq! zy%xa+cxH6`0Raf60NdYhsOi`(MgR6Hxpf@<$PNB;exyO-=^qT}O;9Dr~*OJ}aEf7vQ63;?QYZdnq%jQ8#-JS+g z{SNvy%N}zBy_TSVr&9ekrgybZdXU=IO|4w>`xub8AxRN)*ZcWm2JPb;tV~exZ}lWx z5xt9rT23@?m)5UcJvi-U%VrVZdSU=nD4jdcqU`84?486(`@0Jch zyCfJS-k8j<3-!$c*%mBSW>`_C4?+jEN{5n`pMIvv1e!1BCD%L7ZPT@zUI#fiMQC;2 zsO?(&pD@@1sYVO9BY4$zQ@_q=X@T%%{X+j#0`hWK`An2AXi4*_3ERz#WXV>l%{w?j z|Ie6&oLW6HHM}H`dQLQ${62#@V7eU&_KC)S-<&0O8}ROvTv`|Z!$a*=@-L*Gz$bw5 zmP+x8)(mLT+s>Gzw`d_7Te6A(xD1HVRi``djygJ{t z;g^A?MKsuIW{U#g=<)vlGf~7cr6&X;h)o#Pj0d>!Jie($h2p+tQ#gT2QkDCbf}x_{ zs}P4OhiRCgo(|c$DPRzq8WJ4RKhdF{tLdx3gPhsQpb0i#6&#L+^Bw&P1@VBwK0DGe5kBMLPtnxqQ$$o^Gnimz({cqh^<-vm34OT&Z z@@*`}yOh0q^okZwcskPS&d>wEkJDM|tE-E#9H)%*2f;l+s8~h_$_1|WQWkr&wFHnP zJmy&YA~m_7d-qTCjZUJH&S&CPi9oP${PiwE4y;Et|MbvJ4(6Ln|J!&(Tf%QeigBEt z&~VA@2Hz@EnKC*3J7kX3YTb29oXf%#f>zJ=q9v8_l155S2MrI6usu|MPEH`&F>ig8K8ZtK+l*4)OzfC7!TqcvmRNPTQX-Ofl zV@wGXREbB?L?K=PsrV;_g$&!InByz`xNe?|9m=6awA2_LWt(Ei&Kt;AF+cySR8PhA zhW*@!U*1Ui^W4ff(DVxgQlXP`m-l_Rb*b8T%T{39ndWMPm>OWtf|q_3RbP05Ytx!5 z?9VRKdXv9;3G0waH{18NPK;dtgt$-kZt^IRDtiV7GeB<-`yRtyM?xaO3^M$Ec>Fbl z`vZ%J2>w3j_*#P~^Q(N49Y`77pzkPIi2=Z@MC|)}Z%N;3K+wILK}rl|qi*OtFZK0R zLG`G?Yxu`dECqE?!q;nqXtdxhjo@&M8BdpG-B{KmG(SMA4Gh{T9l-v*JOu_IdMl|zdGN6?n-;7 zVxKReNXzLQ%u>d=oFoK@co981;4A^Pu&rej6BMoj=a-5!{t$la!7hhT+wFw#VTB9+ z*AMVK^rV<_sx?ALsa+b(6FeHgYIWWX=VSu54Ccg8! zKqpA?SC#-TW|5w2Xz!xZnE}l(06M9gs>WIJX5`3Vm0^`Z@OXRaBq*g>;LVo`%SVa}0-vsOpv*9XwOrwdrD-|~V0;Ek0mW+h>+hIZyl zpi6BBh~fiEDg;`FT(&=TNn2|wgw`umqR6m2t;l##U!+S^!OO@u@!lq@>_u3%}M zrl1O|cYJP>2J*&TQ5*Hgdz4kd8lths=oN3R;LT)Big^o*KTCvXWDu!Q76{q}z&mYy z_xi>#z5=sjk-n<-vHQpEEd#T;SFYw}xv=L4Q*v8R4y5lnPaeKa^39g@1E4Cptv#Q4 zGy_7yn5^(rNXDq3&=4|T!n;kx_!r=CLl(+|bs$I8nEFVmFH{-^ z!eB0N(J@m~AS@u#Qg{LjhTl23xyuXb+BH*`uSVrQUX=!=GtxK7 zyQxTh`7ZH>&t0m z7mw7{pY&j#LT)c(mpgAzXUS{AF^GWXAcDX!pMUer)}F{8^v-D5r0nI|Tir0s6iM31 za}aZ?C?`TtF%5?Y`Q{98m^OQXRpx3!lrA>$v2ipZbd*;!ybl;C?&1L;<1x`PZawnU@d47z^{bc4O=K$?_TaRg_^) zKev5Ix9^wVUE1kWL?V)9R?5>pF@zKZtXZT_gKNbPkX6hH8~WpZsl*8Uy#Kj)G0KN7 z28UT9-v=z#gV;N_JxIl5Vh=(@6a63tgixSUl{_&61A1gq@lZV2Exaik4Y(GvlTPmI zYnVxRX!@BMtp>@@QbDn~o4oinjXM3w_&%fY#?*?54=12@=}PK%zA^cgx`NbQngi{| z)HN1<;Qz$pPQ9;D4c649?Oa5r!YkYWUs4wF4152E4oGk7giX^KDY-;M`Owtcu}yu?#co>DBv3!*69p1nJt#MUZS#Ub9(J>TtIxvwe#^kL?2l! z>Kxoid+--OL4?zsx!tY(YlfVg29(~%o>(!Tq^9ept+XG`F^DYchc{X?;``ZiTVSO= z%~Jy}ab3{zuMQ-q0^Vi4sAD~mbfUw(4~?O z*zbJA{tT*WDg1MyQ)f7##=BO+QsbJTUgH}%!=5t|>A<64=uvWiXYb4eg_;l(M2mQ= z{N^jgMOMM3Sdbhy)GNkj4IzB2zAlQV8A=Bp4DkgRSEr^33g#G>i@#vy$|P^O-$;jD z_0D>i#AD!vAmM|FFvdPV1!3x?{mWP;HmKNnB~edxGO~O0cR_T>anUZP~qsc zlA}NGLkBA;M<*=UOshnY&=iv7I=Ld6tlAWnmvP}`;%%M(B~8EsB73sI-YjdOGYz!_t|04wl3M;aj;oB zNzeW`2Vo09kNt`P^w})glLQ|Q=rI)7nHX<(QkAM!$LV0f)m|9xiS!fkg+y)`_rZeI z;_mAba@~>r)w4>#kOXE5T%#+Ny=L%Za+ga>zzQ}-qk9Lj85XzBK%PLwY2E(77Fs~Z zl>K@>-Ys3iRHfK1b`|&iS+!pA?*NDby&W$h!h+CHQLVOSk`%JRrArp3>xKmy4bqDZ zCiVsA9Qvb8P$`%+90+xXEZtXiqO)*@TX`F0fYW1`8Z#P}xO|fMf)GR6db7dwiM3WW zNe;1t`(FL#nM5|}{#afG?un0xsI_DH+b;A>>a;&SZW6}JAEZD+!J6k~#M=h6}} zc_T4B(o>d{G-)Tulxi~(>OO2hQer-OH$1x%O?Ar7G00ta`MESmhWH5vq=XRKa1*V) z2??R4Pxwrru? z!p8ev@q-!0JiX>;@J~S#18d015nbK{q$Qh)e{6T%)d~k(SY69B+Do|xSA%G|`?>ad z-YAeMrjh1he#K=p}{ytBHbwmcLa`$hc3~smRODVYEGz+Yxi$UNd_#H5nMFPu`>>u6`peVBzoGq z5x~p0Gyub>xJ#&v_j=6aCM8m9+)*%{(vak#7mUM)hvkX_T0<>BSzkCq@KXZCdn}&) zbIO41O6d~(#I-qYz^s@MEvkJ*OvE(Bo$he|9vcsHK}%+qhNlD;)3#mE0~x*2ClGsW zRP2YI#D^*~3?f5<3+utzKw+b^bRCR=b)^6IDwC#|GF0a z)&ktn0LQ)dHp@3_D#k$x=Ut*M5i%{5sa`X9G}r`vsjGXt(ya5nnNTDR(@&BF^~0g< z@7^^=?-p72IUE_)Lvx{kh>wapNJS9nlbU%x1!6c>9F2%nV>40s2DQai{kU%&Xh zPXYzpe4wB4PdPCgQXlX>x?K@Mc`|A8DW5KNbYT~_EWBgL2_ny>nQL39vDIk?b0QOc=T-scuZ z8cNne%MxTVj{vo|#965=y-|{}#b_tbd;;jizgVQSjS6tsIPQMPdNj1g`$e~}ty%c? zL)AfzWD2Z;KJZAZ2HZ|21Fx6lKqq9(d@v(n*r|H2t^5+u9xGlkVH6h2)dBMD#rl25 z%UCdzJRxH{T+`K`j*m0GObdUIU7O|!d7zjW^gS;f`RAldwt?Qej2 zbh`X{bq32!Q#gP{ro?)Z2FDUZ#O}`tWVY^KcNK5>xBv*@xBoEIzDB3cFDXl6G%AYB zzYd1SAQ#eLAX$gg?FSWftBOLub@KkgeApq*hJ^!z$<&1sjX3(MPkrnZ!-ohs0olfs zz*F}N7zlRxJGqFt9tg})vB9;y-VeOq-ysVa{MXhCGbP_~&Px*S3v=ay^LFXzNz&nC z1e4py@(gMrZMEHX%{pQpeG=9NDHN^l?pt)~bQLg#5#OAe}>+$&jvsL@!sMY%bW0rz$$_ z58n8j0Q{3s8@zYo6g}ORGr(Oii{jjK`8#1!NmvWDIo%IebytI7bUn)-GjsP#RPpcL zQ*VT3i09D$SEcB!1+6(VVmXO3cWkT43YqG<%EmM>fSkALhY!49TRMTFHr!WO*)h8l zm9%&gLj=1>TQ$j$f;J{RejvU>^ z^fz6E3RBHu?osED_4qr-S|b&dwsV@4px_ds2PK#?bmdiucU+PEW)X^l^!l%Prb6PF z6QTjbzwLLMcvJvi;~xhKRY0YvX1!`H_^o3P$vJLA(oqpK6#BHkrkQ~hdBLRgf0=y0 zBCQXsrp%GfY1sQo(6cCU=MdUwpn!T+h0q^X;OPnlZ?MVH^ zYLB)S7m0h7*yqzFPdfbxp?{BeL+|W|#hc2QGQYqn%y3eJmJy#6MwBJ)F$y_`i*b(| zYS?A{rgq?XvWZQ+4_d{AW&xWUwlmpnw^^|t{~jJT!jO?H80ot-XPT8bxhQRy{o#Jn zT)}rm>VT$oPLl8XZMG($^vUXV~1(iZCDk3f2j43)^82un$tPAa=@GER~W zrQ>^f<576FQQZc)*p3KlgI_~Kc0JIMgjV8B5E-jul7;PaoOJb zPOut#nr!_VnP@)MY+R6zIc~Y*^so6-TBK1eLOQy$MDK+nqkRnkN~1-Lq?1pH_1K1Umb&e~3|2!6x3(Xbhk7|J$MRadj=? zIte#m#ZIXdAa}xM1pc3F$#+5RIPhOD*@p8wKmz4Mr1LR&aB2(uAa#jpbZ~UKuIZI- z)(vD)72w+^@|N7{eWZ0p>(>|<<}|vS=q0MpGn$dOlUe{xK(fE4uwk())*9~4`@Z5G z%^oF*y?y?>mG=M57`Lgw1x$b96?F1EYJYZ|GSoR^*+aN%G zxn5EDf^ia%QMNHcrmXtVw-f59wcA64sedg?XA*LhU4{FQ)vCeU%Z{)${Ex zb?zmw#%eiFG{o^#k(u;=xbG8|>hZic+5$AIGrj}+`{fR5e|q76kLitSlFa(IrMyfP z{6FFWzbxo6+b)}(R*Pu9(qH33=q#G!+LAEte_mcA;JxVju7Kxp%!TM0F2cj6 z>y*< z3vD*jw`URKp{rm)as_}4Lo&>}sKZ;Cy=hBr-$Fd}4Aj>uZ-zK8ksMIN?_R^(O&x_x zC1g1I-bbXUWuvpLJt04La*~y7zS^C@=#KocNz!5xN{}Q36gVU#nCN1nvmBh^Zvmj@ zgL7MKM-MAd8cLKtxw!H^?P#QV=SX1Xhp8gF3)5OEWlM}+54c)39BmhmhDMFOV39?( zHSmd|Nq-8_+U#q|OkXOLWY>SMR8(dL9t7A3>ibI2t-f~I^@fQy`#;%}(PLutM(wQT z52E9cwHShv*a&XM+bs+=LZu4%?2kFDZ25z4= zcbG|NEyO+OUGYO9uLNo4E5S*A5amy?=Nim&_zy zeFCSKu3Rcc-A9*-6)XK_zDU?%FC3w$Y02QG;utWIjK-iz`gd z7E;kCIkFGfX`Mb}?q+vgfMZ>i!8LU29M&}&5+we?_gJ?nCi}{RCLeov?B{oAE5kx8buY=Y7L+~W@~Dlz+}wasgff`tpjWok!<(D0yV5rG6Hi)7ap1syX?q zoh5|i`^b&UgvFC8t3eO&*l{!N1IVV>|FT=X)6qU}-^37L!4U5R+yRc2 z#@zgjgdnS*#s}1->rmC_f<-vX1}EY3Ugl%ciQS%?_@L=%V4w={n{35RN448w>Hr}( zlPS!MDZ0y|X3!x9%^)7TY973SOMnU4XYQt?g4*QadXfU|tfxK{pp#t_0==!Q6dCCQ zK7NG3D?&zbE=9S-qug^J!-GlU z{hk24Y4X8KKP~!G=-;kVzD!h)>#1($3E#;jop0e`N*7Qxoe*7BV#c8J8P77C!8zB!JcB$60|y>0~LNJBU(F) zd8c*POfsEpTkv#UitSI!r2a^k#ajg{Z%UVzTKU(*R}VCiFPHeMM$_wEafr>w1|^EV#L%2zW=V;uk>09NR?YHBK;Zj zGvqcFsozokiJC?e{v1mgZ4$Aml@AzAes=?RK65iS>Txv9_XIKEeNCvDVz=6VGWa|fH=)#IhA-$h}5|yp* zsIuFu%EDE8fOIvLauZ*7FPCo}t!omUYB1PzBT2Ere`qAoH3NwM7ZTPP1@fF4ICtnrM~L__<6zEixnb&kV5q zu;h69wt(k%y}p;Lq=lNP1$io^-;PZRj4ZUrQ8`qr6tuUsPd~m%z<^l6Dk&b_gQN}D zuHju?Zl}!D8GYmGQxR=aHtSBSod5q=5Z|)u9O=Ud&gsI;jYAX6EQtOAgU+&ZqT?f& z!Uwmy&9kM5@o$p$2sG@@U4Nk7_$wq4Xy`Kf(28IWtUogqP>NhRHH&IWM_N7w81VBM z+!3BXcD2y%ay7jDM-)*wE?uSwGxj&)i4{d5O%2Pk4JAndxe^w3sL@m@x-Os3u}h;|6r+s_Z8Xc7~F2N=da4UtYmDw;Fr`4rIH|^BfM&; zsI;1~`2|nOe^sSQ0q&h<%``2lNRtwKZE*8bNb}gF^^Wp3v)F1a!-L!a+8jFwo3X}TT^GcKrut8|83I9h1%frUz(%-3r&GsQtFI@7L$aSKJ zOk;2ooMv1Qc93{#k-Ho)Qhw>urK@u8c(JiOdBXAITn9J~J+H_N;>ZCr*T$Z4(!qevb z$%}>x4Ekj-55v45U^8VV7o5?5LEMOU{baZi4Qu&}8ld0e4Sqz}>v8B9Y78&ck5>Gk zUaaoj-;UGmAWaTg3E?1Q8Scrz1EE7RK4&2*7zLwNV*$2@b2USE<>U+_&}aAH>h=#*!vN>@Ua zb^D!nFBe^ERLuGTTk}2SD@YsRhvUHQg{AX7E;c#XZwmo`P;HBD;bqc1y>GHwfy*Dt zeJdXNktb}ij|BX$O#V0_9inRSjU?kn!Y*NgC4fr6F%=(NiliAs+(!1fxVqO)7ff-6 zrUF=+koRXI5QmDVFAw)@DbhGdb0#q7USoc_e;GNolFlwW7AC-p9j%^h?QExgP(ITw zjeMJw@wNW{VtFKxNeOV{);&pr{70}~5zasq4*56+)pxeXz*yA+XwV$%M|Uk!4Eo#R zCC20Ol$`&DdI@p5x&2?21r^XfsWO6mqS))DO(GbVGEXQ(irA~TnmrUKQCRV^c7M!eDc=A4qkYzHEaL+$ z^YU0r`a2L|=fXS{7O=cV>P_O*aVMLc%$;W+7VN{dyBw=O{gfb@fd~J=LivYq9WZNB zo(Z_b!>-UC3zz{m0*4m~W7B-pK}@9bM)reooa?Ml`OHU1V;|lu{^zjzZ86b7n8kMN+ir(nYo=(-*eEuI$M`2*T1x`v4E^6fI$}HTk9CB`^UTyW-2Tb5p9Hk zU{$4Z{hYUdn$iq?dIJEi42NSoCk6C{FI8;o{^@XXB&R;Rt>I_;=w}+v@3yPKTNaYu6V`$5K$0BDIOWdk#Q=5jCg`E$qW zO`#eQ=*RR2&j|Hg&YFjb&dH%L=1?rYcz5T)mby&4{HZxg|HQu3<3`ElUure`x1CEb z`glK@_PN^~;@#kVUav4T@)g1Vs^53%rJtmM4C!hO@UgRio#|%W#~Lvw1%jw^VQu$? zy_6iVn9wclQ{T#yl=Tv;W8Wn7ypXL7_qDA=Nxs8X ztdbg`tcO~LUGBuT6Fj6Gp0oQZIYUkEnU7i=1_u$X49?(}pWfjIxj9|*ioAgX;%;8D zdw0xiO7Oz3nRl&FIypCW(FxuF>qDA=ehsUUo#ab{=D%TTLtmEVyQ^l^cbTFk6OR$^ zv@;0+`M`(ZzTP#HQ3@qbD8e;okKx`Z%ED9;B{C=b0KIEUDC+hT4!RR#K2@HkV;-(K ziZ+^)-N*mP`N_~TsKHu~$jKRAbOE6qXPISSl$#@}>R2;DOb$OWMZ6+jjs1%=gW}C+ zWdT(01&S7p6mz+p4FInbG8!j5ti1%3_>En6OeLvxa(S?0>qnGddPi&VWepozYggWzdK1WS-Jw3rG7s@{K~m2!ARdCCCe+Hj@Rbv^ zR=nd;TTcx;%Le>Txdg|+xqp?8{}KY8y%d&Hl8%b{qtbb{$d? zH=2(T9n$9fbU_n9#*2kmFCs0d$%0`qrB}!Q3a*RBeN&0e>&#G=!Wk#}Y>GI4^O?U> zx^gX;e4n$Bb;*3Eczf+|IPxI+?<@j=N$C@mqwM>MLOp9-#VA&PLw}YR+<_yRqQaZt z84M($*)3GOs^5^>wF58&RK-G7qdlqG!Q*{Ib5S4eyqk@MAz@L)xt87N2PaWl=Xa-* z%?aBt(Hn})0kMI2X>o+|vEf`lJN$TZ<)-w{qyt3EzkFFoV zuk8Pd#IZvo)8lH5x!4s_q3$Q5UlMh3#KSabvvA;)`eh(RDz?@r zWYLbr&;W3^6R4Xf+QpP=M_hMK&|kGPa$+0wX|Esp^2ne65AAS!>#3$sRPjKf-;it^ zDsG4vsNzaA-_M(rPB58o5xY9QKdBVvcAwnE+|CwZJN$58bC@D}-ef}?fiK$4pnfke zO4LjHS%RNLlgUX+MMxOfV94_OiaY+J3elM!lautbLxK|BJaiISZ|l8H&)qNO<2PW# zm{^=Rx}Ehs+!R#|YLmrL;d^%y^fZz7Tl^?RmP!br68qBcZ%(ixj zhe1d&tq<1FK<8 zBB9Tt#8>osKc4>&b8*MIvMoi%j(vH)93uj-$V-0x$pbG~%m$zDuffPOp`#2%7}WFkfUSmsGT891rIQ%0NoI%nv%f4BBR(uQ06;LJb-|g1F3w@ z^9{WJiHNSMu!euAJsldR3qa)t70^ry>Dqf`0~38NU%b`s$h}S!X9y3dk59+--@bhy z;nSMagXyT5OE2Np8Jpy4Zp~^nq-ZoU)lQQnM=%$(-s`9eq_uu0slG{`_O-nLRD*1w zjd>xLn+h(_L=|J-zTil1a~|a+R@mgc&#u-UK=dS04q4cEyq^zADI%r&p$Z~LWTPYR zxVEZDB`mdtMHw9o3}X#WWwoF?Vj`t84B1&6H_tHtsO4t8-%!TAv;+L<#?wGpon6u2 zb}j~BJ=b+N#K@J_qe$RT1%SBHEzPtNI|4fDl1|y4Rgj;Yr>b7D|Z`O z<3Bp&rl7jz4Vm;wG?z}&G&lDme~My>O7fga>51NQ|MSeg4sL7PU?9L7f@CqhZ73!X znFGu1yAO#Ce<<`w79b71K!RE{l}NgeRl?Te#$aV?er&x6Q>hx8VR98Uo0SVlHbUq* z8Li0M@c8(gC2Z6TM%Hs5YA;nF>V;|7Qg{#|Ehu(lR2P|IeEXM&r zeL3=6wk|Tw6!%OG?l@kNmC;=JH+T&zU<2U*cI-8gru=v2bxuRAvy*9{1WFsd2t2qc z5-Fs=Tp9*ESS8ul===F6mdm3vZPem+v=A1znBs<_B%|YVU=qk4;b$7JIh)lrcnn}& zF$Q9-RVpX8!l=!eca2B+;)clX_-q_xR9N)+;NN1sC4*`W2NR3w+lF2ZVeWdG=s0~a zpVJ8I^ov}uJ%fGI!fGvQ6c;>_F1CY>O`cN0-ix-XzCG;VBVnN!^<}NGhtBP2G(P_+ z^nF&P+FH6YxU=QcJzrnST3-Ss`Yn+i!POg;nr^axrph3z_+81Fo|(DpVg1C2ql%Ws zA``L^Dl1&K;PRvGxAUS0edg1|XN&AuY5R1fS6=s}+0L}hG;`c?#N7)%F+N`djn8C0 z0-L+rt=GjiyWjMWCQ8wT>I$5pUc}<1@@XuUWpUVBA$>suz`?o+8yF=P`KE5%ZtNR0 zxUsYDe-Qqp2PUoKxl4G4Qxal-nYqtm3Xf$n0b0ZjxsavhBOk^*6=<`zHBKI_-{y6I zKj3`g&-n{rl+zXPHpR?%FHU97xbOjbmFPfL2y54A&V(MfxcOmGPC*n1?87AhIw7~f zCUoRhkD1BP8q0)m*3`-raTY=(J;Eefz3xbD-0RF5CDIhO@qR@;6(FD6x$7}^(@I5O zkR)ZyIf0V?!ZZ*Ax9MLeU2YbioxT<|kP<@}2un)hMvD5v7>Q@m9F%I*a3`|HROj>e zQJ{YmHd#nKkY0IBVKBq~f_hFn@~MHc;(1$8IpL>Z$NTt`R!u(VpB3cG$ooM09p~1S z7_cQ7*^e+<9I1pU7|E76y)x}jFH}+z1#4N%pSZ}?@Ju7I2a|eQGBt*=2}+HNciP0S z0AokX0*mdQv2=h3Ng0kGd!$+z68>Oa3DMZ5B7p{@8WRYzsxg23`+1g196Pq}Ki2v) zM>_OAwW0liwymih`LoAaHb6oEfON?|q_L(R{jH#)H4b_;~^*=WOXlZAa zz%R`zhKCvk%z0?J)$daF6{o=tJ}kn=R|Zz8XuvE#_Og-%R5YQYuz#N^o>{ZAyR-$G z^_G9fD2(s&+X)LxhSOvH?)~bk0%hwfk!oB3NMt2dIbRihyDFf{`|sgyrVIpTs#Dm< zULD-7?v^oFa^KusgxWXAXPh+7I{Yse$P&tG{D>N~=rA`PuekYD&4UbLYam<3dz#oV zO2=mQeR4|J`>i9WB|qf~8}(?C^RKQ?bR|}-{ioB< zW{Q#Xi*|*f&r#F2`?E+sq1YSOp^`<0z3+h}yNR7Ubo?g)7VMe}-JpDUIS-g>PP9@D z(ZaDf=o=-`+^OY$X{LOdqz{{Zxwx?pm8q7EG&H0bpEnK{GOX#<9at**1Z6>$MuCBhPm<^uGCN_kp3 zM?XV|^1d~LkQA1$jCYYc$cDNrAI2ay$Zjbxn{$Up$>v$!>ErIg006o{Wn_z{L2tO! zZz#PZ{zj*~L2*2(;u@P7ZSkSrmZ){K-;f~&<|{<+3qlLKaW1|?zJVaFQ{7{?9r2W< zhHlqUdb(j8vtR}DiWHKnbdDf;9&f#x)BYJRB7!mXof>wHp*c3^RGj9~TZte99wjP%id}@-;%_h-55WKc00BXo z5_m(&l)!?Ym3|LsF>{7)dW&Bo=e~c~maj!PVq#kO*`pkBK^D zmIr2HI^n4Ctt*z%Kjw>K*S#lZRFmqU`bjA+6VRMNxXv0U^RFPK_)_yH*UkC;dz`}w zB0a5xvt(6+fr0i>fTT(nDxuBf^HoGp7edqRnrmLGGt8|WDH|jMp4=ak&6DS7_9cE8 zMIRI|FTODLq=7Ues_|d+s8mIrypi@qCqUQ)mwu6?ThqSfGj`T+kPn1tRPOKK!mWr8 zkNDx(GFHD~x3LzH@SM5R7nVwMR$xvYH`;RdDam%rQzWwS50JY|r5ux&#!%b&2-kSO{VF6W17y zT9+Nn!qvFbiCi(2S^mF*kh?}f>JcKcVT6Yk;aXvkO{gqyBn3Z;4Jv?%4!ykLTK}T^ zRbO2n4h!M7N{Pf$kBtiI-WzhvA3TN<=h)aU2sZPKgja8GBViE~oky+x8z6`yeCfJ< ziIa&~C$-DiDRO|}w;xvo#FBDiiHXKAbO3cDfyx!pkxz6h{kFDFRo-q!t9^GY9G4O*u}aEsia!#aY#}}3_{%J4mgh! z9kp~n3Q^sPX#VhQv5h5gQu6aU*?rc^Cvqzr$TxBuE|!&}S-QnQTx6Vjpy{D0_~vSB z=!4@XiptYQF9lx3fyR&C4XFOi&j;bBr9E7IIcLhSE@AS!TEdzh?<-?Ne8seh;K4`S zXQG3vO(VdYyaNe|sGl6VUHcE$ELeU?YgJp(w$spQ`Pj(5=)$ND7v`KPZPT*f`sgdA zAz6Pgxb>Fc86G{GjdITEXVd(;AKC)BYgv&! zn~Q)}Av|0K#r6^LvbVlgG;~t9vZ~ewmiCH>BMS~XeWfFL9PI)UA@J`HhLs4gvRs0TVaUu?Cn+SF-+{s z8o6g2}k1r0XA2`AVVgP2A#jA_Pp?}2Eja%Y0tXSTV%GOk~KuU{Kd z6D}KPB;iIG`Za9slG`YIio%xl=3$xwsCrh!P`AO4%00e1TV8};N1(APEtK_wy$oFB zc5i1IDZu-bSHA>E41(`b;ex0s?6L8`Q@Poh)@B!TzXBLsWFkL^BoTxx;m-pqaN|V+~(*0R~rs9C@L^JE0F9rc8P4>_n(o@MD9}ec~3T|^e%7NPVPCe9_L9G(XM@yW#1-PV5>!*P0sAjMhdY&+hpjAafjY89=9iIb)Ns`MeDs3qm{c1ERB|O}cqYn>NMMYZ@YWtt zP(JyAdBJ<3FFaR!O&AmD7ot)ju@xuF_%pXo;Bllj=9Nz-WJQ}ABqj4@6S!M4ef>L? z_U6V|c7vgbU~FQa!7=&!1s;1QSUJ6kJc`TfALJ%qrt(YKHlsJ#y%|VZcKzZ_#_$)) z!AGy4XaxeubCVzz6xNH+lL3i9JE{;R0QZEPIjm@PYY7KU>g)})Bw|swfD^p)>xxRO zstBLQ8OXVjYa&E`S#1Se;QG-;6J8?Uq^lIvj9`nr2b)DV{K%!RR(BpA?-nRIKQn{m z3_SQ1vBg)TQKCu1g8gSUkTdp&s^Hb41t=Z3}l*e33uJ^vgpmv1WTCySxQ51lvW;VE}Bx zYUxv%@eIp==l|}5I^y3m!i9V47Y60#*bndbDH+A-=N2%j?fAb_hkcAx5Q+;`nB!u? zOOHwkE6=v3|J5OEau-+U0u7}fDHuN*nrjPowkz_?TjP*jpG8`NZq+V*r&SV zeBLRnzymlY= zcFL)jn_b7nkqZ8iU~wHd*KG>vv$*0*M%`U7bTzKYvGYCxz+;&8V3g^%9eNR=?s`Yt#j&yl6n4t(Yu6?J{lfTxZ;?04O*OcLI znK|C|lY+OV6vXQR(jjvM;Ccvz$_-UJI)UqO0Fp1(;u78iw#oRb>@V=oQ&c}bdfA-l zZ#gd&(qntC0gGKCK&|*}7{IaO$t4BK1x2Y_x_<8a`GAo7N!7lKKMFg1)h)DOoi29w~u?E+KYb~-Dtx%PSa`0awlrtogr;mvt>i!laYZ3d;mpKaE_ z1iZ+YZ`+57(aLE;%lus-{R8*#mPJ?eL^B>Pn7Yn6HMn1_EUmlx6Z|@&)za)fRCgTd z?)}ls$As3*zDX$np4tT+fT>0KY5}xvoDUr3xE}*03Gjit;uC_j9mm22lQBoj26ajy zwatXIz@@YmJY^U&;kUK1baU-5*GCyG!(xCRXo*o?_dM7S1nbR@I8&2g++go+K4tEN zY0sTcf6fmh%X&CeEsv&)Zrcgaa;pH%(6geoT6gs}J#?S^6|rDQ1)o7J7~%3eF~aUt zmfdPIy%Q;Qo5)OwATnK&b=z>BdRSf5eibJ7jgZXQ6xCSiJ~z41jhg&HXsOj21WSg` zrSwM#7{7!YD7Lh)^EVnX(-R8eFM+gU!Gi`mMWsm7=vU|l`fiCW9*BmY;)D0_Oox5K zP~LZgLrcx%Y(W0f$n>ms`yYFUvB%Ih6GA_5_XSZW%xo-{*n^mYxk82?2)(WD5bIn6 z|2zt2!|!qdoK^7!xOH25*zNsiz=>>bnXVwOrk+witiEC8l^~b%-(?%Itw6<2$@ab! zbdkRQhm%I1T@|CXPlxa79(BEbCfIJt?)uJ$;DW#HUibIfg?eCZL?Mxbf_B<*OK zK_hv5KW7b$K6_u6FN7GdupRgyV;Qd2_;tCs{pUovYG~ud z$cJ{oX;|>2wnhIX?@J@gsF*y9M%k^^+t6O zfwDJk1%7Fbzoj|t9Y0Y#joJaWhV&`O`Z)lO?`oMPVg>cuH780n{)@z6%m2q&woZ&C zMy_MRF&^laB(ue?E_Eu?4h3DseAR&ob#u?m z%T*0umzkerk!c}P4xJY?pM<7`>O_F=rFAG=9GF^V3IaVagM7xj!f+FZycLLBWwpx? zcc>hP#TBTDZQle?WF%V zWaaMQaWMDRCQKFQREc0=cEV+dL7}z()z5j;mQy~l2xdTUt_hMUMwy0CX&7T9>l6!X zuT_T1l5FDTfxbOl6_p$WRs;4{5WU_4V)^`(c~P^|Rx-0r3xo+WO(;dQH)MP=*9Lq( zqJ84>dJ62nMuL;-YD{T#;#_~)-SZFshP5%EIQkDX>unB}LOwI>iv1S4Y?O@g(nrtovKFk6UCN1>xpZ*1`#%9$YR8603+>@n6H9(=tX*6J#r z-EH&pN8S3PH&db$A^SQJpO*w~KJAV>8~?S`zfZO8`tB!LshF2(2Ph;3QZUb7Y^q() zQsDqg0H@HyRN#35a*f8pr{6yAckl|*YoP9XMP>^5gxvVkl)BDUq(2okeY^bt(=THY zjpvmxBZk_( zQ0(z#@qY2b8e;~->ikZnuK}cFtfvb0SvVg(mbriRmm^)&!OQ$c7ZSBq1jV3)+>-_9 zXYqg+A7A~T|Kyv@A<{W_IZ+(o4zPsL%M&ALku^kQ+qRP)6sWf;GX^DNXOZ?TKJb$O z=%|MiaUc-j{@2ibliWV>M3_KFXH+M7ECAdt&CcQ;uvxFy1Nxj3U1+0pS+7ODJC2dY z@WgXshVN7vuk04SI`4!A%lHj(|0JUgM*(TJmPnYJQcj0Bq`hybE9h`kMMyG4-P8&R zBW$?K*n{53aF_V{_4CJXnL_?GbpcOq(-^k+{G2T7DRvyCIks&hd#mJRfk8A3azxY@ z{ZRH-JAL0Sv(1GT{sB9TiBF9PDZ8w@%PL@8%s7UIkz1|u4S#iOUj{vmq6 z5b`r?K&W|E^=Iksqgh|mZud-M4#J^1r2ZN86)xb58N>h}bV4_Of`jg?=jbHt_B3S! zca8adgN)<<=VlW)0-+xylM&Tg?|~9F%((jWDSmQd*F2xRW2eH!egWLVBu5#vlI1=Za zDB_f2zn(WV#o2elVi7QwSt$8?6#F}>(KaE{7U>vL$9HcF!^@=K?{DeK*jh;ZT@EfH<%1Dyad*t(rAN z4(}_a5y#(U--_oh4$VZ45EQrV8@*JJXOi?ZW-G`9@uA?%!g$3VqYdVRAhqh`y0KBk z@PR0wA1tgY*`KHuiQ8{CL-A4?0zepv#ZH}!>jH#Phbk%3i3^;y$db|+QI~sfBXUCC zGi~(*A@F@cD7`WoYww!0`rV;bFPfCKIjc#l7R5l&%^)8^eUQL1u8sDH?k^Y%JITav z-wrmntu;BRyt|#|o8eP-Tlls?8jDV$QXcG+0Jp#AZ$V@=IqQiB;McPPAh{QtZlc%0 z061STrsdawJQ6hAzSi@x?fU^qBKuRUR)tMtAHB3RjQ=Y@vf+wG@kAg4c9Al_L80<( zUm2Y$-*xlaT0NNVBc2h~rbgp`p;I{=buY{~`hzz}jBwUDH3XbB`=dvqW{wr~V{1cA zvQYo^?w$m&vtHg=&h38x8REwfiGbhkUCNt$Hbu44k-*VYKDY%uWF z)#xz{zqN;%w|y=bBG)!8@#p;MWGM9F{NF#5iT`~-w;fn_s>0il|5#RkRu&kBFbv;# z;laMr?+hifpZ(qbGeG~X6j^+}RSI9ChOjfBuk-`sJKR~uYEwheAiuLrAUTand5@E= zmIfA?G)l9sgO+)i49XoGQ5UdnPuat7>zn>ADk>u~)Co5>sYbQSKU7pb{-4nbY>aJx zbUVzhtJ)PkjuR!RnC+HN*5IMt?JWh>nZYe*G^E6gigfqwN#`l(W6mmRDJtwnc6+G? zoE~S;;oER4Ngcu+r0SY73>l-`!48y(RFFd0+X(@xqvJD+_=X@WI4)g|$?Hw!PTBRk zc8w5ACLC6PSCdQ&@v=udqgM58x8uu6LxQqF+?6zx<$2vUCx)v%KTPtM2U~V7Z5>af zA-`|=!%EC*q4Km_)fembN@dke%_=buyJt1_5U}KU18xy3^pGI}CL3l|CaVX#Gw)JA z9!Tk8wDCp&w6OhVHOZ=u1eY&whW!d?*$;-#h8rR;RcC&6_h?w~mjnqjG0rgnPpUti3GgeQU2t+ye_CCDOZG@^M5 zP2V85pAF#2hU#5@FbFBSYx+s208X_!ZMRH4WxgDxB0}+F(2d1gA!l{{GI6hO)sbJN zFh$;`cuDxutS}c=pVpBCdT;_XVC?46*^rO2&O_e)DpvkQa~%xWvV}3d)2&MgmN6g! zjf}#hxQziD^_*bY zI7n%)&V^DYm|xn2`i5yNlIbCW#N>XOMN1XfFP-eSt>{?8_M~jRK3=gKB7* z+v1_N!Cd6EG~BL)i^!Z@zbc=5vYL;Md&QZK&SJ-3gtR_bBYCP7PG#`2ZNaeTkts+$!wIuN`21OpXmr>tSUrZ z&VCX{73z25$*n`lLVcl_zPrc6o{lXB2j6f#+@w5Yj7b;mjm$CrWB+z_?|=QPRt)2m zD!HfnCdwEu>Hlk@lse}Nlt^d5{;6%Lj2a<;La6F}ZM?t_2`?V8b*HIM$(R?R1P&rk z%h{F{&uTRI;Z&UM$DiKD=sBD4Y=N^kWDzPe8;6iTo>mA!4J9e9(AD&Os(eVF)=Q9t zt8jM3uL6eivY)NZzQMMO)@!7@?%q;jDV1mESHe?^mns2CUm}P34@kuWL{6>|uDx&&CbGuGL!XNe<>#heF- zrO;TY$Hk`}pYgH7J`n@o74(BtW?SDd$Ap{3mF$bFGs*OvQ!tqV`{ZjefbkCa1rboz zmO~mgTcDQbxSyWrmzQw7y_JiZi1nNG6-+r#zKDcT>Xa&`0={oRlD^l6_4>ASSSmID zU^H%mNje;Yc~y-NY)yxLPNz4sUhYCxg%7soNH9@P4*5Pvur1rXHH(7+BF+ zS*;)qdFih}M^camhyJ^P$fEDOB(pgTP6aGZA%jZ*xGbA2ry!Ori^|a} zKME1%O*b0#-i|9uCy@xnA0;+fzU{X*O32p0G^_$0UcVSwwS$M;h@7=!Mp6L%a3!gT z+kS~&`MtT1ZK^PAM&X7KshcFz#4iLG?CJ2aacTz>G(ntHNF)1^xQ^6z5&_B5*YUdV zX@?PZGQ;*v3 zhqCy>rbzO4I5ixhY%Tq}mYF5d1N%UXJ@iTduH7!*!PW>z$80?tMyperlj2U@OI`51rHKngsmUilO@)C(%pv;AheI&R44O=&-a4PF~B{80m2c zHl*eeXLmE?)Y%cq@n7iYT21C9JGXz^y`SBUl1*C>Pcw>T6CVxGrhJn?+Nfg_XO8UGDu*jp&%zIE*UV-U`_JEH!YPY; zgQIh&T>N#v0PJX9CRW~yf+LBXVXCBo6Z7>b4Q{qfwh|w}6nIKx&z42|Zn^~Cj|)v{ znd76L5W`s=iJ7yVuF(K;?w}O>;Yu>PBX4d0xl20k>lpfiYgGddJY5^lHJY+n3mxY6?rk)#ZB9wvEcz#x8PCbwgR69OwY z{=1+Y-W>k5VO%-}4##-p0fF;&?aygfAmNM;f8C9^Lj7VyBtCVx&>9O=Vu6Mh629-8 z6OXBhME5bEYd4o+v2SO2P^7W@J_88(6?3F1_nTp_3K(t75KlYL^>Dz3=Jml}wr355 z6CfJ@kl_qKF=!OA(1DEUL7R%x%_{UgpN$8B)<;ZV3CJfIOVNNIp;`Zjs#YU&=~NdW zZ`*7X+J5I~6MDXq_ZfzuWojl=4ETli#d9hqv}0(Ukxz)D`D-q~ymRyB&xfXE9mKsK zBSaeEpjJqV5x2i|-|l*y+C z9jhvzA460DxQIFBhPaKju{9bjj0dekn7%^Q*J;fVfSYuFz>@pr;Go0oAa;3**MF96b1OmQghi7c3^JE&jFa_J!2eklF z@8C-Dl$T*(5Y?#~u(G!&=quKj##hLF z)+59D?n$+^qTaaQ)hk%<7;+zJylosb|D7e&yPI`_kqQF}dwSz&r1Z$TztkSC=!owO zk;00&#MJzrh(Uw&4P5(yS*B)Mf8=@;f9cb5Vfg#ls87won6iKf^4&cMFKE?0YSRJO zVK+)T>b!fvb7&e5wlNE?;Z}e@^+7zARNvRP2A6l(NS>Vmy@}=F*lJg-7^;NtOs6^wD@jHgU+9dhaW`pEE!ygpBhd;C28l|EyLGTTrtFAJ4(D?(Et_P`|{%L zTnp#s%P)};j!ZQ^BbEJ?%bT*L^yz4H3W|Y{UW<6=@AeoX6(fZAH+)2=dbzhpIq>aF zJ1T{30i^nvYmFQj$5%xT%sg^6ajncsL9bhplgdllJp~1%ndaA>>V@f08%lk z*s(P!1zqOf(zkad^Difi?ysTsSQ}w($c|^!Q#Pi}Ugj!|8o7y65GKWUrXK;hOjtF8 zeSW!$9@AFEeHGPpiR85^{hGFkvPkwS-{C@ji)EO-X%s#DRH*hOeW&&O1!pkzd9`k| z<)=%>b`SvNk{)XK)>H%%BoV}1-*Lc`b{1e2V;O~oIm3C2y&VIRLXB`zezvc8Q0<1*iYDxn=b*aq(Q&5uS=+YwbjHDz)xY{V?p&3Q*Q;>6K6S*I z#nc-IQLG6T1i**#3})#|o^tB^X?WV9RfAfi#!k+llFTCDfnZwyx2}*psvz?FMomSCJ zP0Q<21tC+-Sh3>C-?V_FsOlzo=$ZTE34QM4PO$z%h}u^NZt^vSwo;TsfW0~gKp+5F*<5MwH@ZeHL;u*{2(KfwA9qd+}an>t|uvC3r)y{A>+^sMyjuN&}B_m8dKR zahci;E|IGSFS3EpBY4WlmMWdQIPLSPFNIVqq9%9~>R1mDpmZOA30*aA!{f`GFpvN8 zppn1uR8dQx`^e4Ox3pRFdroU?dB^E>LBtj8CZDra9F2Ci=K6D!?&lx%}Sv@wLw3 zonlL64!iR3Y9fW5lgqMFs-AWWDhXKIbctkSG)bC~Z01-pLnQ}1+UZoUyDXIgv!Rwr zF*Meq6dC@>ZCNv4y)IrPtR4fW)z3AUqpn*e_)1*Q)C>F4JHS_%w7wOJI6iK zw1YAv0l<jb3|>%;`o#%Mk&v{ecocq*0x#4sai9Q z(>J_Rzk29Z#2_>Q-cP}%vh5q)pJ@uFXXDd#R>F1Qm9pgAgp>tZ|O&zPn!|#nX8p^-_uR zMxQeXS0?<$+kDJ<&*F%2oE{MkH`v&etC_MHYvke#A!~T4xI4lSwIeDDW;FWA0OiVYg z9M29~uU&6>ZPa42f1{8(EV&+PRH=>)ba_n%Jy|AMZKAjU+6B)e0^Aot-N|x*SH&=dd)Z&%MPX}sIP>zf`jSqx@p$lz>PENkOF9{&f`A?Bx>4<(eVX<50s>zV zyNejI(q1-GDI;R@V!jY|#8dd_6K8rI?WGfFqyTN{So$p|H+d7^1sj`SA`SNn=w2-= z%MP9<{L|;x(}mXenge;`7(SYMSBgk9{!O0@b=9-5Tzj8Bj6qkb@omT>y89UKIhtNf z)N(c&Qrjm)H?jV?ZCO1NbAsprcN;JD3UZ}>2YpdMzyf5JUKUkydLG2_+33ZiaSR@X zp|L5sd)%f1(D4I6NuHPI@XDDb_Xf51ZUop|<~jzkj-L*N*!LN`u=FFDIFwbd0KzD) zbL0F!^9bu(sK10ame-Ozd46UdWqmZV;;CX%ExNrp^fjCAIPkX&7hf0{2;c|waNqe2{p`3-q&6qtj6Mec4s})Fxh0>}oErK7$zt>L@ODfT z4*S;_n~}IjhaHMAfL~r46dq!g0)9^juGe29FGSfRiv44yV{ZtsC^Tv`m7G7OJarim z%Jq02Ke84H9E5i#_b~{ zx&L}EgmF{FCLJKR@h=Rfy2Fd50ZHjcB@WGo`mof{OwcaTlonWN`HDh`g%p@exdZ&uZJFU?k&DY=iLE37KIE25x3p-I*5n-pdIG0QY2<3>spoU@b&Eu_tm=*dNSf{;sl&Ma8FQ${Gk}=s-;vydtN`lk-WH}`DBjJAbyuN(T;AP@0^C>27KE0}nMBS- zN{KQYeU{vhvn@Q9vb5X`GMyAZVHa4__@7Ej4NHocl7_GV;CRVz!}F}ea{B4Z1UYPv z(&LZaV!;N9uNWI?bxvbn2Af87nZw{^&jRX6mG=?U@}s<^_3h}fjEl1>9chC`61sF7 zN~_Vn!jZL$(yRhkl6-&*;{Bgx$%l*w(BnRisz`&=hT8QSXB#w78J@(W1{w)W?n@Sk zn}@`DOIY-;XFL34xoIO?4xec$P%f1HAZdpnBD$V;^J8u5Q2$X8ms=hEEm=)5nZ8CG zAT^zYRnq5IM7SeAd!vlrqlo~_{5L^b;!XKCZ@~0vH%BwR_sfI=x5#U}XofRJB@kXv z*Hf6Ykz?e7?Uu+1CKA~y{@9P!-!wvCwFce2Eru%#NZTnDMxI=z7ERg~aft(((scIH z*Ka@~T}fKDLRU?=@p5Gnsa4aD$%LnYhlj6+JXxas% zx0RIV4-CaVou?iw>}QAD{Uaa>r@U9mbxPyuoujUww6h)eq3Eh3yrSRcs(4m{xfE)X|y8KDQUoA<`Q2yWo)4HLymg)Ee*K%BYA{*oy-8w#h&?R=*X0I@d&QSLfvl~3kZa% zaRJzzRUuAEXPmZ^Wq{U_PebMIkbnJLVA#LWg`93eVYDz=qJR!bq*xXv%QB{`G4 zRooBR!1wLqREc!XeVlEK&k)Mt)*7S#L;U*!P=~h+5;(~J&=*rSXLr_yu$Mo&cby=W_fr&DH@D&!`5uv4^x)(FUBfmq1xDRx;KlJ~ zDyFt#L7FJ0c~|nGS`jHOLOAPi!msYu@O&;eXAE7NPNmvbY#B78*<{KDuFLQ9hurc* zOx7n9>u>_UwONK~(db zFaI4wMUn-DP%J8#k`Q8RArzz9K7(fy{h*S;53BVCri zj{!?>TK4=5NydQwfbG;3zc*T5+udI@U90Fm61Qn|_-7B#+)c5?+)t#za&mcgVtm=* zzSuj>PQ<>b936J%9X{1{Us#6^rZYKWImb2ar2WiuSDuJ09*Q3n-T_U}HQN|xLa*{N zSm9WcyyRV;gQP$~sX^{w>j(A4ozZ5MD(bU(buk>+bh=qR z;xE^|t)a~ZN*vaHmPOkI3vL2BUSRnEbR=HS)@jPLC$ES9D>2Z1!e+zjz zcF8nMr@w4IeX(XEnH!^RmQ4r(Tf=<(KHq;g=EV)2UMaDwE<+ys8I;Kf}7q^s1zw+MCZXZyC2*Aqtp2si%*lQU? zCs#>IuZymLv1GG_@cIgWiA@CQVoCWadj1kj$BOK$&D|8|=qeROzIo zDbr@jd0XiFK{n-Kl_$*$(8->7lo_=R3nRy*w*AegU4vWFF#MCa(V9{VHp%YMB!Ca%V?!Y=vG5&W6u9;o_Re80XodS*|CHQAXQeG=fDwFc`tE8 zJn?*5W1nq%POSwv?A}c2wE)^q^$Yh9REHue66yrjBlh6$n~UBXg`KwuM>($42@2J{ zTU)wmSD&>dSh(f}3{AdUNZ*R%s-Q!DUh6I2{^aiZ`Mu&He!By^GH!9I;B%_@8^+LK z4hZAyeDzXB)OO*Xa<>#Mov09{Y<}P!)0|f5LYthU!Ca`se-AKNY%aM;>E9=;&x|xH zn8F0bmH5b-e|);5Jd7;Mo~D!&!S|)Ei>dD}qNNDN{b!viV5IKuQ;SE$hprh8lGHQwk)s=qlAyT~l zb;RDbfwrN3tj$Wk*wLi}_PT_9%B9}d)!Eu8@jIB|306RGOH50yxN{-mcJyly0`Cg( zx%*g%cM_sP@47ND8KJUT;ScioHbPrdN5tprf3BsJCS?pgHMRZT^i0#`L9D-~i8RfN zv(#(e+e_a-&|`EnTo4Qv0^1UBUqj_UlIVR!pvt?EyhtM(zp>*`DH-VScz-{R38g|5 z>>%Tc$BA2y|C`_!#IdGnu~JBkBuw6ci7Y~TA%Q>kTi{RzYrrD^6&hu2Vu<@a{(A)~ zJ-@{azppbX4K<)efMn{3z>`PHZiwMMz_&BZ*PCQcw%B68dbpCjRZM)&r>B3t;dtd? zbwvVdKV*!shV)fLl%@fS#ydtFF9Qr9GA^7mhJbP&O@DCu!dBSPs!n@wq)%mQjXDz~ z)e)(rs}j|vUAFfDfPRi*K8WBL>X$#a711^ENwm7nU1S1pzf69vN12Wpegm8@bDt;) za$n2{kYv9sPJBjjFP>q{IH**+yG+MqpwLgDMr_m%CN|YXD5+HCejK zBdvssatn5oq~Pfm<4SM4Wpr3z>d>9XIwuk)yy`{`7LF{#OrE5H&R7t~+ftrmEV(;n zUCpg^9znvkQ4LbdXb*4yPgyveI_$5;9)bV<(n)vQ@kUM?(Av}5X|j?%g42=?T6X*_ z17sU=Ozpmd5f~ROzXuQaEyNp`rcuJVR;JBV>vPYMHt6yt5CiDY&G>SxR3)6oe~%cU zOHGwnC&GWX?6j#J@WLX5DCd|4$!St`s-lG=~XgIj>1B-iVf2`n&_%<47xb zz(8$HY zhDTk6Xu1&qB6(muUD?>bCvK*#D$)Cxg4IXgYpFFrS@?dRm@TB4?hze$u9p`)#07r= zIw*??JztLPPgCa8tDQ!6u`eGkA7Nt&9}cy|hcnqxmr9$2$2=8iZ7@)2)0>4kbhH zoOEJuDo>b=bGCYNPdGTBQ&|iQt||M>eqgPvgJaQoJ_2`R=~MwC_gT zk)WGPDYI#&7qIXsyn(b4bs6DSuu)f)-{t>907XTIlDu>Dhygm>R*auTh}L-X3kSWH z*sp)iFK1&#QGhdfI;OnF`Bip0-y>MGahnAt6w=%J*f64ZO_3^p(_9qP3Ig%zBLt!3 z&*I}ju45dKW9Nlf>z_lB5$+H8WcvR)PcVU;wg-?<-KT0B#IoP9BMGXZo}Fg}?W}+D z_D9w|H@-nTbq;9x%Dc0Y*>)wVsMNY4J#w|FvBN3qxN%mw;0e$#n*ZTkaasg;Rv{@P z9A(6Il$h(zd@F(c@842FNQ1i7&Jx)w9kWL9XhG*5kOSUigOlBFqqA0rPS>xlUhr%+ zLJ3w;c53%%=!N)?avX(}qvbgO%SX0d+HOeA(_*zEgT(Zl)zMOv5W5KIc=h7Cu}GD3 zAU?Zd#&_mWeK{OB@L<=-ox^j(fyMYG%!4G5hqGgV4OY8JhAGI)x_BQ^nGaN#Q{|;+ za_fmg`j3bH|2{k{-89t`{M&C)h||vxGt;-q{ERlo0ml&U`M`n=8xcK}`7s2?*RB(P zfh$RD;bOuiH?f%Wg3vd4M$wZ>4bpUmLifThC>eL+y^};fuAxMJFEXtiuEV!#vPxHu zA8ge%*fqzEtUv5Kb`nIF!hOiI1ni|M)s$ptmx7Xa(PA#EWOR-Ay14!mX7v4{ z%m>(O5m`O@Q`Jr?-X?CdRCwU_K@Y3neeOHZq%=#zvCG097-Fatd5T{W>Ai?)pu%E0U7&HrOIyLTh4%Uf+m$q{FJ8d{pcb&X=$N|?Bx8DE}TWp`mv8a z@RRH<{2sM%6VM_AfJkkGega`XGBQJnS9sVrhddAU1c-ZPV#QEd5*q0*MWdHIai}fbziQ!EX;|vTe}+v0{4ymt&*v%Li^ywFpVP7`ILl zUUipL{5)(RFrmGb{wfIcblR1lS54uz(RYQG829mU0!XMTsG~f+C$Rhd&60Ze_eXxX zT2I^PUxMicy~)(5j0+_?^d+$A+&ER(vot`@qsa5Zt?;z&*Zr04t80l3$4WcJ7xBRi z01A(63y736@$sZDf-sHSMe-eE?2mmy=p_?h0iQH(KDiqYGAPMvD-!rwU2*s?=sTl{ znJ9IQx8wc00>;7Rcp}3ZXS~F3QCJ~dZvOR~TVVV>GTxJ{MHD8bRSW6P5Iy{674l>B zq-)%rIYL;V5Fsx|l{6ZISIlWA%p1LcVV2JmI%ANToHoxi8+Vhp?>ciQQ?VfM)Hf_D zn~bL8=9RcrR2M1W?d*5?9tf0Oti)2r92~W!t6_yyw3)%BNj?ovRAh`7w|YNR!NN40 zkj~Wkfivpfu^=B>iTVT1`6?fTuVelcVPDIpk}k97qaHXddyWU-I?+V~a z(@WC^EL&GJW`6b-5bz3hoOs&~@Fu^*m+%>S3q{s}p1QvCG~VZ}BCSGMbn(!L?@Y4f zhcX2-0IY8(pib`3R;(wWS)ruX8@@7B{P}Df5$lJaJQzpo3E}Fos%HTuqbqMI3I|J5 z_lGHH3Bh6+la(5vLJ*@C!%ZM-v5^ZxEG^fGVFH1XS~SEW4xWoxKek!$h>3^Dtu9z( z9u=cA00I*3y&FfNqOwFtw1|uBKN^Dfw@QQSu?5i03D5~(^?TKvAwZ2Hmp}so?|yU^ z=9F{j4A}P~?~ptVM$Z&iBaZJTMve#@$~abj&|inC=~c&xm91SUn71_+33AHOMu~BJ z4;5UhX_OS`6i7xlV#iGX89@(vz|x;a1#0f3Q?D)G1!QLA@OiV;qvYrwX zFf-Us%4|G52e{bxl(XwL?+93>A_3qE@hZOITckRx=G}=!s=1=T%T%&7auKB9t&t*Y zOtdozZ7m*B+bGCn{-LT+MDllAs?X0=6$Q%f-W_yVmzq%2=a=K>xEH%M~d1mbFEwY&?t- zFr8gs({ui7A(?ftlBH3bWF>i;GmAIt48So~qsX>a#=fRoidzb5E#Viy)_}}gl^0Ma zQVdmM#`}!+A%UV1jSv5Y9fwHrOzApbPq;%_rlVddua4Bm$>p&sw0P7jWR5i>WwNv^ zG!px?n$Fu;A)Q532UMpBmSmD+iV3{S*f%fky|S9EOyRII;}ywS1(*SzV(UiLi?s3u{{fEwy8hQfZvL!v!-WpyQ#os zR_w;}F3xjZ@PFuXk4Pltd`12k-y5iN`FRJy6I(wU=K~-%olZgOSw}%fCN8ZHZ=o<` z3CNdLvI%Di&)@9Jw153=pT}lMWfJCsC=Zb`X72}1^Egq?R_PLqu4pn^ z3%^SN*`+e{l%A358eiw|N}!>nYUCCLD1$47vMqc61p%{B3^d3{`B^kf2SYzNZo2%K zMwTW7KIDbfO;;N*zFyP9c>?c9qian_BuAv-3JQ^lH#Ugo>Fod!779*tTf>O2-&chK zZYTKlCGig~pGX_uVJQM6TwO~*<#4|dklV&}oy-bW9lh_+VOABzly8S?Jh#>u0~L&} zhv4#`+0c_Nqg)-=sAzF6QXz~697MqQ_ho~f+`ya@mLQW8AI|rlNM{LXa}W3{;~RYx zRnwb2bNQ~2U3E9w;uIU;dVdx3&*mBIQAK$QN7)sJ$}jKyMI>1VeNs+c_#+7HnY$!644({SXm-$|Zz5I_7A?H^Jz&-P5&C zjzK9IK#{u3m~=>ILmM!TJ8Ij4eHEIuwGw0YsL!^DprJtw(7i}rKuZ>4zn2S~*BQ0R z@(07-4?#rr_h)goe1c2bW#4Px_%$vRylQorcFuB9!TJpQ3x~>v`5XOBRjUmpF^J}b00BXoQg}nj zl)!?Yl~lV@F$<})=03HQ#?nau4GXkBu;zc!Fd0zdF0d+!B?$j&xO%Jt8lisJ=Zgb3pnyRtjPdt)4c^ zf+vn@bwfTUSf?iFx4;os>Laf0_0W;;b+1%BvCoc%#s#c8@2zYKH+V83WiW$8+6&iQ zVQYY>KOhQVD&QwO_tVP&GX%i5c!LkVG3+rej>*B=Dg_j?<$A?^h@PN-1mNP`A<8G6 zL@$@6etRf%@hXL%sLgZqST(3iJ_e7rXlvTg}v69}OWc zwaZBHFe>^Im)?f6wh4l|aJ?Ls_BI!vAp9Xk;pB~SyXmV_iJ4#OYb8BN@jI^dU}O!id0D&x-C-Vhy1|n z;GxQcR0E+!7Z>GMzNB(%aRz6-H<33{xJE5B`zdP%g#pe;sL zaXzo6q>ka^zXqW@r==}mVgjcNg!WRee5#0{^G|p|d{fnHUT}OF2Wp{rx-!uYHVS0C zvC#?5yUu`T@@@)Q$M~T;k%6m4$gGR^p%hyK9i{aYK!PawDaeG|ESHeZcfa>!y}JW_ zA13GrMBNH1XYp6AOg|65c|5F)UwLKtsdkeD(~5zs@oi1h|T=ia+)_q9<(iX&535Vww<*SQ)CXvdMuuOf$k> zBN4-LwQOZz8W1Zl@NHw5Mbdp+8U3+7rkC=beLg7=?!zNp@dzM0)Bo=?F62>(R(%U) z3$-8pdVWw(VoI4ViH=#8p`R?7YA00vobG`9z%-I1`a3q#i30MEzxnY=eMxbjP`3?u z>z4}h4}{b61d@)s0nOcQT8Xwq+Iv=nsOG0^5Ut#y{n3|;{%2Y9VR-m^5)peYZ;#+M zJ{y?3^P__U<-p@+os;RijnmhufjH%va@nO#*nUbTIO;f{Pq-i*N~~H2`Q!9XMtHjFzx0y*X0w*vtT0@ zsp$7Cw7eD=3^9NPJsUr^Lr+1_06sU481Un`@nRDg)$e?M+&m}V`zuZ0{p?heYA?eq z=q(x9Pn^z3Vx;`}71hllQ?J;U2Bm{YuapOBJcY(?cKQzOmRZ)d;$CdWHFB`VgZ@Ro zH?WeUidJtJOzHbGYPC<;9w@}&fbqL@?LCnLK^vi5YWJ2ZR&Wq4?AoGLG? z9J5J42}Tz5kaj`YonK0lc3!iUwDMaGTG@H{NLEa@}q)@*IkpB)f0 zxLrov{Xr}aup(*gPRuse_ONDQ5x(_?&stcG!-4~GD<>V_8 zP9{8+7CcNU58=BYe-TfuXF+nH6-cIduB3CsME0a-Yn;YzspuLmh`q(28nW**LL2m@ zs@(E%cTzLBCk5$tMg4#Ft6BXa`>tIw@wMyeR6u)f+J+v(Dmy82AP+kN8)G+ovb^J#WW zfVY-+utYqhU-5Hx^mNrotB^9jIEK6cA9CvYAR4M|T4(30s*O8WBR(fWt*-L-qw3Dm zddGQTetM7U;OobzPh_Mf8{b1#kh>lBeV}N_Z`j6cM<7}M1n#cYoOXI{|*G zfxjhAcl#Fvqe_yrwNVbawf#p4$h!f(wJ3fXgbqe(E4bGdjMF-v5w{_)nyJyJ2AO6H z3(*eoTS4qw-V$wVlFs+9jRMnAb&#GLAO(O z5bV1;=MddSk5o$4AR`TfPJqGZfX()31Ca?sTfU+TYp2>oI^SPkbfSgbP2OEQ<*3DYuVG7$dnO3HXB^d^ zVZ1lEQqH#Dl2xE-w3l*Gwm&*{`Cfvm)KF-}G{JPZ-}f@hB$_k(JSsH_)R@`Lx#iH< z{uKLgKSvW@0KbOD%g09>JKrBInLFgXV#hvNZy)q318gW#7|ijS&*N2SR=F^)@q%w)Ti?c-(il<69)^Rs<^2`RW4WeHZTBOozW&Xn1~r`ruYE-V za-S6UlqA?iIB?MVgo#;7-aibrfpTxNRrV@l-jSfT?4r4gA3#H1?F{n$lih(J=j{Z` zh5)f>Kkya8_yJCVufRhVs8b~Nn)AHl;&{ka$K(@&VSH@8Wb10q-YkQR*t)roxJ@rj zHKr~#xv3^$@D?WZN2Z%uYI3ae#R^TDFVFppHOPz>`*~6fZA2mG_k#*rJtt>VBJq2w zWEcRjqdhozw2fNUDhG+&3+G2?cgweym zkqQ#xIW*~C5h|!Kd=Vdpe>nID-*ncD?Y@d#xpIvELN?)xS4jXGx^OX;y)3h9;}Eq7 z1gm-HEe;^h)W3aCk}+?GmTqDD)YcX7I3yV1NI0FC!h7AM-YjZ9Nt5fpvxGBf;zfwB z!p?k>ggVNS)g#KCkswO+Kw^ zI|$I1h1j!Pd$bproHmy&RwB5-7yAcSD%Wp7PEasuf;z`}&xBr4qz!2~lu?cn%W`R; zVS{Du;E@i3ORt9*F#=x-=3PalH=oekFIub`;z1U89gHPsVT1W`*;yDB|-+!cDXGQI815Qpjd%h19w|Qd6>bi?| zR{NvD_1yn0{08f;bpT^T-NsD2t|uD~;gyW+Gm0e&y?x{ZEy|2c3vN8-Hcc8TsS|K4 z>7^kU*^x@v-kyo`UT3Pba!(Aqf#iee8V)g`Y|EwtxVRm=QP)CD&fUGR>0aVtL4u*K z40Z0MtGN87Cd?Tr6apSe$auQatRz;lDL#PgZNIWO_*Cg(o-y7X8d{NX-fk&uR6FyP z-QS38bQbNddu3S$opA1_p^orhk{tRqN`6nalTnY>Bu+4xzWowJ8Wqs;9|-3iku5=w zXUap&PV*2d;|x(~^v5kpP5~3jCG6=j7H8eae-NytE)6-(-QKJgm6&s1~ zXrL%(UdP4iH?`9(D5)i5O8Lv-%86IchdBP*Xw(Nz+Nyb;P4|dqGs#*j_Z^PwXpeaZH=8Jr-?OQ{%}GWN8b_)2#eoHZL6f{XYE3g zrf|JfCMrTh^ODJ2jzjen-#nd-6B#=5iE2D(Z~1%uAxNZE8L^C~43!a_mMY!WdZPk3 zHMAy!V(e+dR|8(jNa_!u0v_GCNn51b@P6-RWy7%aB&Xz67e=tx2V9q>dq9kPPl4>B0H!I4Cd(R^5sG3B3QQHpL-;jH!? zxw^se^xZrm@`Ee480|Bh6oMqJv1)=ggfq7Bl3o&zyXIM@uA$8EGRi?9Rqr?$XgbG}3Wz`lzcjJ-c9Ueje zb7C;`%0(`2xvf0LD@Q%;H zU9J=ZDet+o6|z5u`^u!A!hxjr)$T349*+){&qV3~BU^m8H~fO2aZE9%*NG>m3h9G~ zfr3l_$s*IxgKzv%dEs=g@?N-z@#r*sTHxrOHqpvTGZg<{oBmgWWj#eTimak@$!w>v zIS&W$xNUCj_tg(CWovfpSfd=FGhvC73J2ESIf8$e$n7IJ1|b!;L~Rr)Cyqy*|a5e7@+Tf6sWKYqo#hrzyu7#+ivp=2Y4- z1}z6?SxAqEtn?@|3K))XvAwmVslSpC5-zAxV|rVE^xm>-S|K+_)3Z&YMFv9ps&-Z? zsqI1X8|1+RvovFEz~t$yJ#$y(e&02C+goeA0ZY?`8C=SzY)9|~eO2Xj+H6J{`XUT; zL&6ZN4NMht0V>eavld>#lT~s8>fb~xVY(3&!vr{Cf{Y}ox+00kWJ=#!oo9mUvci}X z$7jJ4h1;W-x@=?xiS|Rubv>juQ|aq-v%g|`O=pV+2bkm4XkSTwu4dI}?Q^rSk+d%~ zAZZA8L?TV9bgqLN`QfN<7ic@dipI)4$G;3w6jjrYap>Xb*-jfv-X`n~>Yjov>*6@H zDnOBMRCp_M0}gzx7#3MQFB1rrxXW+L)r>3T<#Ju7Y36>(;vZ{mmk>959}NVHw?(7@ z&i1Z2$(s$&>!=YN0eot%EUjBku(ODzdPcmP*qtw)RmVs&_sDC!(hQbqk(`Zo)1%S2 zv9v%^PEjzT`dZ2*26MssAY-A%FTT(qn3`vE}ch9>D&69MUy3+Vp<) zr%Kh^qa?CjZ^hwb}~(=Q^0nWwZZgM48-J8r*%_ zY%i{!Db_AV?42j4{NszB8F{V~5XepdTpvKfzC%Q-CF!LPBGM0J>7M9#73gk6_ zr#a<0R2w9Qp`w)i+xE5JTxBgEto{(rC!itH!Ryd!N?FHzSqp z6O1}vLUS)i1^`w+a>RX;aJS0)P^U=Y?aHO!D-N?TPleA+hPh9a;>U54LYs!1T*{h4 zR)~mA0M4+kp@}g=FZ#(*UMQ0b{&Zg+R-L0uSKj*suwq+{l-}Z$hNPwD<TP#LJ)_ zA)y$FDl1hB+G`|ny84b_3VuO9;CM1q47gLWPsYs>LdG;@Zd&*r+mr4U%h^#P9l8wb1X+n>1UNM9W%jWy^4Fj(;&39rj6n)=>b z&4)J+LT4?pm8qM1_zKVj<~{|v&~67`*w0ytL3(%texae;LXq)eTY;{PI2a~? z4C9h}Uw0txaI+IKmoqTqt(N~U;JU5dH5IRGMrcn7pV?Ds%v)^j#dw0sh{??zaU)?@ zJ0^}K?U<~}X%_wi4O5u_y>j4wqdKgGmZ~dkRp*RPn+Wh{seM^;dfoYWH5X8-!#p`t zhB*3WYS*7(D2TQCN;Boe+UcN!6@AhL)HMFS476#Z-$^S-AMM^E0-zNg^m}NoW8%xp zt7dT2KWvcQOw_ieGDZ6#>OQq%APO-@5AGKq#``$fZqIZ8w%`_!X1=d?cD=QMfQWGwonKU%NgDU?#yId=y0M zRO2>lPnr5S9{P4oGWpe$fNYToy10~Sm&0dGczegGrkmqs?%?B$)K##j#eFLU(8L1F zemc+NoZGpv&LIG1uGro7Gc298xV$t}%_5$^YymwcP(AJHb74Wly)Aocz8MI38(_3OthbJwtAtjnzOw&{g(bSkdgsh{7@7iHW+B>%5iEq-zMtFmGU2 zb){%(^h7k3=S4V+z{Lc;?|(E}gYbS!8? zjY9V+KR!o1beO5O!*~AiJl@PLUPy}rkXIO3fBC#gKQpwJ{0}W*^(-9yBa8g?^N40gk5fcKzFSeU&^hQ_YvvE-L?$htgLkM~D_pdA&<~$gYAdPBZDJMO7eF{*fWtyZhwL}l z8@r&7G!``ga$91Xc*XXsg|7ZnrDwA2Ootl{mhQPnS}+Z{48~5q=jB$)c#zjaqEF3y z8=ZAMRwl!il%kJ*&91vB>2IT;V?oS5AZ&Lg4dg6rqPOQLiOkM#I2=<|#6u?DaNgBj zVLrT5>@=h|pa5C+*@Hq~MO`F$m$oe+R)j-nO0dvx)6dGOwXU|F3A6o%d4B)r*^Qlr zJevxwW}{<)Yi6A|vKV8b{are#Sh>xfehM8?%>a*O57x6`5rdoREGgku3GcGMF(Ohj zqVnHW8%w>p$b>jiyCx>ltwZ)CQDScw1~Nv~IHnuZ@oFb7aj%E_v>gtwZiFD5NU8f&&FV7y^FsCy9O2Hw*#J7*h*elzHZM1T5IoCr0Xvkt%jy$HB0o<=ew&I2 zrSISiEL2}F;v74HwKqtX(7bn7t}oF{YkCweUr*+ftU5aMFpLBjvT2z6r@uHxOCD16 zcPeN}VyV2lT5Bn)RhZNLzrtQM)vJPWynLst*mD+q=@-zW08=Xjg~Un$6EeadFOazB z$e^sL>QNk_&r-_fP;6g&uhpBiDT9-iZ3rqmwsL2*&YTko|ZAo3xr`~hHs0u z-k*(p^yJEG+r;E^h=aTNOd{xK1d4HsZw-xkH0xD>l4zXiPrzF7JC z(JNof)OZAerR1x7>excbr_J)Rmz^WoZppJZFMDKBjddpC|9^6NQv~^j7FBf_a9%!K zFSul%C3K?9-R)D#s|w)U(Z=+SYZ_S=ITXWSDsQ(@2;v?b ztW!cSSHxTu*qbJ($u;mW{EeZtrxrJ;;+TRT`Sts=h60W9wHn@0zd5#6tPG7WewSHO z|90i4fzjK6(-_H>96~nvfb&K9yG=d7suGJ>J-q{l(cbqC zE1#T0j-=%F;yy>{Cw~c3w-`-)(2Do%vpG_q!%UtewbT&Bl;}(w8>b+|d8y{)+wUIy zBbYQS8Gc&?$AMq^R%l!qVc$nln_kMJ=N&iQgro9+>;&$bisI#bA>M%${^xNh^Jb&Vmor6Q0HU1Z&%jN=_k!9nB?!}S*2|l;} zQ!BoiSh@ZLfqs~!chlxWh)?I_?0pzBbPuoUc7$)JVzZ|38!?{O2@8Vn^?NkJ?rm>i zek#hGV!7n1;ZA|MOlp{r0EkCnmaTX8R^!ehNNT~XCE6ntNZ?-GX25yT*?zRQZUp;meqto`|=g0o#k55;b%?v(V!Y5zvE7JK0%3_ z4R!HJ1zr;Za&Jx(r{;6=jbs?7lFf)tz39kaGKePSaJs(@pyvfY6=srKhcfkoSya*A}Gg9u%~NN|@U)&Xj1&rmId5jrJvq>G1n| zv%9<*L%ZK`N(^}CdY|T&-nsOhJaspP9?XI)JX=)og4%rM-wkY9EEp6f_`j4hWbpMA z+{GmpI|T|)+AGd){vL;Fl8Q?b^FvLgw_%dDj2LBWZnzAR6j=o`e*+?;i)YIo8#FBI z@YVHbz1oY($DQQ@H>E!Kd7cbc$>2gylUvizvENEgF(J8D=n-z%jK5q)VC;Qwv->T@ z_y}F^UpA}3#k0euZ9>Gsg5{bISBV;68L8~cz7cp0yy|L>`}c~NDDE802(hA8C4WfT zbwn0#W<=jeu^mvEz>Qla#zHPV)OtbDlrdS$^QddMsV15G79Y=a?OQRsQ$G z%kaStKjDE+og#>esq(zXd>r?j8(>RrWYp}`K$b}GR7wl`UA<@O<&s}WSE|8E0HAcm zmw6X_OsZ?Dr7b|?(s`#4nU{>Va^kO zUUmrC5SEq`9n+l>Ptj}VlWW7Rdg)Z>4_;+4v#`@iMl+{*q)Uc)35FG?M&{u9WS zATX1BWXMS0yvYd}2`iEKwo~RvOoe>_hy3MsyxBNB#_@nM;o3g@+<5)4tBvC6vKKWtD#e^@p2G(f!PX)$sJFpU| z8U+7-HPd`}Vj{OT5}oR1Ry9os=QTY;w#(xw;y zKYgZ9iD$*t^2&u)lEt`@U*oP+>JF@+)W+HWdY+_rbl{k-#-?sII`u6%BVp#y?GCRf36|t!KT)5pq#RfOG9FiHE#gJVh+3 zgch1@xr}5rFv}DY-5uAnnHkGSNk%NR#N;A>_9Bf?Cj(9Uct^EO? z!+6JfCe?7Ly13(l4+s43|L)R9QoMIf4H<6UGU?We3fHp2s#E~?XKcUpb;&gl${6pJaNUK=>vlTo z^>UGOu`}9wHq3uvjP}^%jdP&sUM;D~Sw(WYi74@~iAl_c2(V+lyvblYlXUJOmen*N z4(U8y={)pb=?+_&a0Ytfi@y|z2vi$Z#qWsALJgMmDD%1-p%wI^=U#-arH-&Z6DWUU z&`;ZhP<)uVoY5l;^{An0I^hLTLN`P-CX%p&!Lll^jiJBeN;d4CBi) zNv8qf+*rrDl*dsWOzC>;Zy_;vj-=eh5xkJePDVU(FG|cr)~C~(8{GQ2&KWXK^H=i$ z9d&o$**LT<(Pser=1~Npdu=J31}q709B{Rxp#U1ZtRv&BmBq}Ro9ixjH)QN%C*jmi z>v@f5h7owWM*EYNft~WM&Viu7Ee;uF>Grmkzysn`MUFnbR*roYTOUL%I2+A@`Y!zxnT8ZrCQO;?j0F{%qgS#LssCdSt$DpCi%f^}NK&^0eK`NqvWTrGPCp6|OeNC<#ssX*jUmrjLw@BSa9%E%CWUfCR95VfvEIxle z8VMir@rTpkCowhUkEB(=X+8|9EbPoBTb`kNnKFqvi?DzOiB3NO9?*J*~Qu2=}Q9}G%|3>;b#sZ8JfvTv+?gYJCCSsuH%#AKLC-#fLK+v*w zC%n($Mg*UEICE!^;aUX1ru(RZBMG*`%+rr2;R&{8zsXWDB)PbvRos#7c*}y>yqOzr z`|2g=DdRI^9M@@S5Lr*hNL9ZKv@Jz==AT@dgNxPE7E(0ir$Ul3C-xk=q=)}|R=EfB zv*(Cg=f{KU1&4vAjsxiV_>DRy{a-8NK|zjw zQafRLYECH;f)}$sMTjVS)gel9X^vVpkjPJ2cp*76CP2v+8$Yd!?0(zQu*{@q^H z{KZAd_7g+ck$b3O=~GlxA;*s^M`PKcUn2Mp6rS&bDUs81>*!@ThGG&oEs%tx7n(8g z%mG5FBt?*y$-=%WtLPkaiPUB5W8a#?&clk62piE&)5Z}Hj4$I>Q+dmRMuW#Mbxu}% z7Qg_Y03W$KWZvDmmT$zEkNgoI$3gj+?seKtlS7#we*!k6;<^=la8o~K2>zR7EcaI0 z-eWe3eA33nQ4zpe6+Ug2HAE(l#=+_!OKj0Vu@Y=XtHTTqeb}_JtnGE|bz07U(Epes zMa)A~SeeI&1Y4KqPMG@V7rBE9oNeWbTyqP9LB4IeY+5!XE`4+5N7vRmacyt{l(ymZ z?Kv;nURs(jgPopQ7wb`Ao1engp86}?%0)Kw@%G5LjRP*$R&NzQ3nee)|7~3ue;IHw zB){UMZ@Z2h-h9e6lWL-~hRr8Ni(%^P#*=3g%&ay~+j%Q|3;o!gXv82;W|WhYoSlqD z+biR@EB*g*gVGL=Bcy_o*`B!`EQ3~kC?xL;zfGhYM#%RAoRTiZ-i!U~nzW@Irx|zA zor0tbjWRYXoP!fgaX!mCLCbVrDpof7aBMl(;TL5U96fF`>JM~x^JMBgeLGx&|ChT? z*?US9D=g%bRVCPjZ(yAIe&|M7XE-XWCwX2JZ{9Kr0q&5>r#8#(V)Euc-%e<}Vr{>R z-G>3a2YwCdKD=E{P!yTv?b4Qrb8xkdV777eZKOUbhV)oc*=%%5Wys?bS+s8h^c_Fn zci%>tMS=02i;^Ckw0Wwi#8=q%%`I-RX3CNOTY`V0G9~F&EG2f47`4L2F*jB`n$;(@ zM7q`ql#xrJol?L$J3riz;0P1rKz14z+#gGqPgoqEyhujvY2@i)l-0$64#KU57K0;i zh2RFwTT$=~M4t}0KxR%R_-PWFAM7Mo+E+po0Jlr29ifl!j}Tbp_L@4?9v!a{Awx2( zBoQ#*X+&%;)8NVrO9UMU=zw0bWFkApstyAgtWjRdM3v<9)-xaeQ?V8^E?SJhi=Ilu zp~?7|i6I*!Gv^ZuOzFaW#&C$=$&_Nq_S;-kM3{|UfY_X&u}-0&IGAztHm;LBY+-c2 z6D?t*^(een_9U}e?3o^On8b8%dSWI~^m0gQ{`BTD-d37-(u!EE05}m!qKwmHfU0iPiaM1aeams&EAdh-$ zCO8Fm!5-zR*V!mWqveLT`2f=*IWLgW0^6kveXat!RDekDH%h(XhZTa*Y$WGm>a7r% zKBe<7TvA@d;@xQ}VcbV@ZZe1m1+i6x#>Qg-5MUGa!6lnSn{HTTWqFY7^`J^gN?^rk z40iF-<^ICvIHg)!x)ZT<|J5fSD)OpcZYYjwc%q1=ni1^K#VIVtKlo2xQjh>4{ed)Q_q z&=Anz1Maz;of~Web9lCu!HL*^EUG|QFN9_k;Nnkf?r6PH8%V|3_earl4!bb0@GunG z0tDX8MNj|^-9l(Oq6IOD=2k4y$Z%al(6=22;b(L1cutmvYRt(H^=i%@dH07%6Q7Xz z7dG0hpD+d*x|!Vj)QwS!v#(o>dt&yjc|Hc`_qn$0w<5Ef%4Xr#XOXyC11mvgL$bJsmyx@(|_7@TrGlH3hy=}nD z%Oqti`$S4LNARx*`2ej>$1RFm;;76up-6y2I0&Uaqzv?kOb!gYaRjiTJT1=ESkd0z zv5|-Xt6<5|*YvO~m5?Ffc=-V+=9{vM47_*j<}79r|KG`lzVWajBtE zm7eFxLlLx9wQ!P-rKXp7$^%36tD;QacerE%?1O6aiiE6E`fl>YE?-`A21v{(c|}Pb z(SHGn`ERhpH0`Tu-la92SM#PYpacZh^RE1&JG*uQVTOQaR3)PjK0d=Y*^gw zmW{9&ePg+jng9U-%IQiH5t4Fu=&%jF0{M#8WfYy$1^Ynjvf_LH5T|pj$j^wq?7nZ! zO#QN&tSSTu4JaJZDyAWap*JOPb(MoxJO)No}N4N)<3*eSYG$6Wvb%RUJX zhfK>^pzQ#&?5HQ(`Rt4WFdZk~Vp3&nwahj;o1R(Vt0Cr*+Ox)lXUWwDbk5!QFZOC_GS zr|qMg2!SG}OrZA<0@^9puLh3c*Lk3@X+!dvFE#0_{rmL00W2=hn3IlmLLu4ULgAp> z)hc=MQ;TFgSGWG_(Pi;$tFQ=Q(&>@JA@Y)CGR$|#gRt_?7gq(8JW?CJt3jJKIGPzr zT#|;)RHicHXyBLoDAxc3Q@AZK`jGdxhVaYI^qfwRiwT0=TBEhar6mv!nYW{j(60gO z7HD*|(vB1vR%{zWIy#Lygkq%BfxKNEX6~^T-Di2a&`Se7owZUQWwj%g0r%DI>~oac zq&tYvtu8nU4#&bd)G5Z-j^wOTydAoWmT$?Djk$-`Jf3lz?m+mVX@1$BC|bw~z3To= zH16fJ#|AexG?es;fEPZ#VobW@Oa`01OmIYOnEMuPi@kWog9pTrD0j=y&8FvhyE4(i z*+sUWeQ_uMNDfKSZe}J@LkBO=<$Q)2=PvI?hGSKB^VDCG9GT&k0r;H`WM}PFAtTv0 zWR!4AEml%`L=tSYUvG^f^m*}9;c|ZT0L95}c){)((yt{12$vIYTJh+dJP)AFnsDHU zQ(FWyNW=;9*Bep%-%#Ky4^C`?GYJty`XUI`wcs_Ms|pE(zE;%65M+CneL=SHKMf}4 z(TB+MM8b%@#B28WSyOphSdrdO7ZrlHB2W?R)LMTJ*q1QJ-w~cwoKSqV78mx@8YWwf z=q~vjCoKQ2XSe}Lj=h9JqBGJRGR)G?|B?8fSFsI>Pknz)&3`*eHO!U}A=$~jm+KeiKKiA>>+NLUQ7d)iqtSRH zrx~8v#mL+wZhtiKR3#Unc2Or9hdm5#?7?+NHyHxjHlgPAw_F zUilY>om;7@aZI2X`ZuxbjBGxwKEGdF2$N#F{Z-cbEf@rK8{@(2BRlRVonH zhpEx5P<-cL*Sv^$TL)Z5*rMOQ)~aYeWg1>{Y;9ri@3LxkmZ>>S{zF68CuB5W$5!q% zTl-$(t6ZKTP1XPsc|&rrJ&cW}S<%t7mpH^S$qA62Hbl%``WD-Pk09mrT@@Q$Z;ART zg{t8@jj#B{oL95{(332e6e)$a&j^Xt-(J#t@acs)zYldoW>dKZn)IBVw<^|ccL&8J zEIU^^9`q5mXg^`4ese_4**?qGWQ+PzM_(#PTlcn3JsrM-;WZ18ApIJec!R)8{Q#SU zr!^pVO=CH9`G$=jv{R54|H+}20Sf>2JIScYUd-%iE#xpLz&AIO)YQk|cbY_i*){V+ zk~wpWSjxbaTC#>I1`Fz25ZPk0hi(nmU5Z{OyI_5MYT0NX%XkPNg5F>16|^1*&@_{w z;>Y#J9CY^YZP*!+##)PP1LU2L)N#IDb;*~%5<1Def9PuWhQbogNKpdrvfN=K6;2g} zAw;>z%~|Pa!Y!dTGIs(0`V={k-C5-9+vN-KYHJzMB2r(yRV2Gh5xWlzeS@I&2Jjo+7EP6ibMCb5d?k-Bk+PYRsp(4qB3Jdnc7oCeFP$hx>yW9{azPG2C^1m_|8Ve2Mj< z9CsnCX2hAG#qo!!mSBBerl{87DM129p(H*nyDuVS~M zrV@iEzT+$0K>5cvI!-VFbU70A3liff*gcKessRK$KvFlP(t>MlCb^GYq3fM z)e?uGi#@z!upY6^JqRq(MAGCAc1|Yz zB|Pj6TWVeEO?k6T>vEu`foEdTZc`tcc&FAB&^BV)5d1O`a+s4RuN%kcj{95YT1d zi`TbX?W$xIjnjuATL4#Nde2X7aNLC0WzVyi-<4;Eye+>-x*k;~wg}84mBU7Ve|e}u zh4t9LpjB`f`yO?#Ov_%RwSJqLR+ME`WuTjtiq`crV3AR!Kt4bzdWS-0fWCjrF$!eW zQ+S9rCtPZ|2b`7m>k*Q)vbCM%ke}<$!IgDlo(?!Z9?qmn1ZrgNEsf2(nUbw7tUV9- zg^k##&V>r%$jAd%Nwp+G72rD-v$6)gq(&-nB(6pJQ^zQ-!*z_ z;B&Er>P~y*!ImRfAp?)hyce1MDvwz)q4$m(UfiWig5x&eH9F~ckJ04}qu)ldSgJ^; zyo^M=-t9gqp$QHF4-3aXbTfZ}?&-Lk41%0IOseK-;qLK;nQ(aVO#lr5r7r*bXL(pq zemp@Wl}o24f{zC~V8}{quxzoP9l(qYQtCrpTCaVDiGB$iC9|w$&mr@)SLcAmCxm|< z8C&AFNO8TUNl%sKw|q-=g%GT_yi+ntq#6Sh$wYXohvb*?-;^BJn|BLJrYvd4z} z=0y6JpDrxu&oUa_vJ+r9pl?ijoBYuGu!v480H6YL&k=N3;rmn7K8@Ku)L3)D6!_wI zd~X8`a$W`qong$wfDH)}?(?%p0cXJb(CSne(vs%sQ;cY)j?^{OTARBQi zkBx&Jy)LB$o@yBEC1fSMJJmqCd z5Lbk_$kCnAov7sa^sXVYNGS7pKFBc*&QUL>2c)`+n9As@5uKV3A zKL)4B*S@?fNf$_+c3F)Bis|xVUSfK28;&0n*MYd=pz@%GGpX~!RpJIWr`KOc)tB6s zkP6gEnotIq9(p{zv^9)joas?d=vd5?Z#o&z-erplI zyya0U00001L7I|yL&=oDf`7v1S+kWl_;=awA{jeFs+4B&Q{eFeL+FVl?}|y?Z;f=kt^R4^X`$PNj)W3 z>It*uB%6by(HAC5Ult8Zkp76C{fb**LZ9)8R>Yh$ly_|66*ZXK9|D$2lz}1wau1qs zH&9cUbPfjEtu9WYLsvuGF3Jhyi9n&KQXk4{^`=wGqKwcll&-U*l4}8`kZDb}8oMEY z0WmBJW)hV~z$zJQJS#ZO`wcx5&O+1&F6|X~;T;}OMN*Y_cFu^?Nl@8HZX}D`628E@ zH_D?iEdxQ>B9Xzom|f*Usx`Ez^vgBIR(aVt70R;}C8=DPcngd$rt~f6W_k{Xpc3BL z%~uw|gzvUW)X{fUx?9YkIOuzjv?|UPj@T;Cv-!2UX5=f!kZ_a86z#ie?z-BK4w~MaYdLUp_jck{aV{9K~IhoKQHu_md9AC3Q z5hEW0kHDXK)nCv(@cR_&Kz-L}Xux(F42nGPGYS(6Lvy$=EE@GP#@*g5$F6DH3-b=+ zmM1yG2+R+bN&l!b_xh-LQ@<8<+OD!OJln&%pqc71kK^T*henYtE=iha`NCH#HzCgW zk6x!I2ZJKUi*#~}lRXY9hoQYn`9n+#I1&c8qLD=bsoEy(VwL`wPHRd$0YupsWe(9t z;auFgq6LVU7P-F#U@qN%gU0#=6EAOo@ChX2i^|z=S}qs4oN>0@-aOCS2?2HcRcNAM zgEK1|XvFK*8fX%66TSU@=0B0OdIck=@C2NX+GficW&^aYfsnsh8X2d|Li{;(KA2(3 z9%$RX?@O;9Y_YKyPdN*SAU*^>6oI>+32Ntv{Q>D(2eqJ|#h+Q`MDSf14Eld@RBMr) z*73cNG^RZ%=ZBQNoL6wD1(H!&$#l3xR*>I-4$Lq9N5wvP+bCXW$#eY~1!#1?(q!B; zB`b*jmq4;3^yWj}2c%c%<|RajWx#=Lte3AR;G|+jP}}P$FwTcmwZND$A^UFCUrL&5 zgU-*-$4h)!BPl6(l8c(R`rH@3$iEu^%IM=6NVf-%FVdc^dUW&8QK$x74Xx1q*@0voh z>#Sc%94fyrbp5KItpLB+nWnj{7l5?0e3QyH+L0cFccEgiR7ZC4 zPh}lq4{C;2XloF5o%6Ype^?R`^|9Fp;ENsmJW68D^EcgRGJelhW>WYph}kU>#-&~p z(Ksg9Q>UyR`JIlj9Xkr&_v4ukkWCMco);0ZG8YwepoGzbA?76D-$=VeQhPzjIlNSn zy^&X*H~KLD7ni27l@tHsGvgWCZkA1sjbVK7dIJ9?I&@{7eT6#Eg_==iUnN+cJBLjWeCO! z-NM`4gF}VKbb~vKtIHB}=8AJ8n%1!w#x2~5FnRt}*Qsx^C|ol&V7{3;E6rx(XDF19 z;wts&L?xBqexo`8PCOfFNuOkTM?au)MAR}Y;JB6(7 zP}#Hx&1dvr$VY-0k%ftJD+%gxtgzNP%yBnPrpt?9NqOht7=haSnzQefH|1%jcv&po;!U zDg33WV@G<7LKTp<9CpHh1O}=CpRtV{qUbR?3=t^h!#Z`EqsFli8Vo`xDxFO)A50)_ zeTe(}#zV_O^(rJ^v8@y4a0!U5w4g~~0(?J@5B&!R5fl52Q zedxDsbC6_ZGd7A5-7{~`TQRVHb$cSuC#G)4EL!D0oqId*%#^r`Ts@v{K}CVPAKu;G?WYj@iov*=5J2J`k#tBDa}nU&Dlh>{3`T4fpWd4tYCn8x}v44L81*k=^olq{=qh~BVMRTJ{_$TXc3OTv&CQeqgyE<;&lVQB{P8AKN- zRx2{MibUo%_M?PY_~co#C|Z3r&n45HTu*kl-7W_A*VbbCOvXFK$K0c8S95(!f>@K~ zjVve(FpVZ4RGqfd9TL}LwVVEj-^@M2O}0BY`RHScCi!*W0x*qL-_85ZbK0L4qta5I zza*#bwRW}ndUhUX;j!j0 zmhj~q%9VThqUhA`_6BX6RS58I+_Mj_trUpdqn>aSqer1ve}hwo}%fQfeoD=AB zk+rfa7l|3P2SMv%CY6;nH&QY%(=OA+@I%V)qT+s~$7L8>i^Z3)jO)>_D{4JJ z2V6h=Nr#@zaC!~5B*?9Xq*%iT&=*~w%*iCZZtN41P1m1+?|Fan=g^0oEuBYaB+raq zxTDDq{(;mx;x$9#6*YaNSS&fWpzNb=7`I$G9hdQZJ&>HnOwPM0DzcZ+ujep~vuj6i zPg+}n@9g!e237lk-;17voBpgaogmF~&m?@3eepY)Wj_$R#DjHON%N zsfk8vt2k1$R30NZ{if9?B?elVnQoPIt`DCHWh6tgCjJ#nSLK2%)+G@lE4pF*A$#wU zXJQxaPQK8+S#-%Jy|cC19w!#F{;uqdQ+FCBfU>p8xXgQ!fD)T6Yb^#T{zg}g$O%s_*F0U<<8cPmLSXpUxtcxbJ=4y@Qz$BV8_b?5fAi* za>{5B?>=oX8rp<6PL>t~cQ;jv6e8f7Zo=f{<3f#E>9_~kYd7X`W$RqGk$G!4tGMKM zBF=_NlMtW6-djA&t~bHV8(!DNyK3JqaQcg;jFw|;-E7M(i?1RSlV`OE3cXcbok2vV z)n$l{s9@MDe~5z)VBoGs1GQ`AMHeiL{=-VqVy=)o&9Fh&!Kt2Ui|r2L5XQk`5KrG2 zsSnmhZA8X;lq}2gvHZ+!hWtw=q<`UqY`2px^!C}>tTjQ`a%OTx`}p#Tye++dh{`T0 z(XjF2bImw!QmSCfOxtQ_g%$m~RzkGTC70p*qvJ8cyTeBPS3QHJ^SdbCSc-6cvq2%i zYJTV;x_(&xANvrE<(kiTz^BQ?qS3QqH#_S?iE1iy4eZc!)#{BQOXPHeKXgRTc1s$p z6aJrp%_eQ?*)Rj&RKb;RF~ZXRy4`*WbnUA;N7(?{!AHIdV`+QTY;X*uzLCKP55^sJVrH7O-3B)sCNM$yG;4_neUPm zGVQ7xa!?fP24mi&*15OfUYTZ&m!{Q!7tFcaU@!|-WMo*Ev zD7G6bVsmR5?RzZ&2~iK;A_#u`&DR_n#$MFJc``{Af2(otJj+``15)4?J}JbD0(lC<|8$Pt zg#sETtrMqwzJ)V=DLmezblMM&3Q34%z?9w`x}O6sX;c_c8Mv1iF30Y?j|9{)wjk0k zcp0q{f;fi_@4-Lnjh6-v)+`cZDKsW0e{aTR7K+0B%2CLX%|`jO34XM3^gD>V0fhCl zhi75dJgfj-SmrfB%f6b*{#`>)D0Zz7ea5c3;XvInVQ#+~XX#QAgx$W6JB~sKlKR4# zh$)UMxx>XMpu}TslK~t!VZ65`cLO2Uh#5VMpk6;w{e|Qth@^M(_d${ss|qtv#xD)5 z95>DTWF-+vEevNRYecBo2;}_=Ce-~6OG$XE|u_lspqVKRR3^pdZa}WS4y5Vgn_s5 z%#S8tL+t#O#BF1Yu?l;{ms) zR?HP7diG2!5-%@naB#GI|2YX0KfO!cAARW5-JK^7!}TNIsKqf@Oz>TC8~9RGGYZ^T^TDE zoskGD{x1<+m@lJ9Nd}^f)YzPRr5zG@q}XyIjqByX+AS-uV7x}&DH=P3e+4j!Kt2u0 zzFsiSDdD}4T1LvgrDE{U9YPKotL~ViyqO>8g$B@4OXx675;Kf#Gn=f+j;uq9*YBt` z=wjWW>X&b>+w{D-+Yf1{AX`LJ%_11n3fFDYUuV#3X-37iTW51 z_pcOT+~jn*7K`DWJ6&Y=psXA#klT*zj6$>n$ZPsjs+-5d6S>9=*~a8xe=^Xt0q7gm zh+25!<6KRB6gYtop=`31v8}|y4mEo~(+|Qf;nl0-KwenF57B!&5QMB5=(Y3gYcLEB z{+_h{M~-e&kK~)Ec=54OI9Sp?7@Jwd2DEp~5N6)(%Hf;TYP9DmJkVQ+x(_H^VanD-p`w|NF`TE5E$*Ldexo#@7Jyg z$@nO5XiR_H&$oL-ucw^U{X%Gh$oz68Pxo!y;wl4KUK0byfG43lvv~y~5-# zLNaGmicbAqn1Wmt1tl4{9yyB}X6r)moI={5N9!;JZJohC_bL!CizKCfrF6J%R5*>) zM=eA9vG03JC&n!NIItS{I3mz94lE(p){M*nRC4;vEc_MA4sPM1hG@{QMNp#EuS~;| z*_k=s(nm)U!bm`lD};CO4NJde2FjWP-j<+(g@0PPg|gXSwM3|&EQ40htlDlBl9O&; z))|71rqaAMN?uIDM&6`xHtvVX8#t?J)yzsj%ZHU1ZtEu;q)%++DI&bvEt@BnikM z{aW$@t@%}h;j5oLz`pP;@4$G9v5NtMsoQC&N3xfI>UzM~3pw_5+DblnKKve>a|}EQN+p=v*iL+!Hf`<8niq`19>%E2An*IQYq^7AR%!e~>4gx$`86xgO=tBIbz z)VJA$1=XU5dSj?X7!$Pr`#&ku&#N9~E}g{-WFyF_l@Ib*`0m@TMy;?~l*78o?k~2I zBnUCJ7XC>ZKp<>m22%wdP9?F%>Wf`xT02D!rPo>%{jF&R%x*cDlrk%>ue3bPR;VT5 z;%R?O{C`@nb*^vp+~>MU-&~)80VfWi)s=NV=f)}8fdXvTdEO=tTGGVT5M$QqqhZEL z3dYv>)h3;DBD@0vGHe&&+I_M38nXL`t;Y$Mp-ZUQYo4|en#n??Iji&(CU75Hye;5R zkinGG2_VQq< zkR*URW+Hd=)a%!ngazYi$u!}cUVGBobY_1k$H#W|-%*(*<{K?-GtP@7kXyBj%iI@8 zgdB6aJPwy0zbn|8RDsYk5NCJa9WOIRoI)2Hx?|OZRqFKt2azrdk`SE1ajZI9g#Av|(aumk72v5!CpvJj?WOj>$$hd_cp z!$N9rmd^Yyz^nuDbq0%5r+bZy#)Crws9Bc>b5k2X=hS~U10F(>#I_&IcD2*wiIa>9est7zktb=bJp9xv(6k`AH~`F)%E> z5w!yajM*)p>HMA&ymKT2QQD^%%2qw%h2A365lfk_(_k*uV!b<^58Js4%hK_(( zB(?hWBQ&zw9iO)CEdL}4C|lPDa1oXh^147#e^}Wb5-vs@7xO4%ndumsTBTAdc!XrAzm(v$YBc;dr3 z@&?1zAGuW)8#SkP5*Wm+gia`cGic+!Bf1ACY$?Ee2@eI6!x|MUUD+p$iI`4&N1LG; z2X@~I9U|E4jHl*U^rpu5DAF9sBzO_YQy`P0xAOh-y`3Ty9WzXgIZpmRrY*QgxXjf9 zfLk8_Z&$$TV{b+PQ=@fooZn-l|BOGhLm1%$Mtn%o=gQ42^?8YL_M zR6wi07QWn1y=iKc^~*vpRRzu!9FDgVL^tMK)D;G=7pdnI^l&fS0!0J-pL<1W;=Q<^ zIU}v?>8c}5;g$=~Df{?r>gg_60EyN)%l&Y^u=t@z(!=+j;>rfOkMQS*#Ize0wVPEb zyz2p6FV)QX?t(S^6iI(h@&7?HU=0ceRgu4!IScfkE}htt{%SLEJPgZI8`cP^X3(RePyZ&rs#BE8a5zF?}ENV75mjvvRQ;O$u zT1XxOx|Z!dXnJDj%rGZ%V5J~ckmM^tys(W7E=JuzuT^V6BSWP@B{?d=Z{K?rvZSOt zGA!B273igdO(UVsX-gM2SYPb1Z5U)`w0)ILLFnPFOD_Y)ue_B@i=TxXC^p5#zQ)@^ z4ei%G^*%!(J2gjLu@#n?FQvgx91z+}X}$H1l&xVTO$p}aBayn%=`1fPbL?{zglG$1 zSa}7{OV{F-&9E?L`{j~CIh$k*W-H>bg_MHfja!eg`v830aUFB$lTKeWz0Q%}sk5eA{@$1v*1KD2)mpNh5_mQqeWOlb(r=k|e{y;fay)$acQ zp+Un5HD5JDj&vJF$iah!VbBXX_Zx!A;NAw8RwFBHG8nQeu!^`|el@P62wFfp8}K=-gdv(ZzTL5savTsd`!@5fXsPN_h8aWi81i&4R|d3iXun1s~cO zLx?J=9#!ylJ02yGK&HCApGaDB%r$7nSNd~^1t7xQV%JXpK-h4cC@x~Wr7ul=M^@nz z{+#dyAy^tc#q|2V%j&bzpZeYr6l3;A`#YdZGEGzvM`rsLF(ciS{MG5nfY*0MBoz;Q znm_?Zqo&Ezrjs{qF&BPOIa~z?sTm+eu;uFDJ4KFWU+*hdE>`djmqkq7n1*DD+6Fnb zL;l{=(43_M8Rw>B4V6TxI0EJnu*u2J;y=jESX+-?Uri; z0Flks?xk>zZK8D%tcB`!pcl|%#1ev5gnNhPHt*2393s;q>9_z+-0#-a;{~R__QyNG zp3v$qDv->@)rQVVW|41s7G=BBKq5}c8m{|_NSv5yU`jETZl1Fq+3cwo$3`IwWWAXa zmp*f>v_bvHdp=FmYK=pc(3fT+{mgAh-bM=2Mz?~8R!g8!uwt>r5K&lQ2fQ3VL3S$J zdLSs0g-^;h05C3H`N-v%cV#={*Mnx=0^A%(Eq0y|d&^0;idvrDZ3Ub_zzY9f%2@OI zs%6gFt^ujm)xa;h-weBAu_(P+B-*>Jc#UVp%TS)J*T2yw@R%lXp5186$N-90HR#$0 zaHez2fE+w0M6%wq&#sZLGxzJkh88?-ryN8)y5!oYmE{U~A|{F1zyw1zV_NrJ()od< zG!7VBo42v=(I9gy7=rpNHpX6jv=VWc4&E z#YP77Tw2S)z50Ri9x_W3C{n$U9K6Da!iM_z!kfHkGlOSCGM0D!V?4-?`0-K`wsmu0 z*1}EePe$0LCdBnW3OmnK-mlC8C@)LYYFszyB1qE?G{bS)!j93kG@kbT4)$0H_qb|o z2HV=Ky8lYI0#_06FHpFC&j!tv^VA2%}zn#c4 z)N)oG*BikY`k(A-MYOaU>|yE|vdLpYt?l1PNnpu!wDsrj{lYAGav`zuEC$C~~RhqsBjUC_aw?Q@(|(Vb(be-|*{zBuf9TqEpZFaqqi zah!7?&U#Z!qmelIsLd&Cyw{S{=v31YLX;J^mILvr<``WI9bPAl%h{3Y8Rv%=Oc5!j zZ+us&;nc>Ws9TfnCht?t-J-hOr?MNjY&`5(bH1;Wz+s@qWl}^Wk-fz`n@w4yz!AQi z?#LGPfvUs@UVBDu#)O&18LPLhZ@GMG?`;1~rS;ov6YN1*eU#m0 zPy!91IF%l-00001L7K97L&=oDf`5>o*h%VWV>*+B0Oldx$d}I>*{F&qyh4G#5kdQB zsZMt$$%=UnOzCiK!ZTRHI^;aFyM^+^7!%EBATB&WYvy+-3{eSIId{fxi@M5)IgR`# z`1{LB;(0iuA)TY_N;OK^SRlX%%6=12#ZjhXbwx@pq6?n_22zzp>=D62^{w&lp$EM843CRdOUb7T~P(ICUvKm-!Bdn z^>GJ>3)}LOd)yxCNyI6&7B{V?+UX_ZtC5LrIJ&px)S@uw>-SqJ1OmLdXs2~pa}fg< z)_dM6(vrq{qcD^uqv_g6B_0H$5Nbd@#tJ zw`22RZIsXc*FZ^}O=5|*Z3aU04N}G@B#bmj1+=h4d7{|qxC^U@WF9j{!6>Uf>xdao zHh3m-$3B8EdP4etuJa+Mz_qg7U^%Ht8Xba3P>@MUrxR`)SK2m7#!n_`9DTGK1~f9| z-o_9dB1GzTWpCG^$+8xc2ZKKO(q&PiZGp!M@ZbjlE}~ z-l$*5ee1owh%jZH^>c@ezDi3*CJE+Hg1O=baELQP2MpE6BIaRStiE)~!Ob1;APQU| z(w5ZGmI;1b?o4zG1vSWNDu-?x?J3>TPcGXDEF~2@v!M*6!U0$TU?;X%)e(4LN83hX z&7bak!>qZeLz!L4IJe*ZnUS2Yjhp%Q5_}MjVwYfMk0_qexECYx6@lql#bL%T$`x1-?sav?8z-@ zj=s8H{3pUNPZ`p&`2Jp_9|hBlG81~i1^GCE1u9h=l~n38qPn{8t!4!rt|eKro;Kp3 zv{!<80Pqx>4}es0TSLG-H1qFPkBf)VbJ&l9%a_+5=#<{{*rRM<_UY<}`n6bYhc`z~ zvs!zEb0aF+Ne@={#v_v~pDJ~l;1EWI>7#8*@n*0pqUNfhKHk_gUsct{n-ot~M(JS3 zMXn9S&Uet(DQMojF1LYFFj+ct_RsTfQ)1*5vNylDVscaCM}dlTb%&7)T?Oc~PV=Id zko{>S7-}fhmn=BuXuDnB=?cz)G+N9Vj;8VmZ&@5N*#PrtHu{(IZRFovA~_N#pzo3= zqav-6X;q*?Q{n@8;P~}&Z&6MhbD8_HWoD7|+n~7GJOyf!*$l0VlWj?J~)7VMNe z^f0-CHP>sxpjQ!E)poJMow9jg!10YLl*Ml2+}nIj61m}X^eneC`e=0yqxz%vr*iB; zhtmju^~8|0$1WEC1>l#>m{NRFO_{=NGK;6soFEuAC2yh9^%@jH=itCikf)TcCg)WN)WmWzHnu*-CyE4h$S+x?6PC7Zu#pV?vuTPmR zA+{Waw6k1HxmNiCi&=F69?MAoeM@QM?lGb8Xp`~`ZYyv2fJ(JOBi*9UE*!oWjj|l= z3rtfD!}v#QVklW4B?MBp3k9r$3yEk>*(Aozyl`vU9YZ(pds@t(>t^=!q%`@rok9fK zBQT%$euAvx{~=$Mh&o_apq;E%=y<*fY=9M&?Rpu=Risd2^@a>UjC@T{aP zH31su-o@*ANyiQHLvCvG?RiPq(qD{?(uGa%C6AQ0%lfQpM^A$JNC$JSqXtY~_6zYY zhE#${#aR{a!uPjH3d|H5I(5U=efAU>3ygQsC<4Fe!voKzu0(Z#jKl3e*i7*4ZOO^h zcws^dN2Gs-Gwae!r&$tmTMT%ioy`!<>VieKL~j79qi4sxEVca4hqO8;RON zI<}7~)SlW=dY@c>L9Xz4|7U-r*4%UA9n0L*huqYE6T`wbn`YADrk!H1r?rz|#j$=R z`_g5-<;7gOogJG=UM*lInma^@qwyhnLFhviRDHPKcf=d?f8dCyx)UtMcR zI@Wd|Q1Iapix#dW@mn(%YfpxHbVaoO0N-o$&>{?g+4jz@$$!s0Wj*<0lisE}@m!iq zdFl47aI#qfPD50iANm8XSFjGiz*e4%A9DquZ_ z&;!f-G%4Em-IwIh^EZ}A6vs6JK>qvN)F+X04(}K;LsEqGK2Dq4nL?_B>Fg{7k@BjMhowp|)GElZO?>{k%;SN4-sUoQqUZ};Jgk)b8 zOfm?orO>(Ic*5N^{Yg-JNJ*nViqER|ENj`EG8TV<<`ED4`6VF!r8GCJ@+o;i6tK=xcb+IfM5Ds!BaHzp&4lHXj@ zAITrZ1;>M{Ig+72$<0BKq_>`r_g6`^15RQji4NoHd#8I4_=eB9g4!i(EqS+}_Wj;%D{YfLcus!e+X*Y<2r7BLKnOoek2$jA~rV z+|dV%GBFLt$vDpnEJ-y3j2ch~dYzazym3VPWm$ZS?l~{qug0@FvW-4tx`54#;mMi6 zcc;2YFvsx$8fm&M(lWl(OJ0x)N}u~lom4gFcN7;Ivuiv2tKGw~xj#83ULkf};+46) z`L!-f_aa{Cvd$(5QI#bXG}?%#EtQl4q(O@{9;CoPI;96q`50JQN8Q?E+17W;VK6v7 z6OhHt2tgah{3R=aqlPU$E|HCJQs)LvZ+>8)4vM3H40_jZVV-obdT?DX3ii? z-wHsEBX*Y9+zU-`r6@}Jvc7B;=5+H${kOBTxlAHFi!4`B_oPm37`4nEYo!dTCmqDz zo^{F*Mxg2=u-bW_qk~@WEgHE2&{t}A#YFXGSh?jp?ce(x3v37`*d991B5IeA;#9{+ z+bi&r)+6t@Q(}X>s7giVK+9t*D6$%Wn3*-n+xtM5%~;AmLm!|vds7jS+QUI+s(!ZV z+4C*yDzs42mfG8C8sdUV@%}|L2*G$Sxmkv>p3caz@B5T$7_VsrK$;NBZ}QwZh7>HM zX*PsV&Hmj$AH5dc6zborRS%j^E)Mv>&1XS%4475bNd~Ss->@VfA!vjnfiBH}!ceFO zmCLHICF1Ym!z{w1z%m!LEMup2;q}hWmh)W7g3*s3*qEDaCr?@&6j&y5&^J( z9Ok$D8|>nGK$POa`C@vih`C|fHbBd_ZPLIZv@?s(4=g%26Ch6)?i23@A2zuJ)#@IG zty**nb9mNbI#AloN|s)UH1_ZNg@20!v0Y9$1w#V{Vanzen*iHjOqRY-1`o zB={H{XMD1;&{!0F=+zdgi8GA~t8Z$<-fVDn50Prh=tp2Pa82V6ibW+I=m7RCUWLB535wCSWK zY1NAp>8hmp-TbN@E4fM`xzcwF{bC^j(;=@{1lH!S zeSacPC4G$tOG_L6m_aqMk~lY<(C+N3TTfBJjS#sc5%w>la0EI%evm5yjurLMsio%E zSz3Lsz%8(v^hxR+HuaC53(SrPzz$vB{uSF7&DQ`5PTBV5;hqsZf|+O0F%>D6eIyux zzl;&d1Wj@Gn}3@!#OY`oirmWJX*WC6Y^OXJUX7bD7O5z*0JTuPWB`Tbdy(^UQ~nX* zQrpgWZ9p|y*0>k^S}qh%px`l{)GvHk>wdt`p&mFLHQO$!B&pBY9*{p2Qv zJyFALq7)XeZ9nC-ABiCL-u9U}~^eqDB4gZ4o zoriQ?++?N&`z zwIV@hYD-dx+zZP{*0fe=y6lys2?LKaWd)t;2K?Xjz90yQS_VsC#IscA&}h0VtIGwqrJz8 zu>}!uLnzT`NGj)hjE6%SeGQ&S-Dc3!64-NDe^zO7JL#9|m2k=PSa_~|D56%YrGq)i5;1Z$0MRhk%ELxVd#f(K*DnNSPzx0 zp$3`*i;SzE4iu>*aRt)#->dM&-qP6RLd3uWA^UQrWpyd1$xA0C`fk>PO`RP&+LXvXdIfsi|qehC%Yehg(d&nywf2PezQ z#i&Xih%*N(tt4fD@nC3|gSGULJ_99=t&hh2ihjmZb?VWRiPJJWF}P?Wq$;( zPrjsNmLvx~l$`H%8XA%)oS`IY%)((&l8q4Mr1_D|v^SFOgT>t%ULxc5VD0-PPltK-HjC3mbas(bl(>Ym<^IH%jbKovo4PpV#y2@8mhy^Qx zJ~AdeW~P%7SHptT$xC|hwgN?dE%qOk<@m`ouU97L9g-*0UP3;%yg%ekex~em{jwq&n683n0aGem3nKjnXL~}DO!4vqA@ZQdb!t#&8iuEdQY(4nqWDM6g_Re zf1HpO#)elFqWrH>5IPVvH7ZBKUCKm>7qNn=|AV!7?{UvRc^MP{} zj)6rbP}Ys%tn65VB=E9|S~?KqfGQrV3vxF0cc!w*$OR-Nb1dI808eqXgYOnufvMdW z9*;fov6~iq83i@$MHcO~wIz>f9W-gxlk9|O0iXijomn|UvD)p})8o^j%>Z?Pth0zt z0eIlqdjlz_kR-bLMQtf}w=o@~r5U6SaJBb;)nL8 z3NC#fUoMvYY!0lHr`p}Wv2T0kmh_FWCbi!6If?(Dn~i?kWfzAg)s8N8PJXO7`En`T zjx-bZr{A3|odGQYcN)6YkK$7orvL~Fn|%T#QrD(KV<0jwb*PMG6WyR zMR@y&iXF|4hS6K@04KLQE2bnRepp&14>@iJ_lMl_ZS>!zy9Q(*~hv&x%7N*Gk4BD={uW`7HK2v{9tZ0 z=#}xbeq2~gDqUWZ$3e>@@{1R!P#&D=i_r3JP3`_qXkd-k56ESBiU0(mSVMPbZ8`Bz z>Il8c*ty(q7PkXvRy)&rP@GiT0YitbU>P@5)v59uA4n%6y zt&w}G6M|$IAh4GIBa_4rAwnoQhme{lw6NG2^8N%M@P-;)qm!M4%p8Mss)ody`aE=_Ls!?^+W{{&*H)!ei6i*MyBjb zNaWon;gDtoqavqJpz@PIZ)ZH(fSMJ6^a}>hc2cPJFa0;1;HSfeSa7Qf3)^F9e(wXU zEj4E1>vPxr+lUWotAZwY_F}z2WRc_17$lHC1etD%*Qv#m4O-4SH6|fUSA7!ty7fY; zYj^>(9ys;}Vzi$kCdn~4GUo*#9F^Qp8!Y{cHcU^0$yUFmYdE7l!lG z-x6q3De?WdQzgmFho00}Kv<`+5tsHwVP7;I3LF=>s7viC+MQW za2`V?kSAM>8x;}Q>-uHnk9#vr`n_*ZD{f&L?QEbMToa?0yEw$iI*B#=3m!}uwzRc> z{^eyMYNjyH!&e~DfzO86e^Q?We>Tv01)GS}d?l(Sw65WiKWxP}=13trQcXzcs|K5lebrF;+cnfC2f!2_y%A_%;=nnK5&?Ht|8N4@ zw!EfD1#^GM9I8YDoOfIm!PWNTN9ywL#+Zl3%Jt+8WVE65Chbytdb!+latc?%Rpih| zi6SqY)V+A+;OIKB)4!@Yd+;#yx1xx<=~0cv0#EcxQ z0J!teOS&O-9KslfvkfoH?c=C&tE9`%Td<|+o0u%5ePk}vLZD8CYp#YPf-i~f*C(f* zw(}TkvV_9-Hi|54SpFlBsL{$u`Ocl-0daZ$(hVuiVE6b19M7OKVS)a8KG1zlJn_;}@Az<^*7_ubjsUgzaA41~9`Yp?KNBhqN$}bgC!NM=&?D zZX?9+d!lW3oiYfhZ9xd#vP^V_L6_>4ofb-S6B*RUJv4mxq^jSfPCf?4>`!SPpi1N$ zQam@z&XGzeGO?*g})su8T(plX$V9RMp@WIC#FWpx;1)` z#U4`lO6>GXZmf4It1qMxqwaR;KpE5=JN+NqqrtRoS{YcBLkqE?F&1ab98&5~ z#Q*>R0YRG5ctgpQz=D5}8MFlq(0H{o1f%6=AxFOh*0L6_uVZ~4Hp0~RAvB*siZk6Z zUfQ9g&xu_DnoPo_u0HOC)OoV#m49O%)of2Z(YK_@tXq6csfJW?M-5MJXXe7It(ok@ zfBHMnm|6$W6|^{(qnI?;T)eaC;<8fI3``uq-z6jE_0xvOAoSp+@E{Qmb`F?4-ggdT zf}TpBI74@$^e6u6AL4|1c(PGif8|Tuv4iif#+XTJN&Q_rOY3c9DeSMn_=1{W;NZ%B z=^2w$SrWGQ-q_mQ(SzsLu$*wk2T;-&XjT;dql8r*9P+Al=VQLDo=>=?Jm4&GEl0kJ z*?yztLJTLNr4zCh+6B>#mcGi6)%)gBNH1|^Lai9orlv0!hu1^=I`nw&_10#i9%K#h z+)MtPjKPqZsMXmMDKhIHbAB2AVtPYx&&O}3dt&+&G<8NVQGUsW%v^S1a5XGR&#OR6 zG+JMvaBSUxZwbyH{*tYwR$AWhB}o_-PM~SQWL@rb1(dcEl?DPNoC$P&wWqj_I5R zi6Sd=#4$DlriJRlFuBN!i}NXGBYhvTk1BomPke{^!dU^Op#W`wN-35hEEl!op!!hF z5kzSd@kLNQxicwAoZE1>@fB14*%x|cP8t3p>?st#!HHcY&^-1B3Do%A7m+679GitV zQshMxvqqB=4Lze$hP@dv#;_&!;u^2)4als1OzgoG5ety)Qxxzmu2Tv-yT{hF)GTec zUz?-(^u&I!k^zV7qUbl}I9hUZ|MnPUVH2Mj$gqFH99Tg>1oxePWgQT`a=CCb4Z0rQ z-)fhUs|G9Vr)2I1I4nfDi+xaoe~Yb|#3BFM{)B_XXldXs^-dLQ^3&pZ_CoI=-FRi_ zGx_sSC`qzJj;?Fm+(M+~I%&x%;|#My@h1_a7(ul#?uaLm3YIr@Zv-8%H3#kuVAxQ; zXG@8ILNFX~IXIWBJT%K42U{g7dgw?Uio?kX3+CUYzo~Gyx)49p29r+*xO#$$#B(Eu zpO{awL2sC9h2Y|rarhaEWKY__r+Vkx|6WfP8KZ|CTP&NyepO7g!6+fO1ne>&L_8ou z1tUYW`RoI>bMWQ9V|+TV1k&{`*M5hE>}tg{)nZRSi`n$cR42^ycGjnl$VlzNWCGEVEh|<0 zOgT0$EC%7^#5-IhukpygQX|3Qt|^;|unPn6B{a|g>Y|^O z4SkV1t=13L41to3+PWyHjqzd!-BL7kUfQA@rm^AR)^}g{VslVH_zr&KnI8r7bu&8C zZ!rD2st0e(A3mdZM=peEeFHjk^#QG`dioTdfG>4qm#2`rgVjA?dOfL5{Z~mtGG``~ zpB#kU!PSF0KE-nylbj{WAA66|reQqCJ40 zaOy36u*tHeakajGvYla2FPyks02NlxYA^zbEZ6s5w@3BM0=Qeug7OFe#pQ&^c;9|J zfq$lcgnQ;brs7qMo=n+cmt+*{U2_rXxcc%a)*v?Bs=2P|#vdG>TXYi3fJy{xWv?cl zv}$k`8Me;I3`}R0%>ueyNkVmCoKMQs-J*rRvGQ&NoX=zahjdAWJM+O4tMCPX?V;c( z4(WOn%$FKvfdt3}+w3@~aP5W<>E0p>&qW2)_Z-Jc%8kyfrFk_MOhEC0t`i?t4OL|S zB7ouol!HSMa&aFi`74*mWZhQRbuUQktQ`R015VR605RicB&w*_zm(z=ZU)2XoRM$+ zlLR|t&Uef|hrlld0i#vWl>ve#Qx5a;XTaYLmHfZq9bXV3)*@1GwI=Rq()B{H-tF_) z@~!^UEQkp~!=b=_@ytEm-T8SSAGLeMRX`413_*Zp$a7sF&`Q#@E#n}z+(l9Nq5PVX z_{#%t7$S_x%4S41YY$YX{H(p zI*1HI<7}ZxegYXQ01cB_20B!SaZX4QHe6@xZ|t0$P3BALRXT#d)qVBS4ZKMEh{X>C zIn4RR^~Oo5Fexdh+pT3gP@5AP^>%mWccE}x44=&61WdQE283algea4&`H>o8Ml|3wcZ3(I>}o$RO{`XDa@vD=w^4(JJWV z8%>LQ9ol?Ak{eEOf+AdRm^ys^iXP^h33%C)_oysJfD7Sn!jTbCx%Y}OC#S=Dr%6|{ zNLSaKOUw@XK!^3nh;*6yv>_J%=@|t6WL+ZOJwjiPv!2 z5CSxq=?!0h2`J@CmE{m!H6ywEd?BCyOM>N_>vN$3J*%gzLO6rs6i5VwZHS+;gDVv~ z5$D!6dJv}PCdNnu$nJjCF>FC-G-fm6BKNQVl@8MSv@Cc#tT~VGp09E5BKyRtdSMW% z)(Zy?fI!JBMs?5r)a2{f5i2Rs`0UtzpOKqCf;YJrB`6Zf^LSPKfcFYME0VL8u9ccG|QkcD`LW^Fzg1AWM4_$Yi@|serZwEa<{oi(j@N zH~#u{P)g1^2@R7%9g~PLQJ@MaG7Fb@OdWI-{;AtNEkmK+K|9N#SnO3yN2X85!7*0Z zb@Wuwu>N1bGohwF8`1d)BN}U!WVS6D8Zd~8zbu5R*tH+>tkfKW`xFVnM25R()gI*t zx6!));0vk5Y1-RaQMA_y%blHF@?;lpun)V8zHi}6+A;!Et}kO&D zae%+Y0%i-dx3d~!v=zccLfqcAya78L}1-%_Dw z3J$wLSG~4VtgF%~iVg#5Y>HIH@MfiDO+3*EVJ;kfjNc*gD#xt!mR(-b#2Jrh+kdb?M`VNg&BKy%s581w1}?Fw>w zxAWGSGOiDcW(p4ApNRL&MA0?;AH8hvP^C+04L;fj+#Ial5YmP0jG*>m(M}rl!6KCknB(wdn*rmWyz>@QE>hA@a1{XRVI9iwji*>On4 zF*GA{JYXjO5`P4QS6Eaz92ujgZ2VJ?X*#w=(+YK~h~n$Ny|4geLKaLNUsg;luZCj{ zZ-xa|CWw&q@OpA9*Z_55N#V%=wuYvS>3?z8V{x1N6Zw;x@Hve&^PSRu}tv{3(S(@9&@j{-DJPJevq|{cuE| ziv^Ya#WRT*WXMVDP5D13d zxgB8IIzp0`JSxAx7kf&d#2uhmM+gC?_csUmLj&=7S*oMT|IlwK!x9+%$m`QLvF8dS zkEyTYWxU0kQWJ-=$Rj*M^%g&Zk0oHK1@8=tc*Dw5QBqjY^j7+BJQ$!$Ic_oc9C?`` zO=FLRA2w}I{N$c_2TItN$k{f%O&Ph!NRe8A4U&cBevYnAB7UuJsll4Ty{QSFj~Lg5 z!y8hvvUStU6eNP;^r2HXa1^s$j%yc2VP`(-_guT;1I`mk0s7b7C28X9c%g=JY$v!u zAKI)axmxGS76l2`Vag4v55$Pf7OkKBfrN69B9TPO?yokDjIu7|ik>I80 zpY6iJux&(2J|?OQ?0@_`-H65B(`GKVen%>0Gc-ueU=!a&$;PILsmYYt{<9N$@thdXxN9kK0SA>q zTkuXXvS)!oI1CjkiZg8$SY#0A8%VZh1Q#%ky%kyhpR3HxGZ%jH8pcfX~) zJ{VoUNve0zsl@2n*{XYSA)qH>8J;BjvIB2*bZ{fDvM%V-<@Bys8^3HHO|mnz4E0gh zve2(_btg46#Rk;UHGVtxy?Tc(7ehv(_5hoP2GQ`2_p8zXAKYg}LHxkX+@}NnAEGR> z0$A}HB!2-_v(lc;Abqv-(JhGykEubwe2Q#o$8kBE?TsZaf?8Ix5hl?rJw7GrY`6%1 zV+2|^6=T{n54`v5^Hr2Xs{b#b;dxb5e=V9-uWRjM7SvMLiF*o4+9oN0u_E*|2r-3C=lia{iWkd_JLZiTaFdtWTEB zWFPa-9569>xpt@)dOvk_gObD8mJjk0;S2IQoHI3|4#k+bZ~4#Vn%G7u)WNB-`%?1! z*-iL7f6JDQie>bs@rOUfQfOfr?TmXI?Q9waT*O3nn2%}Q2@Hm8NsCIV-G_stTNXfm zeyq`EO84?doU(zqGT3x`i~NzqM{*3qxCPEq7NLL+EZ0;7W{1`i@@F&Ktmq$y{o%5U z$zatXDpb>RhA($V@9&Tv20zP6JrLTeDPvj%)7!(!qil2P+sn!q z)!r?x5bIthNgNaoR%l7@DKF*&TcmV<9WmiA2G$8!k$&-b=#zKVaGCpko^ zV~X5en$Q`gB2G}tV^ww0nTjcGGy*=3{t96j(1A2E%|lV(&!(CEExALLeH8t`V(~+JZ}IpLHm;5{EWzC5fVrBB`L>rIe!{q zQL;^it8s5tv5fCbB%+j!g=QYDb6j99F(AT*p{Mtr5P;C@TJKd~)tsawqObkt@wTj> z8nuQ!tr0r7-^nL+0Uz!P9)1RM$jlSVQ49s-*Ya)Wu19QS|20hHYSS5!fS*$q1PfQR z93AgdJ`q1C!**>GkCNEy63hb0+ZVsm8A|Jm;oz9rFM)@e`!S+o(zSyL9rc87pTu&X zyeyH8Wvl_LUl0uYN#(oa-7nO)GG=t4fCf{ZxDn!?F%Ng^u>q#gz}q?r)(rSIpsyTm zGeob2{ZB%|kR=&Ae=e^%3A4h0j=mxo95jb=Tldlw>~yNW5n$K0ce(=0ci>lN;55u`)lcAJ(rOTu3Rm1NR`PeWs^K!F<3{OdoNM}JO8 zx$)H^tkBguv9`XHN`4jqoN0fz zi&x+XlLo=01_#^O*(`RHYr-b2d?!5*88V!pv?wUQYF>9Ee6GlHMxqG^JvqSBiHf(? z{q(W=7!#b)D}2+lpyzJo?(Bc($bht03PioLmNI%?a1!)6$uF15~D{$v|-dz)=Ye;eZRB4+sVSep|5NIU}#RTSLe#K73g z9Ti7y9Ov+vQaLkVkL`!gU)3Qsy;ps!-*I}(G25mk*ogWGsS0v(0$Y^IyjRfiK#&GW znNw&c`RT)44jV2HYg}e7T0b+fugQ^%3fj<PZYt zBxMrLnjLPqXcaRvj6@)eYJ$Dbg4ssj9pKgEO`0-e4jkK_Q0Rh$PrUj{@o^j`f@{fk zH!BQB(3IU5Pl(~{Zyq&g3#5#?l5MC?Q63>!)SRO0{7Z398E( zwd(RA;#2mOC}zJfqaGsOtu3aWZ#JAd*1>f2e5P9-9*(aM3+0*BbbS2kT^nlm)Y|pA zCRilU>W1B^J(xz)mShXFo|>Da6RK$5-lLd^oNJplodvk14dG3g_burKITHk=P92BY zzT;GDsxO&@&fIZ9k=gEBd;`qM4gQ8VIKddOBsy3++F;munL;5Bh|)9w6xHhg{4-H) z^AJlL08gZv`|=RCZ^9z~gPO4#wR+by3+{WTb;{jO#>xfUt1@I-h3C%IjeXBVq3jD{ z5Rv{)jz3To*kX_ zI#zsG|6xE*B7Pug<*2G+kAr!kvy~pBT_PchZF@nFwv4MfcNm46qj&+|z{`$qmN4%* z=P^WCJ#ab?R%4R}=IW&ssyC>5{%0;CGdCQ!(SLv3P>I*iSw(*IGj4yryxG-qLf#UwGx=G_S zI=MLEWIZ+%3<%Ky;*~Wd+XtMUXiweEFIqvCX|9=Gk|B0G3FZC?Y4AN|V}iiy-vn}d zq)+8$Q_06F=8g(@a5&(2dG`>-Ny=w3MRvsL0wb~^@JpFjnGcK-1Z0-QHY45n=C;i- z`?sq%4Iz484Q%5ZIUAh@d0x=@Wu3ZBJqZs{A0xSEYJk;EbgzIK&n>`~kNfb*5QPfr zfamkjJ5lNito}Fw^R^t15lqPwbPosx3(>{+7KH47K#}2ReWcUhkl|q*t#N9~ennW= zteSnB-Z1s>Eh%BxDr4Y-yrJhcV^&pIt@FEylK#D7sd!R_rtaInDdgTcS$v0y>ce1Tw5CrkojoIAy*lhVI`;3B+yUEop7_sc+1zuVx6N28gHF znIjhfK&f+(i+F%=5%@iFdp5QR21GF~ql4V49lLHk8KU`bTrWg_b7)m_i&kLAG+n@Z z#1MZ64!YHMpRi5m{_u&u$f1Ur>1BiIGrbEKK12_wA=DqIbGtw z$+#nO%FM`s{aOQSd_>etsw;ij7#ehf(yqshIg(>O$pVQA3+)-X1od~tW6%~D)hI4v zk17b7BSCvcc)IXb0g2GZIn2*hiCgy0(=T@y@KD@sN6mFZ-}$CYjEmACOKP*z zP~NaaEtR-%Tu%A<1VV<{lEc^bTb7hxUuoS?`{ntU9p*3p2dnhODkLoicm+Q&KcR2P}byt<4Yww-{Xii z(YvZSYk3$b$4jiYxO@dbbfaDUG9f!n%PW7@!LVtP=_BRwLin1H+ zn+}bQ_c6B)KZ5gC(|jMunXTqKV|VkzwmOrIQR)&;Eu>yf`2NtB$`3qsc!hQHwbpVb zhV4UB=0QIWr(npX?-`xobK5X>SKRQ`2%|;7?u!vDo65c7|1~~iCEDf}wpTcL7L>4{ z*>ptUx8sdw+eLb}1z2?*ps^Mu+S`&_pnH`2fZQ6~PXX1T|37Ms@*Gf)@Jk1%J`T~Y zy@gqzzUe?@xDk``AuXv_HIT?Vb-~BjH`AO1?oxojfdw`$M&oBI+V*WX0C9R7naE27 ztj(eO*!Jx6L8c^P7#?@e3x?pe2RUyf+k*;)o&n88I}`YvR*?b>k{e`)Cf%cdJvqPN z=|;Mq;TwWP#xel0YnHl>g8kj-V^7lv9SZ)+2rz%-{>|wda2;rPbz1dVVy?hxT6#;k z1^Y4lh1eyEbw4^(VVYK~KlP#7p=q5w_HV4-r7F=3;W&{NL z>uIJ1>b?G30?7(-PX9$;av|Pmd+`}DX{FS13Ty#XBEdd%J6bUEU43eW8Iq}-#kra5C|y@D@< zX%$zxuXT(|kOrP&HF?E?O0yxM$3MUL!&~u)cSWr!Y$Ft6Ug-Lvd;6z%zHgDujCb4Z z&I$*n(`XSU^zDv9d)Y#wTQ-%W%{^2NVOtLEwI@G&_LS{9#QDZW2V)5aE_P)10F&C_ z%~tjNz1B#}Jv3H5R*IbgYcAySyI*OGyP@@QZpq13Dm0~>a2v=;-&QMy8Bj$-I8m;F z1oI+ag)gxlr;=C2S9pK4U+SSoT+zXw#<`4|$?XhH^FNzY$&HzmslLQ&NEHpyGds_^ z`TqKO7viLVo6r;f3lCM3P0+w;;FBVK%aO7c4l?Ll^`AIT?m%w>y_#U160^~8uT$~v z<=EG0WZ+x!)P2n#l<|v^J}|ciqhL^ zAKS&y!D;V{9c?wO z+zqlaDY3`sp!tH2zyJUM0YRGbctgpQz=D6hZbIzV%O;P7N`{|e{8DRwDIg2L`(A~S z&Dibz;8c=4JlgT>8uFLI3enQ4T}ZRTr95qU4d^G6FO4$DL~4t1y?M<2!m4sb_vCuz zs8@vNn{NQCEUt8Jb<_HyjF-f5qqp2M*$mH&UhD0h$QGooMt> zpVrcJR7Z!>o;x%J84%xsjo!`}fnKpv%7i)W%@xZ}o}(=<#!UX!r{=rqqivbmCOjg% zVndxU5+oaLM0^Gr3!GSUPFg|3{NQ|3Q({G^@&3BLoQ>nd49{yKe8vmRTRkt@{ee(h zp>&Pqd5YfVlZI3(jP7gatI3Ka+rHfHnyOX>x5bGxeTA+7)@>K|Y~)YQZX6nEm|CBIwCi)E$srL|(x z`1$^GTP2h7KLup;<7_Zg?-$WmM&0A&jr_>$o7ex9&^z^R5-RC?w#6#POEnM+;j=R^ z+vmxZK#hXI6ETV^0t1Lsh5L4Fg%snt->?0q^FJu}G(dpoo4ZbhGho-G1H}fC%<+bQ zWdaD>rprwc3MdUI*ZN;`Zldu5JwC)1H>PKCFjH$>BwYR~90KXv%Gg2-z?Wx-Jhf4v zDaGy>uuR|WadZdy?l9$GzGX;6nu&_EU-4~{xs*gN(x`1mn(F zA1b(T0i6@E{;_WLDs-qpFTR&(U3B0ApIOfow$B{Y{Ts*t>0rk1esi*l+0K+v3GA+d zYawvywGU0r%W(1jCza5Kj(^WzK=1LPAD5&_4ll4dzqFfyV3?e@5MTkYo&q43WM^5v z{N~{qVFJ^d8}#6OgM;m6eg!Bu*W^fFT2O~(P-cGnxT5Ihk+&?0J1N>nXd!>0Z9Kat z$ai{KH=y)n_Gt;FqS6!t&K()gzdY+FtT>EJWhN=QwHT28ybV7;2@|UAw zrA5;nI6OjAjT!JEo-Wph-G&UG*xuIHrrc=!v!L@T;*7p3x&I~Jn-_}Ml&#jP=}YUB zskg)CY=3aVaG%P3q42)Qj&YgkqBS4pl5Fgk^OLdWX|SGxPbp+ps&-u&@yIi0?-*bO zBAaqB1`Vb~4PVM?h$KEOusJD2JHDH*WRW={Civ2*tFimO8mc+f8AxVkgo{to9GiMA zQxrf=sgVhbt{TLY+S-pCQ6i&wKqZ~!>G;2H z^pIekD_#0>M!834Q<-+>Y8lnL{>O0b{0xd+`d*eIH=oPa^MN*%-d{RR<@!GjW=!Q8 zZh2i&Hq&T>-57UpSrS=TM6$;|Oa4vY5 zfLzI8K6FtKaY=zEIwSzA9!$cb6j&`&7`pUCi=ntOi3u^Rb;$G!+rE;29d|=yph3Wg zexXSLUlba2s1?)iq@hIn!8Ss%xYuURw>UsIy3F#RSLK2-=QQ>ZWcSkMpCf}bAN(*@ zRcwW4)K(q#I_Q9Io!6WQK+~Sc-}d5O^=#`S;XrRdn_{)O+QZeYEOdH>89)0Hn`?%x zC;cBb?Z{Un*ff>#FyjmD60y*cPaH&I+#6+`{*xr{cDb_Zf9IZ^M=N7X_B3^sxRVF1 ztiKp{YncgT1-PD#0~BNSDh7pcQpVZ;Orzqe;iL&h2jMuch@}~A7o-yuj!TDx5VU}h z4!lBJPK)9`S|1=u(jm9J_C;8HLpD6JYN%%75l)S0ThUviam&W3=ej-2LzS%lAcK&I zFl!xB`oM3WuuD&B!97_NHdsIV=hykt{}J$}!>#9G+RIh&=+6_yJ)9$?aCOAFE z*0(87*t_g}8B;W(kG?XAF`(l=@HrI)nzabPm89KtN~2R)Obb_71VwLuN^|0F%Eg>3 zi1hba&}>)#4>aY|3PhPw2o-h4Ej)1q0kX$r7||DMM}-t%4y`Jp z7R5uWAZ@m+u&eOI=b~aUw~$i}pWL8XIFxu1c9lU5K3n><#zQ>A40uNw_u#rF7LEeS z=LdUyQxEF$s!DaqSL%Z#8a8nB0t`Aj&_$yYUsXVMLHrvLrOmqx7kfd(Te$-gT&|4Ke&8@L*+cu#dI!yCW9-CYMY;uYzTmv zxQWn8y$TMYouEN~I}QaYEhT6+8F+ofuzsTmUiDL*d;D_%KdB#gKt23Y!ABrTr zR5EYrvf|n-7sTn*vuMgIZ9Q!gPTRjkO(UI9OSVD<-Dsc#Bxvm1K$W_R^KOMn*Y?1R z%DVbLFUD0GD7Sd?>y66kel@X83FZE(C93+nIo)wbpw$Y@>@x=Aag(KF%eTzmRw4EL zTh|Gb?Hn0hQ9^8ZjfL0C?(Z@kfo@K`WGZN9ywPrN^$7D35N}h)d~2%Vcj9slWsBN_ zvu(4;i>i0KEKHewGc~b{szyO6o^MSJk%V>zo;K2VCABewX}P%8DNB%dV~SrY5{ju# zRq%^sGnwZW!U#O@@)o=Oy(ZzRUNtkLlIk^T-$QrZedd|J2a{-_9Hhw`?KWwN8=0ff zga1jD7OHgw7}l*tQp>6I>#4Ejwf`n#p#`uCQ`o>-78kb*`0zJ39$axZ`ucCfg_PIXTPzj zlTk(LcH_B*8%Ud2)U-W_KsW7C4{1=JvNk5k@1EI~ZXNdpFOt1AP3+ghi)B9IvSLi^ zG&K5fh?GTMy7Q%kcDOU_q#Bsnq%bO)1w|!s5b_~2>yZs#zc@SK&>)t{gdN+%!ruz? zU|?$1{-5AF-`Y~Lxj-BECO7#Og{|PgMbiK=adF%1YtO+%95LjXcXMlgoCN>;cg_;Avb1Pb6F7m zZSCbb5fE5;wUL-XMSpEm7t(eeavNY;y}h|S$o!B$r~k5U? zzE-@?XSg2D*sH+;KAcTl%Z%k&3cBOq1;VPp?U!*#bN*&a!`jg%U0UpD#4sapsZaKV z6Y4G;6W!qkypp0v-^9l4KU`5@v)1AM-ZO?GL`rX}gyiR2dCSk~*b9+Ez-n~R?>!Mrd|*UtB#secpXdr;2% z_2{{FPwKk>H_O<1)Kq6JCoY&AK*OTP$F*bB*R1!Zq~SYqK@wmCmivIF*rboZWEG~k)j4N`aqh^qwc9;-qeKhqQ4ZVZYJmg?uHCKL7 zwPbtGOMpn6b3yXb;#x`OBbmkUY&Yssx;bcg=MCgMZX?pW9OA`u*Eym8i?i#QWw7WW zA&Qu^XaK?<$?ndObjTALv8|9E+KD1~wZ?z{Qp719exz7aCb%v|Wxt}F;`(GY=6l#Y zFigigPE~UVTaiVRQ69E(+knQxA?_;<6~mSd0&c1(;7=dQmpcs&^O!DS#V=|;B1Q^m&pyGvCVADM`xG9CDOWuARqeg+eMed}3wS@G8 zxnyada3+X&Fzlo?kp9cmBoFPEDaUfK;Mw+9w7 z9JD)S-zfnLTsv_V>1#D-{#bYJ7xM4KT=(%=FGwJBKvm>>ix;*Tt(7llc#3hR5=T>p zy0H-1@ipJ%>6yoieh1mC+=vrB#UL^^Ho8*bP8!1!WzlM2bW(So5sI(gX`|{N=8&c! zYFOjsY`8y3)$c04fqrT@TH3ZJnbe*(^=dR=vTEd9IN+Ns(cV#hpTlXK-<>_A(Lt?= zHS-XdedYIU@y%9JD~{4Rc$)7&JUePA_;6IDlpiKjppR0bS)684Sxk#VaGT!cnPf*! z7X#C1!p^H1`4@QoERPbRr5AuHN_o*1(oFxpocBRvQbO=1%mlIHVuHXo`MR3b^ zyvAPeX>X9uz^3>6e%kJbWKRLHJ1nNS+I4T+k%yptsFn(U_%)h+N?mSAT0q#J^#5Np z^7c>AS5G%MGoV$8k=cB9?0CCY#%tU{Am4hw#D8s1*DCZpg6DRRRp^y znrR(sJ~_B-K&_%*07`z`LGk-jD5X6|HQG&y!H@?!C$>xQz9CUPky;Q4t)8s77>nlj7}1?%mdYq=>* z;KfMuS=et23{=kOeF z3Xb%X9ITRxQDAdurw61JGN!4lgl%)LT90xgE|^^xh3G z20)b9- z`9QImyhETl?Aizej2es{H&sj+;nA#`v&&7F*5A~q6>)v#!tx=)oF;6TP|?PMiJfk> z#NYaJH14Z-{`7}L#R%_@X!bWUP7PG5qjQ5>Cw_awis9ACE}QVm!$2w}^j&su0^ux% z*uUhC;&#&n1E$LS?E*wU@&MB-!T~z2pSD$rK~7*-uI`!p@VAjr`|)1>K$#n`ws8Ju zEw#xdA?@V|-*2!704hfD!>)>DOxAQ4a?Ol?e2(?Cabc1*E;YJ5=~0I4N0w1yr1rOs zvZq5kdW1B^edEq6bu`ab&n7?5JzEB=Eb_f|4%`M|OxsWV7iyM+EWC=5P;)sFYyHw9 z>vQt*O9N5~^sJdRhJe*;?aiD7n^uSo$-W9t^2>P9_j{=2>PS4ENnN zr%e+`=Ds82 zUuT!s%piO86qY^*pT8b^n{EeZiPDvTM+0)Rua%N4r&ID_6a*tgs1Nd6&KLMfhG_Du zfJx`E$hubpTPa6!FcR`2sXkFqZ4++MH&O03gVWF>~f1+2vkz19a%5 zTVt5<-S!?{?%(vUMv7y`VM(-MMiueefmK1>4Qze|yxoGc7y5TMDOr88L)HDK$SyJX z7%JJy$a2gpya>IeB%Aon1FkkyH=v&WqMJC^IpXT>6HA~?N=gzrVGew8O8F91?htZ6 zVuRG&s@Q<1-c)+L3?~rWM3ldqP&OVew;umleuKn#y<;`Ia)+DlmM^}O4*a7EDT?e~ zE~XQISuKZNHoGCrP)J_p?Fk%t1#D_fFq)}xw)DcB^k|T5J(PN<0@9aI(-?A`_(QjM%3&Yr$A{Ge=~*kn zQg-fQ)%rVksxgrbG7hNe<8mK14#POO$rHi;9t^$?cXd>ueKdY5q1I$DyJlBkU?8m; zALwviVGMu91XQ$a2Z9 zlPrpp+#%z#5t9=GpDG#F6KUQdTY|mnAQ?yo*TJRF(7&SXmu57%2{jjk}>0TbcElC3Yx*>a10c2qwj4JlGwcxY@iksa) z2u_=NUz!8td7TV0suMr)vh7P!eFRAT-M;$O_6DfN``%Txj=s85q$mAdk$%L+ivBFa z*1}=#FY0yZ7cjYU-irnj2Xi{3qi~3G39(cjwSE}^4yp4*1wx4eAgg*9VLR~DxvLTWa7m2=Xtm6G7IXjksk$tKjBm_kwD-< z9v~Dd7XxKC7XAf`c<^rM_&1lGgIT7{luQpe!p8btL}P+imvpn3!h?-KuYMJY14w`l z-g&3+=27_0+a)`#mT~bXdMyjE5Hk*e z@*mjZORP2x(m7s_e7IV9zARs%8Fpy1J4ldwe4@J>Q2XTu|M#e>*=5py+@-ZR3SZ&9 zb;L(Ss@!3;@JVKJS%sqAv(ommRaNqY`ZB0pHzLD~uE`zEE}|40 zeYz+=ipLS^#ho~g(+^b_3|^yMrTC$XDMwmtZ;1U>eh67XQ?>EP@24ia!R zYafPh9bloct%aGvVCe^t;D~BE6I7M(OQVY>8}6y+3*X}rOqY143gIKX9^(4B4$IEJ zW91UW>L0_TSA>2z0ES6}rncO+AfjA0XV*34L2|sv64>b%wY+~h>WlP>p+#Z;0hm7= z`>dB|7oWwu>@6o$$5tEYN8I=uG=q4z;X^!r8%5nz0$8W@<6^H`ElT9U1-Z$Ly0hbR zN;b!b;}Be-&vB3nWp-T~L<}eXmwf?trc_YF+T@*%SBYCY4n5tg#}&4y5A&9}Au?X; zrg$u*I|DCZ3&24*pHC^)3vRHCmny%N-Nl>^{989bYmc8fG zX*QgQY;tjuw9lyp2QDPzuiF_btIjH`LSa^x)>t0un$NJq-oh*sc4I?Hgg_qdd|O*&Nkj;Q+KWB14A_OIQyYB zc$AFGLuG#X4g-TucwW~2hmLL|>k|022NNwVo`1w*CW$Dl)FxV_&iap0&-$Y@65VW9 z$jvz~cWdPL+00Rl7g3;C3Po`Vl_dFv<(;;0hMdWR@6tw*%tw z_kbr`xnJx2X=}?t2s|woZTjZyTkMpZ&>c*-GoCq9+l0x?yF{cBYMEm<-0Uw9KUiq~ zby~$ZF#(v|^MaU4$=gDxJX7Jm)in|{njFYT@H9-XDx5kf4pfAHB?e5haf6VJK?YiT z?VqQC#wqpJuiO%V(&;BV>aPFO6zhG6dECFn9&+TW^C76Q0Jbbau2#SM$hmOuN{j~v zUTqb3lfZFR@$ax1x7VPkJpX4lK0&4 zIqkbZolb6z{`0is>{2#t^VFB2>tHUr(~7b=*F4!wZ@nEln*a&D1`iTO^slM1P^h|g zbizUWX|C}PyV?3m^2V09^<5a@N4b5NsxHrVao!Rk!1P?*&!0IQ)gK`(X@5fo$E6wb>->fdBG)(lwd+&fR7LojsDvomzwlbx zHOCUQR4t{FqUvx#Nin36JNz!5pi(d>XMdGHJ9(k(RK1id9U>OB??PI!I+Gm8LB~|K zt$4~X{ocT1LSV^!aW#xIl-(|mGVgzLz%=?=I7K5mv%fSSGVR0peeBlTtbzrw-G6GM zi=Vqwp*)us^$3miEa9JKeKAK*)I^Dbvw%tgPwB-ORKv6Xq>1@Q*D%UxgIi{1^0J71 zG4Zp_nt;zxWE`*IvFmb+!b!1D`Z7h*unVBxZ!f~j>mtYr{Yf*?i)n~Ek(Owuq zyQLn2(Cq&Oy-9x~DP_&tD%Xvv7?bpZ`f=l0WF31>1-gSsWLnv2pea?S1`UFy?;=2-nN7j&#wb?0Q!B4Tz3YHn(m6H9F&Pa>cJP z)m69x0k=bh!JQTNCExkWUHq z?qz~Jr1kxS1)X6>y|z|sO0Jxh!N*nJ8k-_@F{a=wF==Wm-TtStgYrRWF6qwAOlj&e zdN_EN=zke)dmnh9`;U(*l8&wnx7`H^k5*>nqJmzRzuJAT-rp`jdKmC~{!Rnq@Jp`h zCws$D5EbN-pIPVEJQ8yA^K6<%cKCI10pa*+XO;8B-EjpnVTL0eQ>q@aQyd!7q);$f z5eE{c-!iEmvD;^jmxPx`1O9`5^~Y^Xrw963fP6Qnr`*N-Kq*GwkFU|Lyl#Z?zVOUA zi8S{&JSCD2>VX2(F%{TtB3CH@wVUrP&9Aene40dwRaybk)Neiz+`@6aJ9G--w}Aaq zu=WcDP+fxDH-3%AsX3=sqC@iys)f)6tUnEQV36#2N?*~?A6b=v#5eybnH^FJEFwrP zZu6e7@`Io4C2aIRv-3Bk%p6gJ_pE~I?@J~E+Fs_>-jWuWWnh86oxt^_3S$C?G36G7h?wEKRUJo2_fmo+OoQzC{s*Fs$%&D=!&b z4=SdULe2HWHa0xEG!-aw6Ktv?ugCHOZFUCn@%NGe5IcL!NHK?V3^QY+SDKioC)o|s zIUFKTj?p}&p4EfxFlh40U*Yg9_#))AB7<6esyS#<)L1J=>Wjl4hX1#1g;=mekGAO9 zc5*nwfl=Q%pipW=7v_OiI=pi6CE<%15xpV5x*7GsV*4eYj_SjfW@BW5JONMshMS_? zY>_yjlD^B@5(g3n7#|UPm_E4g9&y>*EZT)o1hF)ap!HUx&S;H%QDw{D3Ob^owF;W; zP`swV-wVD-Q@pBi?J8vUnIZw>;&KGVg?^E8v3)l_JhgS3_CMrsx1$-0SS2~!#;Y-@ z#SO|3)r`2xzj!WIeSYh);|5TZJS^LF@;L?3=*#n$x)t1S#pxQb0zv3L7os++Z95F& z;lAQ`NYmWwt2@U?^Sk_Zab8mOEsx{}6PUixa0i=`;T>cqV*_wx{8-x1bK!aa1aSm! z?#iYJb%1&F0QD4$270(iGJBvt!M%5oTf8!wX~wxZddQCb2J5X7ZFG^E+hhx?v9<}0 zsK}QBhjz+0)9_HLwf+wuZJHwQRmw2I`>#qrqSs!MF%7Z&IeeZnQ&Bgbj}tixxG~&= zE(fAjTSr6(rQN1ZXIi?j3=2L_JqE*A@kOa!SEU~P%t1=pTNr_iRK(Yeg%5U@V^S3V z_JKdpZ%?a#2t)UX#*xa$cnk?h9A+{HqU~19m3gm>Dg-Q9qb4H%9dAz-TXQ%t$Im%jK z&K7bJ(){OyF{8*t1$tp6Z|w}fupI;=bobu*(d)Wtm&g~;y^i(}J05O9+NCz=17zez zaQqnV1!h)}j>BbXSm%KKBL5N4K-h=ja1x+sN5<@tKcV!#Zj#^lBpL%>?of?d+(t+e z$kpD03On0sYF80MHwP+4XE|8>i9+lkH0_Z_2(z|6XPE4P1GubBiUs3am-%Z!EIc5O zU2=7BTE2QY3tR4iVLci|#;giD=Xzky*KojemX8b zU-8G8`Gk)Y8^CYy5pS`kKRa!FSNli|&3QRo2&t zG6U5U&g$?Zpfg!p&oCmlT(#6{L2nvRbMq$TJHdZD$o-{5dK?22;2uyDSn z$NKmTbGObgoq_nqvfVh+y__GNf%EPHi+Au2s|A@HNL#o&xCHE9ZgmHlSJxlAY*~06 zYCL>#gEkxc6t7o}89K+@;uPxU6gKjDrGVDG!T<+>~>;$~0FKMg2}a1XH3oq*t4% z*BBy|@o~^&^k4g60pm zEsO1*$j#xkdaGVSpkzj%ku3T$gGRQl857?LfyAv`cpQMAr+|$FM`zG-?-9XBE2DBL z$F5Im;#>yf-NjfVfc>a8gCHhy1Hxz5=6@`sdLVM0oeBeo0FTGx|Z!q(AJW1htPDpdq@v8oIZ{0 zO{w;8Ezq_~fD!iT;f2N2bjgJ!$mbz()DLz*rIj28iJH={v_&Lu1cn2p2ZOdTcQDog zi5zCdC}bnq>D>Fl=Gi8AbSBuWz8ov4a(WpgFP{jbN*7k8RKqN0kc?<61&0%Vp*Y1DrH53nl?XRa z8sGey>6cSGaT++}N*p<07!UhecS*IeSEh>h+XfStaK!RR!wc7i;@@_$ic8UjgFLAT zN<~D;{+-!u0DK{re1iKi&rr(SzaAS=^ww;C8M-sB6zl_5A7Zju>EitK_E<=2OO9G5 zyZ@&^77|be80yk{R7^W-E-U%tr4SRp%BGq2s#Ow?N(b-8YLTGd81uoAz=&==@BOf@ z0BsBEHMcyM1z%+d{k>bA62z&w?Zh+1oHld?I=N1rX0w+W9r-`+TwPooCuwI=5pB-_ z$m^4z?H<5f5K3BR!X5q&E1RCSvckedr{wIt+j!il-!Tu$ zN98|2&F!98x{H;t3#VBtE~>u35cgo(M_*7t@83c^tSZa$;tsU75svdwk9KCce9&P4 zUuA_g?I6y1?Chl=1%qXhP??u0V)Hzyb}3WD2%a9bMnp+M0~F*$yZy(IURk-*z8S(O zxO(sDvNIq>3TZ(WFV)}@=6MM8C`G5)c4k(fYVm>Z{EW*}xcZeC=ZGR)BQDU0@nLmS zp!A(_fgu$P_1($laaekc0~30!Q9OmlJ)tTCkhxn>W=jbv;Id8}i|b;FP^BEpirqJ6 zC~m`1ul)wtt_lO&vla_JAbh08a0zCK(pSWIQsp8 zAv?)RQFR`hfkDz4FZ@&`Xm?W-hfu0Hjc-LLV=A)7lxv`CxQmx7&uvM#7L`LoL88Nf zGX@ib(1068{xmW_6q^V3gFdGBjUVBG2E623^%>rhJi0jxmy}eg|$mVBqCylecQ1S>~KZ%)Nb$X^FE@wqLp3Yup>Rqe9m;o3#5VocOdXugt2!JLtE0hCi=~AK(Kfr{~=_3G*P4G`{Jn`EZ61w&) z?)@wcGax~rylB31#TBc_kAc$vPPKT-7ImU}O*!!$5$Od)9>D`7_*O{wG%#96u@PJ# zOzHX2<&t|1dr$(`Gd--`2-cse`lZ1JnCk`Vbn?FaN-TscYqG2Vqy|*@@j-+#urwGa zQ9%Eub9{8g3b%u=ezC>~UDp?gRO--VRxfsBNv#%hK((IzosBXq4~)<(LpEi=c$kjR z<$?>JnhhaRf^lyFpj#Ij^@#Dlqric!MS&&aRVfQ4h(JnQbY1VmSE5Mv>!{_PKB^;w z5d>t>14v5$6?zDB#ld9MoDg_G--qPNVBEtHtvl%*CkRUAoQ&J6GivS*8b0dBQqiaw zWlLhQm}i`+WU7$k%cyl#Q@C`1A&_yNcj-2JF<{sNa}_>GDu!X_Qdc}HOkMok99aPC zv1EOL_Q5`AzAMhH#)84m@%2n;KS2O)NL{C3u0NWqY33{=h;E`rzx%2=yam5)Az+tk z^T*I@!sHOyc@X)A@?5srW=yPr-WRC5@n!-)6DDsT(NX_1HhvYM*% zxeK&s)JYQN5ckV)&+0E6W(s$&NUqA&;X?qd<8j0~jJn{C28ZJGk)tYxR=V0*8Kh$T z*;tq>g6CD^J-!4Vd~zchmJ?Z`5NEnDPmuU!^$Y0U#WylZ3UrLU9WFB#=)Z53J+y(P z42JGOx*lg={aG>q3m7qLqsc<2iiRHTMZ{%RuwoA!wJl%lyIT{9(cq*T; zwztBV1X)`e45D?#sokK920f_@YaQuItc%asr&6`P-vw}-0$SzeYJ5~&CQ*|P$S$R4 zaZ~Q>#zsq16i)DjQ)U(n`tNl8pK&B|0t)#xsHy%V2H2K_XfqvPJ4T;ix)(6Vuwcdt zmZLTTrY=_S|3{I>}MRL1S-Y5h&;pYkZlz?UB^q1-xQYEJA(zjQ7c*VBdFs>28Rl!`-Y&t1ge|0qorO$xuuzp%NuoOJS?r z2a7%FV~LGZu`qWYN{y)|`YE{(Hp3R`OL!f0_5CE1+6%6cA+W@48;Nl}rD?AQP$C*n zcVjQDZojk_-W?1H7%UR&%Y4n6U2FeZJdI#_GB(jT0A)a$za=}0^xhz^s`A$_J_ngU za|s&%u+X*{(v(BlqXv`VSkGGkB^OS>(AR~dBDuu@7ItfgG{A0<2;rc zM2gnb(L(A<=25%R2X`xVzHF%y$PdukkCN?A1<(akV99@X>9fL@f$`FO5vOjllIruF z5CvTWr91Icskij$NAB1}HkmR}FA8L2pnZ2U)jbkzpp9n@Z!X%lfk|1n0?Q zC!g(?&Ih9pbff&;!|=E%Li`apcG^lb`G$=BVBy@}?v^?CkwUXK#{E4)7th;Fm!c3MCC%9kJk(?^Cvs-Jfuw489B zbmeT`AU|CMyRt7EV?ZDbJn+-9Uuz1dgqVC8;nj`Fu5sph@%|TX2vDpd;S%+2YeA|` zFIhgF_topHBoZ{Szcp^b>00P+Q&>jUHm@cHPwYd}6cGol^E};L@AJ%UTQ&DX0X%%?&rl9>m&Tiu}}=cn4A1UOvn z0uTGwfYaUz5PM^ssjZS2<9cyMv0}g>nS6__z9t@CqrgAMG(i$RYL$u*vxwA-bY7Z! zAlL?0;;LI}u=R%>%#?`7YJOdKnQ@7eY#=|#>Fuq(d6Hl2A9mNN}xS;RY zzo;~POQ~c{3D_y56i+^@sY?lV7Iug5tX=?4d&giFPNmgw^zJl>?6}+AW;Wz995Mpu z{i~wCKEWd!NUO0ccy(D`p=kdqqk7hJylk^+D{$nk?$HE4AAC2|Rs=do3rHUC!*$h> zWcrfAPEEfVNX)4bi*A!-T+cwB*BxqjC9eKj|C+oaIz?3IbUhD042GnhhVZ?Lqv*U` zgp5As>Ix;Y+)oX{sd`akXVoQ^(I8oA@eAbzi%QSsAq(~*y z4!ZS~GVpY(gK#CBCb5U8y8(k*O)*VIB|YQHWDD{E5B%j-$zWD%x!a2En!akLtyZ`M zz2~X4@y5r5dqcryj>JDhgx4L5X~YZ}#1a8@GlyZy^jlmb-MpbcZM}p3Xw{mA<#@IF zAUf`o08_h4?F~GNYcYq@XB&dce6u`~x~c@`G5@@PF!d=^D};I2_L}UF`u$hP0rB6r zW6N~A;?r&_#A!JvkEIOww*;=ebW5d}8CrAf<9mytWh!Oryog$2xChU%do?1Mqeikj z)$7D`8cxF&!b0rApJew#N`?^tig|pg>E9S89brx9o=c0Xp!UygI)D}IFQHh|Vl?q! zoLhf#8!99{&z*utJ@m^Avmx3vx11;_`bT*`;x)7PQQ#f%>8z876FssCjpIkf9y0}e zhxV=@Acufex9A7Mn+Y>sRnK)Rh_0D2g7!v5YVxzWYM4;6F{g(3AzEwlSRqA4gW$fGV^ z5;{6Q9SZ0xj6JrP6Ej9?cbk4&O*goIZg-ykk?r1=Tew)QOdb?aX)OtXf{^6Z1s{!& zDvvk2okHj%ftAdKz`7beo)@(49*7N(?2hOmxEf?`gRh1U4;OgIwN_igM}nGZjaWePDv$l?&u&U`h5Y>PsESlEV|O=s)x zAnCLzvR{>L2_azMBpM|R8uKCC#5Ca$sBVwl@db?!@)~N4wdvylB7OnFA@SM~bcT>? znW{36QLz8Ig82U8>aCX6JH=~rS(ZN>=^eJb{UtjrNP01l64TFy;VM#y)m|Ct1W4#S z*<6PliMbLa9(%gUrQS%Me`Blz!S3HH@MpuyNBsM`$j>MLn@JcVQOtjP@%)fCTF(a# zf7P`D9Su8*;(`H|t%;J%9$J3OuZXGLw?+C2N$Z8u#2}tVPvyx{RI8zrBO*&IeTg2n zc-kWPrM^_t@mUj2*y6Ah8Ga;~wOqr-EyN`Lbx&=v+(*H7N>y8}(fQNDHQ)}EX5_JQNpgMYMghGFbIAVbdk?qXS! zBIEe3_SVW!qmXx9kj;Gh+Zlb5cr8RIE^Eq~g3WslnGLu)6bM->w%UkK`#^?4O$oW$ zLKWvcy6Tq;NjO%4$p>M_R{o1$>~)jzv@LwoSLS{4Sp|9hw(Dc-gFUK41xYo4MD-V_BQjy!|n z1}jDF&#+PLC_HOI$rt#0j#j61QqlnYJ#jOe;6iiv-5w+JZ7W8C!{iTIm53}81;(N? zg_ZMm8{%_p9#%%%R~v9bne^G$*;ROQjn~A+M@tqWL1e-5hC7V23i3d5KIwI#*NQ4z zu78#Sh-AOhvo``Ebfc#k>pV5m1gig5Z{)#xwSc8x%uX4F$w4>0Yc>SnvFE@e-fnALm2hcfNN2H0-ErXP^a8ggo0vjaFtJgm%$n{R8&rH%Gfg;s-){QFl8b+4FA(Ko1q z-|n#L1@HZQ1Jm?pfu(gou?Yd!-o-;>(W)4!s zD|yjmCxlLr7!*pZSCYyqm}0$4bV-V>b&?*78kI^*6^?ChQBl^!Fblz(XCK7Bx*`NZ z8th8TM?;!F2>ljBPxvA?ljm6O`bO-J@)v+2Avu-)9tpZ&H!`sdBWV(YUpkBy=*~wQ z{Z(ruszOZk2#y9eSGj^sHR_Ksptz^)ZBfV53JC5I>FG+|cM4COeR1+pD|Iygg_8X@ z!Zx%SY$NFb$h6YxP1!R(AUz`3z>RfO_P)1Jt!cXyeYFSCV$`lt^-dvv`Ge~XqV3@T z0003&n=*Jq$&|o?e~=lO7y=C!t<->xZ>5aQ!*82~*^aQNj`P+H;l`8z(spp#W1j{v z@XeDPz6nHa@7>Z>!3NmZbt=%!iC6@c2Er9XOW=?K2KZ^`z<_YYxB;=Od6uch<5mo~ z-kCokv?`=B;e(U>2Q2$=SR5>Qm88AWj`#kiJ^tu(kVWy)t><$&r{g8jSbzP^0D#Z8 zs8bHq3TeD<8{+)BL#FqIq;zjkH@!V0Xt&o%mbUz0D2-+wbMRDgHB2gDmFn7ZQy%Q* z|3?!0lBGvTYXyuQUR%EFKxh?}ip=azZr*>4rZrLkjJh$xau9DCRNWY-SR8Z5&^kNg zAaVC`U;!K|yH6QGQ#L`p<^#+Gn(u>Tku&IwXcvtNd^pNtWg6eN(VK3*5 zbr}agp^$IJd3bF(LR(iCQp`A=9AQyAX7t>#+RZ8zc@5-s>o$c7`7z|D@SHUNR^l;R z@CN#x79IJj$N1kx*a}E*0dK_;AwRzVOta*n#0P_d&%l88l@tG?#w2^*PT!+HcPN$j z2olJ~c*#8vOr40;-z9CdsH;dG6NU1iEDZ%Lx`&jqQr(pW^9$D>m`hOWLJg8K)Uo_6 z{k3ChYv*{P4zZ6_aLKenVPSv~3l!e!`_9ZwVyY+VE}0F-wiOCH2L&&1WTxMbL1z}& z4-*A>2z1rj%HK~TWz)Ch*Ofv`U9o}*x3lx`5K-@h0r(Ax^!q4esAEf>tLw#bU7P_D zst~}^k{3M^vv<7$uHpj6x*m0(u#G(Oq>-EsNogP?Fk%7@nf@=9Xh)lDw~`8ZxH#-k zKr5Rd3LMcEs{=0Nt<81|eW_HNRBL|DS?WQ%tqG294+_kqbc{$`wSn}U*eGr4LE`WB z=()#>eedD`@i(qMvgvC!no8OaeiXcE;LJ-NIwI61uG1B2UxhGe)3h!REpFUVinYx! z;KAW>CY)vnKP(x(D>{0@7A}{-rzY2gTJkn{ap;xlhz1=grjN|I-rZf_7+O`WJ)XiW z`e+~#Q{P8KKVrH=@-qYXU(t5w)@SRbpGN$oovM)d2q%;C{mZNm%pP@~5x! zIemnG0DBpJag|V*n%$O8l*^Ua)^H)a3!26IZ}u?6(I99&R+HL`S1^M6hcgYI*PGP9 z&CGThBVj>W8C}XfJs4WgpwFV!xJ1@hP> zh*A18g&#&esu{s5d3|9sbygmVE!RFHdf}w-2s5L?S5%jYPG)Z#F@w{^e384N6Zhb6 zKb!ljdrA#O@M>xkf`=?o;NdVi+hGPn#QrR8$VI${v~RYz0)>Q7>sp>D$?P{ug8Q0} z>~*fPDc@THHP_V@1mJx(rmXmm^AO$*4=M=)R3u6;B}@M;U2digHYYgX{yejcNtf)o z@XQ5)scm8R1V~ZjyVIuQ6*4CP8U6faGpp7GI>CX;(d=Kzmb=dMpBxV4?+m8QVj-IM z1e%FZjkRnlP-4(Ia!zwp_Yq5e?IGMhmFi#iC#DEWnS1a9fd%q0y!KTn!bEusd0R9N zZtov09~i8>3V-thG3|gbitLP!OK}ZP>7=0}zU@6P5cUkChqeJ$Ijr@hvDs~$?`wy6 zC#FYIe1dSv0jXBVx4xH`3`Fb~ohUjuj!7@*0bX*_{+V5PT`-#*l?=3+W9i&xM(J+>uVFgyCu8U~z$`OgbqnqtYq9JWbXWU; z5sDFv1U_1en1t#LCyQ>MQrcz@fBIozL&obssgAyuAEgN(@}8i?^KQP8xFE5t==#Il z2C1{Nv7GnjClPtuN`!K14R`2@Pn1*!w1Awv(`i~`#Y&8(h=u+X=4k1L>jNELf2DoG z1qwT&-^=A|D5xr5k17v+P3*cC94r25mHj(jJ_{?{Tk>O99*4+h z6{S#W*_#92GgRi3Sb|EG{#05tTBootRk=(Az7eXT{_;}$NiGYqIK`*ynSy01gAo$q08yN2GN`ePyMTt6K%*&&lvkWF$9uov-|&Pcqz->j zluCl7iHM;I5}x<{vdw}r;%(#Pv*?Zl#8|Y9bjlMj)X`_95KUQ_)Kg71VSiDWk8x9A znamVnhAYAF{~$dxlL{R3=n115SDBd)p^Ac?VGQ^=jn3P*76^QEQdv(an)W8_$86{T zL;6eT%CQqyODLzpiR$XSCFWaOm_Mfm%XZ-bb=k_Xxz_*h9Bof?+Ur|yCMw88$9CJ1 z!c@#%PWPpk_aWi+@JZU3ung*w?NwGC4k_A{11d^Xx%1V_BC#FFJOcm9I~-sQ(E=8l zmV%1%#8!er6>GGceU4uZQ59E?d9*>Z{x`1y=M$T7G@q(RoMsE<6vjlIPv1zJK>uC)I*6!0_ymAlG?|mAawI~ZBhd&_r%Ptuj}v{ z8LZuQhsTe>V~)bSEWnmnwAjD=yg^!MKFv&?1Wz*OT5$~fJ5?P2|NRZC#0SmAMZjjE z5C+Ms(|F!^Z~fYP|BBbwzqZ10ob;oHMmGZf@xqIpm*m|vc*35lz$!RjP@|?|0}R?q zIu5vOj?W-FbA>O3zOsMsxhJi_@CbP1Xbk~?`SDXR1p0(zUZ<|owH3js05(;y(W(;zxg_teFGa{wC|?T#=qi+B32?#RXiCe$dxRAR z#yuaxpg8p5==v*^Y^$c3!=NX{WARcM6G%Y~k*6iqvPao<+U@Z=id7Y)xu;(lofY;D z7v_+z%#wv+y_iTO5>OV#ZOSig(Z{nJC~edSwsqeEr$dkzWG$uqF**6 zu0H;L!6NtmI?X*d(D==EN5Rnwwbr|)FZ(m=)bxg&0)bpG3aI?)qg9|CkZbUetJXbCr^KB@H?hq^_piLC4! zGtz!M`1nf);{xB!?-xQ&Jf9kQ=f0IW*rq{lsq?bO0)Y6)?~OVs9pn7<*ln`lw+A1{ zl+U~Rh@1Om|D_PmZkb<;LtTqa7pgAh2ET`p-1Fwbmm6jHKB7tTkA-}?%kKwuQ+8icyb7vy{2Uth*Yjc}+sI#4z;2{wubea* zyeIe?0NbYoDaQ^dwWF*)J5zeDrRg>!W9Ek(WttqvuLKwxgKCu0$bY*$t{N3b04Mp< z#AYVEES+RMx{pOnJtv}8hO_>LzgD7>RJ?l3=E_`1y~l^X0j{`c{jfv1b^-}^$3FA? zP&n&-rt_k&gS9vzN`#s7b+uUATsoT3&6rNcbUi(o06{;t=Ed@-miQFgV}vww@!_Xr zP3J^5D#?R*_tszLTDmC|YB)v2aqRBg*8bR6oJu2jq|cgxqSM076~Le+AXY zPhoJrlAV&Kyicu(SaTD!Wu54F4NrUsr;XGO-XDoV;&H8g$k9NWH^%e6>xXEK(ZC*) z`IrL(vTro0oe=Rj|rP=aK zi9_yJq!h4?>t>hj?-2(_;9Z%ZNZtQL{7jRbn|F4!4PO_$UJwE0@$0T5{?PUJPu`*D z-CVFvw6Ua;6B|`w$t?(=HN^ zJenX$bz6qBPD3^`N%$#`NLW*dRUl1Fu@iP`oTWHfr0lf^Ej(FaZyCmrJWz#P^jR9s zwxyiVKKrulH&e&PCcK>Kr2O<2$*{b2jhQp`kgQT7bGkua^zb7U&o1`4djUe9P*F*( zjx%z^R8R9gKkZ`cGITykX5NTCFLbQ==TiPaI|gL*ch_I2!ps4T;iE@Y1Jza$kxpiP z8{3u`Bz*qEsp_O$sO`L0e%~o%FU@uMv(c-W=I5$kWSxEV34H!nheu>Iv`#o zj)dovW>^sm;wNJFJX~Tq8+Wf7N~4{*Z#t;ca5=if2qkiCk`ZKQA0Jm|H8IH%N+ZSZ z#xJw28_f@{Xn8c~Svtl1*$r~m5s;ks7KSyI5q1T!+DDUyjal0tCsw?+-mh}3^4zHB zG74{haeI=V{$<2NG0HJ0Vw#@nTyj=haS}*Cd|EdT>@zj71e8lakA7?#ocX<2y4(Mv z&-)j>?$--=t}ZYAP>Ca6V8X1JS@rm&BT58$R)f`84b@}UzL)sqVR3qIl$-CaCSEv7L=%vmfC64MrO49dL^XYqB7o#}yFbKqb5zkS38zB5r>OwBl+ zE#d<>r~Ini6dj>HHwxCS$i?$hiJ7lGa(wMoL^mz6CVrW$=q1(OmTN%}KK~9&T^2KC3sF8N=zMG;$Ua6$ZwmdV|q+cCMAP zY~5gEu$ZUtY>PHXgH7yM=6o%?vkE98CZ!S8csT*+wTFfy`ux-+8dM5M7a*(Uyk&=p zdc=~RQ-M@n%f_qCAh>=em}y@RMB%VO67n$}@l&(G8$dxhLi^SHh*K$BNK|59s>tGW z+%-}0QJ5v#ndlWEfXFV`^JE<|q}u^h>Y5cq=ifNX$Js+dc#5vrR7s}brcF0ir?!F+;#96%=`yWeIJA&>(=^y$OgU!n;K)W(r7R3^dz2SRZgfC{1N{GE zjVbw(Xr?h4 z@uViAlj9-ot`S_o9QaMmw9S%_6wf~LaMB|1dkz(WlA||eOva(gKU@oz>mut~Qt}if zb?DwoR^ixWO$+i0Za%jIO;I3R{=_~ok1B;!dUTO6{Gbrsja0f4w@xJV@aI6ozA6I-o25%ln4YqT4HC;!R|+$h&=^CTJSWNiIpT7{!hRV-^7Au2(I?D zdGSJmld<9nZDfw<^S0u0uVIWCre1BMHLSxm$N)hk65@}+5mT-lifihdUxh-BA!EW@ z?90QIPvgG!9feik2+`sYWUca>|4vkt;e0)TIf&lh%rDbw#3jvpw2oB_qS`POJS|&4 z8Gf&W>`iX+$MDpne9qu`A;W=Ki~=^u_^P{=!HWrfqg&v}p{*7@<2VXF(DOj-tGr~w zlaq6LbJl=6w!*G~l^l&Dr9z&NDxp2O0NjLCO)gKB`4hr6=JR;ay()GqlYi zpuE%kG{y<9V4tmig9AGfPLl_e&dSE-OFtw`Kldaa@UHz^+$Yzb7ti&dhPf+zM;5)K za9`G)R%N!*oc?A+_J2^C(hr??hF>4!H1ivLjN&vH0A)cit7ILtNyd>JQw$J16+e>oYx zcC;diR?pnMy~|jYvgi^)bIm04g16(zMt$ZLOturd?&AqE9Nv-wa+*?WV&?;*T9_#! z5s#wxcXqTC_!Qlmf9lI4L&A-q0Zj-rStbto&zZ>-?3^OVW?{`_fH1z>MDrSdz*Zkn zm@KGBL1(9~vsHG0A>Nasy@)UGZtZTr&h=WXYxy@JA?FA>$4}Gg1LEU5gb$L4|2bGx z7?wJ~bD;(x8Cir)5IqNpnUw&P6fsZh!}t;j97$>#$0pPgy7R7Fs&(5ZgHw$xmddsn z|5dMpJJ~>p@<=b*H*+*fuC{EP>SV&IfblqZGHK_+?0F_H0Oj8Q1%KcKA#~tfk3L;S zThJ2%u8c%_gXCWAZ+9HZ;(P|#QrNa==ew%84l5Wsn z5{XfC-I94qCJ>Yl+fY+?7{kiVWVVq9cLg(08jQf6Zm-3S05~308qfc}h9CBUuH(GS zLyWP&+%66dO7T>Q1e<&sw>Xcin~O7ENrJY5<(jQ7BWszEzVhUAD(|Za*z$}9197J_ z6haXG?&`8qxxx;K^41+-3mM_j)aH3@pgtDk#|z4R@EAF%Hp3W#T9S zxg7)Arv00??SJU;2&rQX>@{^Ut)Q@7UXe?w@*;OSE!;p6=z3{Y7f$uf5Q=^0ti5x~ zuuLB-js$&0AvI29&DbnLw|}_Nc_07afNT!Q#B{i2C-A`_!!$!kwWcNOU01}53?FM3CQ(AU zJm0g>%ATl1Utj5@Hz)gXYIpZ5rS|TWv_;hdFhMI^QMymya~1?KX$3HuqG*{QQ44CH zBN^q|U}G){0khuvD9P8emL45_@xvc2uLwg8NV{g&6u>(`->3+jLP|*5YKHX;b^?DE zO-}SmVRRp2qO>D;XtM}E8>D27I!6P+uZx!Hp)6KzVzQWy$eKatq_n0Im)TK0;{8`@ zNTA{=jr+AGW>!i12Y?oeKx@`v^V!S`bD7Sf(7EE2g}#h(*G%7MeiJvAE%*86vSrSz zwK4U;&C+U8o;EQwI65lacApPyk2=_MkuLLl%1VnkgSFS=hGk~Uv-Nbfs~zaRW2jbK z1yt(Y_%v4E5ju(W6UFPZR=EnqbhkC;wK#{{Y{5Zh9dS7;XX+VqPS*IU*rLxwL}WPj8!Dm?>u6M<7Gt2v$zj(Y;b5x%Ldpwz30!W#`A(LAP?ntw2DKbrgI$F9_v}y ztiJWbBG>Eoox=*XZ;vpHc?zD{_uiJYh^BmW`GP}T#-x+bF=9j}02e+kHk;|+n~WbX zv-u}PJx*#(p$r;zKpQNa`6iRV)8?isTb_kw5YlM6*XsAdm+$oj9oo0bx^{8RytvMX z^met8qrs1ZrK5$kM1#w_<;59@J!?!0#h+Z9hlX8 ztZIZUGWj+|5%000xVhd9FS$$Zod}BciJ}U=i8dVG_C!A%j#Wv=228n>h2Zp_NsC@h1a}#;lz^*zBD@ zoVbJ{44KAMf~z2k_aqh)75xVw4+^zscgQ%@n>K-Lob_epkJ^}k|7x6#6Etlj#w$X3 zDt3;;O~U?!6MxJ{sjdS*%$}4QDEj|5T@RL{SODQ0;e|Rn&|RYF_sU^4Ffa4?5t}4H z`t&2r`_*@?_6jH*K{F4ftprh~-7WZTtL1w6oi0MGVIsSjdg!?D@7^t$-6=<#Jg-LP znVXUB6x(1ZBv;pS2I;1K{%MRvv>7scXU-RHtPf5+RiBT&h0Ce8_2;OFf-cy{=d?z{ zBXgP=V&?}y%n>Zk_`nHNct?Q+wy1(giHwbiglsAvTIxoih#3Ds%CcRn=*ayOHbkE& z2%|X+4Hu)9+kt$88k!U;kI@<3>u^T+cyo)R+N(QtvM;{*!GZO z0q|UVY^I{(hE#8)2351WVTFVJ{dFc_(O(-1*XA%HrE6tec+3SAfHd+zn$KlAQ82!` zY}=uGfHNYD?+%)!jsqI$uiQZA32&$Jj6$?VX=20$>#xDj$Jn3WaowW)dC71zIl{(n zXj5Q;n+v8gEeUCte*vvop$CZ&Uzltzd~y?JC{P^zsB@4b-MX@;X@0HeA{S&-dp-$_ zjpTr^KcPZPQ>D8Bj3eyv<655k>PHuf4c;>DS%Oa7=t?;r_T9p9%uo_{1i`zS0PM5& zpygA9U|~)ZP`{DeG4V93Bs)xB{vt)Z$7%c7=#W`NF)11IMQ%s1Z=6AGNunRQ#VHn5 zsB4Zg81e>32WwH?!A)h|-a-;03C5>)2GGJ6)Fbg*)Y#bQ_Kxg4)&f*tP0;IP*uU}P z_oY@dde02U{xhvwytEw{KU;3gd@Qq>I>h{rVe}B2k>!Q@^eyLXSL(LiMgDmteHL*4 zVb-tPFYlCV$l+t_Z*v{&nJv-r1v~}kV|k>z;J(~-p-w%(k8;7OYQnZVO~Q7Fcyu#p zVNjFAQqe-ISnP@NbO`QKf2P(mm}b3luE-44oV%w#cf;hb8!ldwDH!%gPE_)Bm??=D z4>`Wkhnnm(@pe-S*c8$o$B_ERAR5O7U`2icV*LaY0v0YNxE%F6xgXH!1sklWS$I9U zj@oMD^W`=Ekq~uj+n%UbI&&TttRBN|pDr7t%o}c?pk=;Y^m34Ez4q$$0~{(4N%8AX2Ac#PT^`Z2@1Thx5;oD!4PJXTB;ois=^nQ&5)e$)8NYXL5-7 z8+yC*kHC9C6oT#)|DSgp{4p(yX0^dz(bVDWO0P(ER9+WF<$rd3`R~?BxCzQ6#&wH5+vQzuvKZm2UGG3$Iw{X_GwuD9rO)xpd=_b zTXa)NSIr3oNIidp)WahQvzRsOh{iDa2POMz1Ogp5mkHUz5A!Mzv_1sWZFdaxrC}fw z$cl(NbyCg!$z|Y}htUmIQ0%f`S3c67ezlttzcI&XcJlC_8Q2~7upWb5s`+~fWdPks=88WSvlVy@DH>RcHZegh*C6uxm295$#X_r#J3Vyh7gguNS~ffQ{WTaFc@^$XdoNgm@V};)F)H$b(+Al< zJL$U)IAb8YxNK^EccHlH*t&v}@^-^B8I_z2z7VG-^aBlriCiyEm_0dv6>}CVi z{z1ANjN#h~bLZYz_C~6&QFNPEyw`{cF6DYj?=jzbX61crt_eDq*7mE7Jmz{+;u)Wr z6j6MLVZIWl)Z!lkr9Sa~0Y_A2Q``~=^8J^{Wr^-W3xKGDw-q;inI-DaWsC;#vM^z! zxT;OzO3zuX@D7gF@!DD{ojt*w>37~;IU++_<^36xvrf|#JXCgK%lv2-#s)9tX{`(B z#g3)pv>ztM1H8}exeW#h0aTYRLts$vdsG% z7>UN;3ieemT3yKsz+Om++sbmNO%H>BX+rr)$enw)`{OhqT+GO@^LM_ z@$`*cU+^1+g8{{sYJ7oDg*%UhNt(e=P`g(K_}>-uLg2?V@5vTg=sb|1#|mDC3(6zx z75!D=Tz;30pPEZE=4_{Rnh-?}8zq&=hy~lbUP(_GPQIj}c&s?h?vMAMV~(KH;N5-H zIn6$E6!uR(Rzxj7-J2i%FG zw1NF76>C49e&WXBh!9cMoSCGAD!x#Mlq=6eX7y*5pw~QYOykfWdMTBQ`6A!xRL;9$1<04*x#wTe5QKLb6AKM1g)KqNT&7+J4Q9 z`x$|nB_auvJ;k)gS*0P074!PICuY>eNSp^P93upEe`ARO4GnZs>g(q;Yz_umgE$!{ zi!TR8vsE2}bmq^rXX=jyGVKnoin1DOzB@&(<8!%!QL4bhT}9{PSuozt}<&hK2x4Ee{cYQg_hAgZ<&)SbA9#D0keu0cYL@0Hqoj<_w-i|U>Ck^vH>JqR zi75BKR)~e!t(TB)3b~fLIw|8B-2uZ4J}r{C31u^22#!TNz`2kq?x!z2?Jbffi;|$w zU<$BhRNjgoq-qR>s$Se)AqJrjedv`il0@9*1MB-2`Hb#r^pf+XDVdHhj}7$ssa5mr z{!ah74{@1IX!eH}rM39S*Lw+F_m`~^Y+hZW>k52)va!MimTWY`IPKWVwfJq$lwXS) z>Xc=lw)k(uU5(;Dd7qkZ{g-qSz`s`WvQnQHoaZ`#2|ERRC~DcLGlMbJ8u$ddf7)_X zvn+4S_l`dvve0=BAE4}SPY-{46SC(EW5e+g_@;yyY`tWxShL54M3$^>Xw;S}zYN&2 zry+Y|)~0TV71XbTd`QQG*ri6j8QjXkEJCBv;&Iy@8t+@^^|?-g%pI19Naj>WES0)U ziPtu52suH89#Q2Qr?#n0i4aH(Vyz&PFVc%gk8N@>C^=~ge#*_JesPmELpR-(ulgz2s3r5NmGyZj+!NO zfTA(@ds6&S%A8VeKf9PHxFbHTR8#rdnhmNExp9Ssrc=;`A;sBB7F~Ss_QQ0f_R!7V zqTo}ya%HwS3i4-~V*qqEU+cZL3nb63YKz8r`mYP9T2rhGrjGOY%lqmNT>@CqDFQ3u z8idpkK~QOUasJ%1aA|WJHdNPK06|Mo6PHBJ`H9(=OwJqGBL&kBftVJICKr{v<;EW6 zq3h-r;ki!sBmarxt>!?<;NVUX+M=R-c9!Qi)|{%09!X;7t_F?=wPTNVZQN?D+1Nz# z_Utk4zAxi;xJhk~b>6NW;tpQq0M!)@Q@REr5)gW>V)BvpT>M*=`WNz7@+M`@(`zM? zz|i0uV999211wA{;W*UD&sm42<*MirGnYMbrcd?3Xx|o17B4nk5?{u6h0z1La|n## zHQOJb!+m7c+tjH0HiygEN`Sg3C%b7cN|!hGA~h?WjU*y&W~Lev0x>Z6AlS1Rr<2709K3~`OIz4gIwI^f(x{6 zK5TwI?>{yU6*Lx}U73B=ct8wQbc|Vh+wYi$LNT5y!ytFf7OElMG=;%e;1J$`{91Qi z_7(SLD175y^@S@8fZ5! z*0NhS1Ql!2^1p-sLe1RGN^N&DF}yr%$zO-d)%@py1DC%I3|H=1g9Obk&EDjFDE@SU zfAqJ=`x)(vKd6;N_2K*AWy13to2k=59J|x@L+-yys)}uF0}hA13(Q-8U}7|~B;I(T zpM>zp?)dfIA+i*r@Tf(5qb1my&>;Zuq#&X2hcl(-hG>knYK*yIefja=i8=}#|AXA; zywq8;ow$QsPAGR~KMxZ^#B402Kt>5wspgF0!4CpGG2F(JDCaQZNA?lBOX86G&Ve^5j`a|#YrT;Oda zfLlcLj5lI;4e+euvtzE9Da3<(@i)QoR?FA3b zL$O1-qI=Go+I{4dQnfXjhHMyv$qY-8f41!yKtf=?_eKufAKgvz)h#Z}ato>)zS7f) zvRkb^^B$TXlhtOp)-=TE1K0KiN@!h-5tyT_@F;CTH zPyP6B5#?fnR3|0;+3Yu23+zDIGKoCs2QDh%PslecA)ELvn2yT72Y4w zo?x^!8Pd5Sx?h>j;6_oEZ9ETu;|?)7XsLxqD^-TL@t3$)pJvq*J*p{+6rAE!%?&kd zU-Gx6L2hV9_U}jH)nI2UOQRH#+c=hIx%zLtxC$~ihpxeu%^nSU?I@E+Iu6vJ$CVPL zZzh1UvIt4y)xGXNi)OAVYyDB)r@>^YY_nQ9jL9>pNRGg|()?q`0!Ra&b*b)UG2}9M8~0Ih~>2CG>BIH}GiJ zQ(pxW!@7SBvQ4?WyJKTIyU>=-1pM?=wZ~%j9>B8Jc9-pv=~K(Z(^RtkqHuI~&xmvL z?DmhCvUaXGTHgMf#V&b~u#2$n|8Q@qu_GDN2^qJ{vGG@8%&_W-Oe}^5faf!ZzwwSw z1#4!}#~m{!7?dxc?=A?|pt-z^Ceo26D&jpwc(3%2xBXq8<&DeheMo(0{7qEa8AiA?EyspC;zRd92#G zy#9XS%-aM?V;}c2{G24s-st|3Z^bwssd#ml0U4qsJN1JRKwX2!4W|ZrLd$R`3Z37pc5sN1 zdC{Hm?r$1l>#hyN*S2ieB1$GFX>f9h_LKjn=$8kvs7DG&0rDYf#YQmbRI)uzB_IKb z_ebA(-Emn3Y9`@+w^^h+>X1mPGh`;VxgjX4)n@r`PF=-RWIuL1D&RWxbK$Z%-B#`Zfc0}9iC zuaM%=G9);0PojroslY1)tdp=G9#SLw2}OUXL3$x#mT*sbgK1mBSOrqZ{qSpoMy__@ zpz_bU9kz4uv_of+mTHHuwhQ4A7B}t}F`__j;k2XBnDD2VRZO_<7O)FXKq;bx z5jZxyx-20{2sVF6O?O}Rvj+)oGTT)2nB)VknB`mTjzzrqhhXQ$6Ggow1b*?3b%0Y0 zlb!`fzjGv;{yw_Ma4F{|*K`HTvmth9Xg}@fG?E)d#P;nCIC;t_*i>Ks;At1hBHHIJ z)zq?~d%m31UILVG;qUy!EeZ2~<>wK#OiUIoQ(z$g|LlF`t@tpU36;mEl%(*6LNL%S z6)!7Um&T|>hbU=oz&^5qNxjO5d{%x^Z0z&;U3EjqSDyJSka)jj0a>kC`W5Z|Gi*ih z%BbOF`Gu{zpr!hA#Q#>ZpuZ_&U8!)w|;egH2D`5bx@7N=@2 zV|{fHy#GKpy-|k~yP9#zOerb}%i>18`Y%!-CE?^ko=uh6nfc-R5;0#|-&i-SYFsp* zCd#JjMblx*CZ5g5{jvPU*$fp__OyD=XFYO#Pk@o_RlLyeo7rB}gUp8}Lk_z9qk6o^ zz-JbMX*%M0`GV$Ey7JP*J9*Rg8&zcS`%5!~8O1~rrM(Vd@EO>4a@s!bg&MLRR~WIW zFJ%K&^dxD-OM)jY35|Rr3iq&QwrIS{sBL)MnLzrB-T0Jyb*}MN(#-9(2Co&S%ISIB z8}%D@;9Wv8MJa_I2m^gs7uRkHEbswF_PNG4e+ z!E!1i``im4v*zPR$gW1@*~E(j?mq5j!2OfjI$)$sKY(uu#gkD-_7=xJIK1}%(Qc8&nr$wTUnT@UkirRi)r`h|M7WX$On?1c8% z$&5D#KW_-8kiseXzw55a-ZEVks>qaL6kI`L@_4fi-A3zruTCqKfz8zc$#JS3uBnrP zqNNh->K#boF1DsVziBw)iKPSuKQ}%Nj!b^H{iJ^5(7s-=fwKt1j%=>+In=9{c#SI* zb&W=z5I|z(_>TJnMvDB8^-94Rg`+v>I+&SCP;kX)P+37aiV-RWy&~4n{y-GL0}^J} zO?H_BGdtgn99;TXZf-*B)v5PgxHw(9yGS`?nmKV z2SPr3>GW?<^Et>Cfq{dAuotAdi`p;P@BW&{uy5VYfU4|M^i2A3 zH@lG+0Ct>YXiApm`n{*EBbICmgT)aRvGl9~@;}}*Rl}~&>uj-2>7l2Z+k^uf!jXdH ziP4X+={A;!8)ypE$*^z_Y_@38oc^@fpvmiP4Z$(%0 zo>qmTL<3>AEldQzR&y6WeFvFGS_?|bdhtW&3wEz0orFm1tOo@14vERbq!v07!kxwc~k!@s^9U-OQE2^C`;gKI(9_r%HD<%Y2AG zmB);E!$)!leBQgf3TV!0U={y<>bK$3iMFJew#4JnNwl7XVog;dcBJ~)MT#3*uB7{L zHqZ$r2-P_bWd1Br<<}}mYO+2}aQxzL2 za#gF_fYyWUG)0hUoi(piKWTJ3P^JS`;DDVo;Kk;O${n@;SAzZf|AMzPd^Vf++s>9< zaT081N|kc*${JF3XTuwD68!^3M0%ZK2fm4kW)|D9)o>#QM*zpmK0DDk9!uDq3895H zf_^`Q+Ezi6?QA@O@6wqpc$SZ>Nws8}RM`S|6YYsP^%>hH&`Of@{X}e8;vI+<-D*jD z>-q}`8cz(6SD^ROs-&!MMGgv`-{gg6jJ zijDYit!MHC-%ee_uk*T~4Kcy7Ee5UFKh}0U*Nay(0;lnn(-EkZS@Jxwu(=WXu>j+l zA!xXg0uIV^#@8v%e~Qco#AP8+|2Z{U%tc7p?NwQ_VC2Ofp`QB97as^M-E=` zXVS&aq5+QUoUbFFJRf~ru_qh7%Ew-xK2F{(3FRodTTJ(QiHrD$8_3#-e?X33I8Ma5YbBFgP4DPTMV zESqr`YL(up;F;DgS1F*_z1MPJ839gW{4k^Y?`!RN=I`~)D~xu1EyrE#4%IS^AhKKGn)PiqN6c5>0OGs5^ zB}oL%(sE26YjI|Uz@=rocm&yyJJ(cCRO^#N#j7n!6?JA!b7j0xqyso~YzuY#=2YFy zw+10+DfvMMM@V#Qq@v%-W#|^~7CCr%p0v$?l=?3 z^x!ZfQtmJ^%%XuQ-nsJzf$M|yElTjjT3;SDp!->uih~HWR>6BO_12Xd+P>?$LU*e3$?nwE?(Rhqc~ZRT@~@}{(DAV;{iDPunEu#Xr@aU`v3zwd`%Tu-Z_2k&3lu( z8I*31-#J3bdjl=OCZHLCuK|i=5%(VXQOuHF0MBa>1a(f_ZDk2ykSuBf>4t>so<=q+ z+p8$-=3MEiDs(FK^3FR(IF)Ob`?~KLfBg+$cT}M2o=QNg>!iiB%4*p5BKOsz*$^S% zeXG4!>euK$7!r6Rt3a1=j90&|9#|;gNX_?x=QN}XaWX_@(ZfOMv*#WLYjf?o$-h21 z$XLFO(x|=Z%Ny^UQj_~ykAxEx$1ZW@KjfQIke6rvr%* zD(U-Gp8shv4P(xX-++*Egnxz6$aDViUD?>gj! zh6iP4--Mg=dvW5a|2VZuOvaztwDZfn8WdfjJ`~xOX5jmtYODOulkPeRkn*You0%@n zBl&o`wV6TO?A;dX41va{%%3k!yQ@EYPnJ2)p;jw(x^ltc*XsNEpX5njuSMGz8&hGo z$qnCto%=+`$wUmaPlm(n>)cj{vIWyi0^SF@Z!;c-8t!ulnXjU;RO}rc(?q~ z@v9>rTJ0RYbau2nWziCMKP^b-2K@v$e;7s3$hF;MehAYc(R{jrRhxH}l%-){*Nw54 z97o&Y`=jcz4DBLR%m9$JZ&ThaaxrDrT%QCzYc3kbR&#!NR@d!M*}`apnS@Q+F<1RY z+Kv6mByJ;m4}MusvF+PH>`$&>Sq9$%_o+*nsK{?!p@ssRE3rU9&5J&grg8ctbtm;= zFNOko!jcR87U|J-DwmlVcV1~WZ|>={Xs$fQPv#;IfV>N~gnafO*|?5g{c6?VBFp4y zOW6(+tmD@>n_sZC4$r676Cj(J)IfKxpTAT`Cs5Mj%;Si<{VwGhN%@&1b@+2q*N|?O zrZKLJm+aaZ?n%nhU;{YN+%SA)iMMj2orKGh!i91gXlt=Oqph0yE2nNl7YROX^-*@m zQ8g&YEkOu8_`Uax?DcQQPr#gmEUD@6e z;8T-9v;f^W%W6JlM~k6qZ0(O=*ZYU1E5O<5-Ln(4_E#og;Y!?h+^jygC<3}*T6~0B zS@vu@iQ9q8ACDiO6_CH*XpemvoD+MSQ4i;Bj&Pdt)CBo&AP5n(qTFy-_I!i3p)#X( z{j7MV$_#E7Yw&4OZ_Rh$S;5jHmSGTcx-T;w?Kyf63Q%^Rk=PW4`|brG?xFU5oFNM; z36b?>uyWQQXNJxwz5&R9H%z!P{!H*)P0xEWoe%b3+tXW*emrseMmaCbNJB&4%}8T_ zW(VQZM;=*L-Z}@zp(EA$)KElvOI_D10K4xFh<_hqO5HQ1kjH(7J>ZA>)_)Bs=dcZi zzU_lvX@a2SJ2sEVf!RIxq=jokyPN;0ri4y7nog&yQ8Bv zBlrLjcQ?9)mXs5@dlQrX&-zyP)(Sao(r;gyrRqwIu5owT{ZI{vSrnBLwWtNTj8K3=F6OjK`re(2n;^Kea{E}QYADYUBteZA=^O{v?<%UC`8}K=I-411Pc$#^Y!$G@`D|)3t>hU}U|KRX|AyklMXE zPi;445~@Q*&j~*zZsI^`E0w+~#yAc;)&kyy;>FBAXuZWDeIB#jDk_YDCW|PRLuPlm zi6UK9PV8onOHiC{xzz(<@T3 zE-WJwQysf^)j%V3@4}9QD~yj!Bqki(zrdj%=-Ui9Us;#psIKmFG|~KykMn&<`_Nz7 zxl-4iuDIZ~T6OUa?8ZayrshS$%)ltqPX6IG2(4g!59C|qiM`1&?EEHOvW0KreF?DR z>2H`krC}qO0$~(~u1p(}M&Oolvp8_Hnl?!`L_CtA^KLzlx$<6#|K3>i=BUi>qSK{= z-LbB>^2$FM|396SFvy#fNBB-J59otyz;gUc;>E3w$1`PVYph|F^8a_C1BY7HjpU@> z<<_xJ!m6@Gcwq;%Ekspk_>L$Cycj=K5r??hFg&_k^kt7XJ}$T8N~72bwx~)1Ps!+xrcWW&Rx+kn-w+xEMME^NVgyby5mt~?bhqJb zyCa>3YJz+71tvDFD0>BBBX`O&gg5L*=k?iplsaQeXqDqeDseonKr40I_=jUxntm(5 zUk&Glf4;~FAJndcr4<~%Q^4BDp+Iv@uz_~+k?+mCMGyrxXy_mF&g?zHZ|;EOcZ3A0)a2X@W@;{hv})l z>*x8~Dr!`1;DY_+0l9N0na2dinEq8(61M)!Q|uLz63~U*G5eR)C7wRYx%)(ALU+8K z5;+qTDJucu$lS=E253xYjjfsIzkv=Uy;0tyeuuZfKJ>@{vNS$-k$8nF-mvzrFI(aQ zBFQb5CY%x5Ak!8}{~3FGr<@^(X8yn9tW^HLrvIh$M_K|L)1bw3DKvea&6$@8|F&tb z;0Q4G2r0MXb(jb`E+Z?b8a7{$3I?=jJ0K;NxuEe*XmWI0CMt7E0x!^ag+oYi*P}fo zW4Em=wU*c!zK4<&y8ISpum9xROCWDvY{V|mSNBBXm~5e(CdqA560?10^TwGAJ%Yii z1`$N)#Gy8MY5LIA;kU4=#o}Hl^;PkpQ!cOIeS}+&wuXL22YDoUeY){u^8YP`u0CU~(TCG4O51peK>M*Ail2H71eLs*!5>EZk6s$)iRt99PXt!c(#Dy112*`jb zcDK({4*O-rfXEq_J6IOiJ+!7>(g^v;OaaDI{+W&6Y;zR}ym#xzK-|+l%{={;vS69% zMERP#z|Rj{$Zd8>rtG(R(PJ8g`_sNe1#<|M(@k5hVVB+{ri4Ddo%O7x^U^bCz}|yJ z=yTymn8Ky@IcVw&lmzH)?ciG8#4a@%!n1EvS}_N`>v2cUg`viTEc?SM&UsT~sb;d! ztLTGxyrMVhkfE3LdX{mA;1Nz`A3E+3B>$+6-+EB2j*Of#0{AQ<;P@jMS&DdX#qj`o zFYq+Myq-;rRRI`FIe&yaWW5;7R_2c(3%k_@tR@;SAh`?vwloR8d#8KE00He`ddQ?ndZSlic*Ph_uL^tNYot}rQD%_IAzzL~rvIg(Eq7AS>*C)EV4twE zdl)6!Fk+;=|{jhfWKlG?-!3%t~VvgUJ(b!h$WXSMG9PrY# z{->{-xP?FgR{5XKa#J;H7Ps0`^_;oFV-U_|=mC7E08hHttwRiB>Os<#Pomd?V`bCb1Q z75DIuv`foz7(G?xqzl(Qzr_JoZwqpl0oiTQNiR7i;H=3EVlUiyB0xOladRC+>DvZR zCIJG-P>Cu=EQ1D?uFL@xM}vsFfYMSB~#1JHI`&YkntAWag$!Hh(2c$K|>N}cT% z0C+Do%*R_6D(m)J-w*|ByYNs|)Ozxlv(gYOeG+$pC+BrFZXxEcRaU zJ%3_JLWhdTDi7bMugB8UE>fIdSVBMYtBO}x#&f_@N z7XwVj4<}vH=9bh)a~x#4(R!P4W%w3pEOiSW)l{fvdd|WAM9AupN90Cx^KQ8K0v!m+ z!0dn4q#qEn&acVqFhDYXPsn%ldVAl=%^_bP*22Y|*$AcIvTJ2IW6UPT^K-v&pYw<# z4Y2Q~r@VpX0&EH8ny(}W9GV>e*ijEoRj@$8po90Aux>4h4wB!S0mQTGRtnjJRN=Yi zo6#s=5=j!wfPZ4&3ICN*)d1r*YQ{9Um8!A|%x1j8?Hb$fdrL$bF#VoRO=QT^rY25VPZAT_? zI(ir1s;3RwK_j>*?CMi$E{bBEiD{^c9H*m2xIR+oJA9&?-zuei0*2{YbIt=M@QvTb z&(s9XLa))l&D{Z-2eN`P(GuO8Pnx{(573-oSsnhg@nYak#FMP>Um3p5EnADH)*kED zLyUz#HSk{*(dF8f-KyuxPQSj#&XX;?G}Y?|?qU?%Ac_OGyvrfP%hcHVoH-X@IjY4| zp}tfmZ_GLnaHf#+0d+6~Y(?<46=+`u}=4TgR;ufO%FKvM>rT%TC>qjJ15+YYxtcV(f4Q`H|*6x!A z1(>}}Y4+MI=r(r4AW&|??tJ#C&eWsy{^pQ2Er4B`Rp{(w(fMOVa(&$RIWltxF{$^b z+{mRxIlRDvjhd3v!Xj%SbN^Ra=J152%|P7c=-SWW*)A;ppG2hMIw!cWmDtaK|EQGKAu%#`3W+?*n zdXSIV;2Aw|9|pNnI$2nzmIIwlL!oUgL~OSwL#dXMVwmNDB>nnebHIdZ=9TmHx&Lj9 zPC3;^<=H2?i~%7t(H0F~2dCOX-rdfdqw+_a*UbYU9qN(0Yk%G_t=1ggcS{v*$8gBo z`!Ox2911Ay!S^Z{9bH`TXL4yZT3l4JrH11X;5^Q-c#H%u{ED5$LE7@o5J@vpzDkC{ z*VYpCW_CCx6)_D-ugpw`1fw9~a5C{8Sf{xlwM{rfy^vTq4L zYhSemczwq|6Iy@S+K@}b-s^@6Tw3NFVq5)J{lb7-U;FGh@{@i~vqZ6Nn(w3C;>}^~ zk-0s$n^kJ@gSZo+J1;MZI*o@UA>?BwXceHBpVb_xKC?J#+>Z$8G}tVczmPxDOn(bNW@{f0qW5ZH_@Uk@rmw zczR<)<(oTwTM*>`??2c#>>WG}6T|l#y;2nbCqeXAV77V_fz1RRr!JD4m-#($F$=mj zJodcA`w}*~zJ))TlNJc*w%v!gy(&aMIn>b~g7Vu@a}WZWx8D{4yTSkY;LJBT-;rsK z)|SyC7Bsplk1K19DY0$mN2Kg7rL8~rBPF6A$%;(W^oR=`4A(W@76tzf;J%VIfjb6{ zohO6sb2!PnQsGSel;~F0#w5zENB$bOhYuv1~pkxyl!!fADu+J@R7ofz!h?LYBQ0F2{nDH?@!zrhCz zSIelFaB#+&aFWQpdv1oI)BLTd=6^lQ!_W!?O_KXsKm@MI=8RF`T@y@owB~2_>7A{q zDI+eKvBj`JY{A~hLyE930ZEf8mLtLZRN1Qrv|C!rUTG>D*Nt!#NCs|&S(WSUJd1SB zK#WRe^&{+Cxq09Wq|hM;2$u=^%qAS6rmS2rcDFHIy3M0}L)C@!-So!}glf$IJ|>HL zf2O(un4w6itW#CN9^22kCgGvuQnSa`p3dlwBVf@!BCT6^dA(`KVOwDZ-_uBboQ~${ z0BIB6d2ykV{Xl4E{;N&8_gi%R)1lmU^@!@+%u7_ux0}D;@WABD3xherK?`gZB48$E z413*mHp`IU2ll$f5o^%?j&>(nH@(4I-5dF35NJHNQwm>Z$9+VtAz8+-nBo+~IJn5P z%@ABBm0F{10aX%CUQEA2r*`Ut69Sq&ri+3m!5pRTx`ZA$l13PLz8|o zX1A;=AOCYiaN1v5NA2>Adf577NtE3koB1bxIo00CVU_A6DaX_4 zm%P0mTo=nYk8m1W66l{{AYfdv)QCIcc$R9m3|Z!;e?~3i;Ng%_s_YL=j;RI~V=)9G znh^~dS*sFsOaFqRIus5g9p|_LNK{bgp@KZ(axme9f#pVrSo83N;ew)FGgWfUm&W~UzydEA z|J{wvHj;T^w#vUg$G6UXDE}v<7f#W7OC*@AoCPRgwh;v4zOwkyc82{-_;VYwX>Fwpir#(`B(@e} zcv&239R&H<`Hj58k(d^k24!I??UfJ0pNkW=2uG|DjrUH3Y(X)=1YcB+1^1mC9{;w)~hY=f!1aiQ@G-m^2oDBU52s z!MlNe-YO*>TT)R#c%owaDgE@>1951M1~3kT0K6{w0xeK_=gVucgPbmAq&kz{w?j!-#4;KZce z%y}3+34m>^7fs>;Gt8F8e1p6b0-NwR2%$3hbU{}~(;DTbVj_P@~i;~ue|H_T6gy>y`nTkhws~X_5r;v@^N7>&D z)6f1*14bLK_SXxw^}eS;w&55cv`-H&tk?Jgw@TVEg)0BDv|tn2Ly*4xty2-A@HDzL z8rq{rFTr6oa{hW~57$S@6GXhZs?Bp8v0ktCgZ(a9O!vit(MkyU+}~E2m^}bvsAY$U zf)^IE=SkU8NjcIgatsbBl8A2OEB?5Wb_lYUL1bMK~q8pf46%&oNtMC zU|jy4#Y%-y->=?sGgIh~^>N~pc8!TV5;vo*+`P>X?5CRcH~(UAk2r2sGvOWNHIh28 zCeBtb^{KySMA`l~BM4U1IV-K^0A zK7;l$r;0zPxDVP5zFMX=x{Uxn{x98oOB{OkVnDyDH-4DRE3&jxAv05)hkYcO*sYqV z_=JaIni&yw%@q2Mss4FQ^>N+rDYWfS`}31t&;dzBS$f_TWFg^T_vewN@lGUFRn0it zvZoTW#sDoc#fc(?$*;oesF(0PeJ*^H>5RTuEiv3!l%m}_iIA%mwr~c5AX1A4yeTV7(NcC7>qx?o+Z#rsHpT1xmCsK~Mee5ycpgWMKKtYNsZ_ zT=+Be=40vY)h%Z2uC^{tG0jHT+PC1uwSc|aC(>yiV_;3YL~N#^cs|q0*ZhKaf1Mc< zKmcWT6Bz>^JSSj#FV+j*=3iBQ!&>IcGa7lT*Z9@&wR4j^ywedfys2-MEPfRL#g$;t@@xspW;dAZZ28vxkD43z{25=V^Vc=;UK-enjw$91dW%i2Zjqo%rE`v_rGF}@H0-=oNmv5C z-O+jPC#IMg3WA|iMD$CbVC8@d$F8y5Dcx2dR; zFc80rUz13*xLnVCDZQIf>z1xB`3U@Wv7fk{i3<$~30^^w;Zf-X$_X&zG?8CI^%xPF z^Vq>m8SuTRDo{Nh3%#IKnH}^>-0<{3Xa&|#yF*lm=Cj)>%<;?%FExET*aN3*C!N8e zm{p-@?NORnMY_rrxn?gR+RyLZ^)JxMXOAnqr!1OGKj&v|u+h5}GGY0@O4JSJz4b3$?8S?giPr}GG~@}PbZ?&Lhq7y?hleAF_~yw9ur zQ=N}>Si|t2j2Olhy`hF(UR3DjTw2%0)o9(|SSmM&xi$3m5D{w=$Dz==%7Y*-6T#FH zepqK}P%;qbjXIK`8cu0adf!|K?H?OhdOI}O@z#PwOS{&=f)cz2^^vI$83Ck_NQZvg zGn!yp;v2BEO|vcX##t}qrVT9q1H2>fxm-wNYfQC>3*G7o@(rb!=I@&5*gBh(+YdnV z!VLS+vk+upBJ|Xy*SdYlJ^3ZV*X`()PSqum-Yi8lEy66u09rPAsoWtPNKYpCSGHtC z*{R;nZ2Fv^)OXh4Hr7oIQjxgT_hBDNrhYNlFh)OyF6*7}Boik^?rJ4Izoyem^z1S> zcoR&@hbQb|4n^U6dwY~Urtc(*r$bSZiI zaCL_Z&*p`?CX@OGq`FYsFFX`V-2Luj(%K?9CjaT^7g^K$!9q!gLgvSa(JGUeRXoF6 z&%1MQZ{>vzTPWg0luR9F3w@TEplt*!9g3zKrdaq>EFeUYK?mmHpk^geA<0lCuouu78q$H%>$!LZB{m+T!K_d~BfI$bI#H!37sANNKipX1QY|%*N#^0SOAu1Fbkli@T2!>J zcn}Ea@O0nZ@uZ+tU5&BgR^&>KZvHNh?p+c{uO1yb-krWVdV7}vqv(>s8A*YOd-T*fu1CKGEst|DFiCJu~qVjq+lF&A194mMBtT+ zyYLNzDE*izNpSRjisp*I2)+*VF1|;LrL8Nb-^ZAgpSo}>7uM?9shQedLJyD>BKQL# zWC*lFVG2hh$Et*$!VYW_|5t*U+YPu8QVMMk_lW&#$!W#`mRID31&4V>FpoMd^+l?RiU>P1ylSSOu;77+Gq z$5iVd^-;(y%p<7C*aUEOE`oG5q=?7dmI~;RFujKf>tt0y31DMR2n{Zajg&gez;ci} zaPnrDtNs3$IFA8kStAnQhow@KU;Q}mpkD0yh-R}*0cQbEcDo+654vqdSsJ&lAHsXA-Apg0CBolIvy!tIK#30$tEkyruN=-u_Q(hYwg#tAVv zq1EJL9K&RP3@ZipJi}91HV3uv+d^9THmm@!*zeyu&%hBIoSV;-LG!CwOeH&MucSIw zW@nk1viKX;0xR6hp>n(Gz1$KE~1;yGrHRZa9)^ zc(jy+oca<`(xV$b=3!Kwy^;r|a#6LR5*&Yi zf&T*sY$fJmd3qQjuHmiaF>p;5ET=BbbJnSXJc`fD+0!s08k*Ni$3>!IW<}PEX~PTg z{5u_H!pM_vJsBKQY6h$$>U!x!4H=Uu_@C)0iYVio3r}$G*p|_+MkWNfdAH7I(?>IF zU|+1*VC<9Dm|3H4%4m*Dm*T@Y)t*U&W9Wj{4>?!UHeMcN zL~R4=IZymj)D8Iz!eY9#KT31)Ly6{kj7_`0ARR?J5L4FmndqSa%!A6>Y%{bLQBR^b?oZ(qNd6|5IogO^Gc%uwcIBW;o$gRt zM8iBmWxDHMMbQdD(>3(vQKM)G9S9o9+SaRsKwIWtXYmdfl&}y1DMk-K49g_G6NseI z)?EuJ;J=7p)!gH9)9L$q@hTU)?YcjH5&8)~LxcT{Od}AEF|U7~vFkuad<*@+T}t+= zz0fUND2Xhk77dJzuqhG{TJ!`rPG_}?MJYV0o1w@#;Jg?-xzy);)<6h8Tn^BQx%q}5 z00001L7S3zL&=oDf`5=`4yNQ)(yX;Ml_{Z+^W{D|WYlwPE@2}z6GIqVK=wI_!J{1( zSqi&xXgkP^bkyfCCe7T|cJ}QBEoxYRO-hSbS74H0GCIhO)9Ub^I2R$d67v1g$$rFY zc@x2hjX(+;1N0y*j)I^0Rh&>6E|3RlI4gR_jf~#biR!^Qj#U*&W3`_L8euFiG35RkcvDLe6J&%^ng}NqcCB= z01NVP!szPL;P8|JdE4V6;5ysbp*p8ISMA$Q?Dp7?Z(c6kRRL4wKw|m59t>tNe%{ zRCV!w>)IswfJfR@Bk3_9X_q_19}2%sb@g+o8I`8~_5)`gH3iww>Tm=1EL?Wlv!}A{ z=|U^+#(HA&>3e#ZJF<}2kgVsp!LK-Gq=$EOT)C>3QnfmD|MpR$t`Ek98L!co@}n^K zVZ)r`uhv*8CVn7y0Rk)#j_8qs4+9iRYt6x!+9VVwfE7r0RxeM%H>!?7THdV%S11v0 zK8h&8#2BXUeH6mVcG_YFdAiaE+6^z_%iT}SFJ`a1d+9=i;^;w{T&$rsNNTx=lN00j zKJlHH*@Q-oeIo43usZgCZ2D>@UXkWdA2VM7HY^LlLk%V`Llxl-);=7{{jAa8fEDX8 ztpd9*Bp{3gyi8M;%F%o@pndEM!phOg2>h!+il+YGJdb%!u1|a9n+1B3@4R1JBJ)zg z&gqoW2|`{JO`%O{n(b~|V-BA+y-7Y8ewnUXanu+P3x%tJn`${w@|06mcmSdcOdMfVd!2Y7lb9`ORLS z;F8>rakx7QnF|OThHhN1kAn0x{IF`AytiV7RXc@DGkPvZEktXvp21 z#~0vtKfyp6DM)p{P9Fr{=XS36$A#csN@1d-MoWz>6$Lk?iL&Gh5%)ZMGQ4EAuoLw|EU2%93!4Vb+?AlrADn009bE_U$x~bJ zFUs&zg*7}Xe?ZhakmPfOv$B|W#g&A5*bxgmOKie0*1krjwt(%j=F36gHJJ(p&3X># z>Ps%~pmSzxhq+p*Ax_p+onY#VJ2oh~xgLLN)3N&W*jys(dKz^t*7^`|lWk1#6TGXD zQ?2>xHre%Jci+TLGTaQx65`M#O&~N${+!zPIJ-vUBK%(CxSM1nx0wxJV+6Z-jv0yr zZ`~$fJuu{g*nXukY&DaE^$`0%S@)r;HFZLYQ-r(nM3m^($*{S`LWgs=@}fEP$`kXa z30v?~plvc#1lqge?Y#j<-|wh5W7u=E!FUV8_5mGeHM?Zup1xi2BxUZCSJkE+KCTC1&V)VR z6h$SLrQ*cJ%kslT+^~=npVQ`!wq13)CbP(c0eSLN5>23NXiX%&xZlhCcJKJjy} zQB`WNL0sYrL0&0N5bgUvYMgtEAM&WCCV)SJFm|YT_!fV}PN0w?eLaj=?HU=(RcrKP ztN|!KQ0%;rpZW@&Jotb>EuiW3jQP}2yjJ*r!HIPXUv{|*1=JU%uXY-nIEE1^(`I7Q z+Xe9E)Vz<@noM1*4Ad-8>oQ0jN)@-~P&LwYY9pkAu4Eeinse@9(q=1xtM9!t>=ag( z%#DzUGqgh*$NZRQ`c+Cm0H!zI_UO;t#2ZqLM zdBS?3R_#de8FUn$)MMezfC=BN6z#W@nAcxtj4yTmp=_`#|9p!la;q1k3hPuHlmDFt!23D@c} zMVFbxftmSM?aS}Ah&c8I*pZdW6rET|K=bgRN6Pavkzs(eO-krl*5M4p1v z5#iLoQP+DPEr_-&sce-&-cPdA@UqDV6`1IfP0Mkb=&~ZB^>7$&*mOjjTm+Jv!*TAr zPMu71KQ$a*{NplNi15UEhhi1f& zhqfGBsdQf|e0cV8zTGc*nxp?A(9A8{(0}qtMvD&{?U;S}e zNOiDQ9_*2PU0Jf$*1UW#CB~79>CJYE{22T;EwNWYN|U4!^1yr?dcJ}!10<`@Lc>Xb3MOXMr}YJ?@N$Co%KVi%*!Mlv|1{`u1L3;G`;IoOlg zQE9V0m$Hzc_Uc)C&W6k`D-Y<6De z5O;P_UKy|Uv!i}pQ+0Ag%>GyQzpe#VIX{@-L7QcLt!Z`uU9_T5&6Hc+f9aNr?rM2S zEiBoyFcn*AGJ-)42!nhVk565gR7r(OV+J;YuHD#SZY1+=)oVGLwmy*aHt;nWzT8Ew z^1L8SQeC|45QPT(2j-N;1+Q~b@f-}~tFsVvnt#}C?Z-IRw3h$f2Nu^LU6jO+2|hw- z5Nr_N&mZktM&+?8JcMe;S55hO;`BEV?x%m{I?}!#W;MtQR2#i0{XtB2qbUl>MR2ah zZL1sLdozp|xo>OuyD0T12kQg3a}uk_KbbB~9*Tu7=p2%jkU%Vv!Avgyw1T`kHCP&* z=_uyNXK4A*G#6r!DeMjjcIEm5;-(zZ4g1Acn>Kwv5~Sj&sVfBOHcK>DZIUxlj%6$h zm+;z;y}V%$V!S`0MUWqLJ7)$*(p$0{7VsnHhOD2F^k1Nw+z}zS?Copunq5N+Ab55{ zY-y3N$+}iYx<_?L`{e~_A-|waxd9Bj0p5VUU zZ5jWfU##zxS9C(?u&=ED`h?UUv8k0$26mt|{r-ORedRtH>XYdy82-~cora6LH>jNd zQ9)ToU6YriB}U$I(lr`Z>fQ_?)O|>8E0JhcsZ(9O2e5j)B2iDwdxhi<9YATKK|2t3 zkzPy`kCEX-(xw{+1xwo8T2xvCB&`J)NvTUmjAkq2;AT=;uK#lyl$_>F(_4UYr{}h; z-?J-Pzyj@_-DYEV>L;MVOOPB_a40#t$uuaeA`BOYQUkq?^**GW8h@MM$>wYS7WnWQ z3{~Z`-;xnuwn)MKxcTHLz{}i@m;KKGD?0OAuT7(liZm|i8i+3%e38zCIlFx=%0I0&ELi(aAb_zNxsA+$Pm>yUEs$Sv_bE?7cI()%bV_s;8EDsjrOyX@6 zw@*1!i`Ee9nD&w=Z~$9xr4sfZcN=jypgNCtc}Bv5nf=>=-j3CtM>*gAKZZ=*PzXbM z0+7H3T~`b4^=5EX2T79(QDMkYi$x7}O_JnFlt6ZOL1X|miI9!Z@iB-ZNuaJdT&ty5 zY_^7x9y1ZTLa;a_CXh&LxyN=jMKV81HkQ|(n*_7nk(3D(f0_baaZC|D{T@)tkfQdq zA#cfgRj>w-Z`m<@BnujgU+f0VihZZ^nZac%DOg2JZ?0Dfov3r*&MFZ(?V-!Bn1J@c zBOgDNwcYJwSo}-NpPPT?$w(#(lF;0 z4%z1C5p2HmP2n`?v_g3^GsMLlG>2~g@`KFreBFGzk6C~Bfbab1Ye(Q# z1=Y6cx9nwpbQ}F^^DBj`!S_C`8xbr~&9wL+r|x&|74;G9MeSNH7}aXl_q!Altg*rn z@1*&qYyKj?NPrf%lz9MQNPYe}pU2bkBczvq3DQkG2Y2haS&aL;BVdrS-jf&LM&8s< zG{pq>|2PbQO0l2Wshe$fwm&_`9z5Mu5k`Qsr?9b?@2@e&l9Shu1Pg^kRsGO=fxV}q zoBAn2u0DaP7pc4aiE|EOmLvJHWs=`3Pza8Z_M%Sc0n9PEC+9DVv7By=6QL=}6UvcT zlYa4%hEakRM~B;O85US!U~=;YngKqLIl!sZ(s zc~}>Er&Npzh&XrzxzA9ivzWA`%J+WU+zs7xxkgD{?C-J;Y};LsFLp^_es@r^n1m-P zoxEpGiNfWa6o~{81d4q_mgJsZ>5tj_e8!4^o3sRX+`CmL$EZ@g zm(I3xMMkOyMiK1&v0$6&f{HjIdKZ%~V*GSBU|1hMm5KfuW%)(9B6QvB7}fMa;N@7h zAy`j`AvU|z8CX#^Bg<^E1PSwM=;u_wx$d0OzUAq`V_wS3>hZ6?N)K?iQX)#DmyF7ruzQR{UD)&CG>qy6$z6;M`2Yk-ol0+X9 zF>Cf+4gfgB>Wu1)b>q^1z=Pw(EQ-_Nb#q1e%)c)QIVbPqHO5B@Y*P^r1C!NDOd!@9 zlY@T)A_lFWoZdm_c66!gyD$!YPH!h0N5OZU1}D#R{(_M^$tRg*ZIDT3>Bxz$Pvm_e z9ySopq>EiWo!*=Q7GS@^zcnoc`JQc_GvV2JYtFEXNTkv8i2E=gKC|dY$fBA2sb9Y1 zf}f`6+EiQm!tR#jN(dF-Tb3+DL$((})%YMX$k-;>-8K5JsQ$-EFqjneBn9Tq;XzxG z?n2Szp-lL`p>!CUKRojq0VfaY0d)WPys%G|*&S=$gYNOC>d5EP=anI*d%3X$xdcEWAaUXF#qYcDADWDc6l{znpk zkL6YW1}D%mAs~CGrEPssgl{G~1VErkG6Sh%+PCRcD{{%}SbssWM`nT1sl`=top8U2 zl<;Z8r0wcQZlUy0G)3-d+0=ygKOivjhK5t3Uc5?4w6}m9h!BWxnjJDRO}z3B!MmEu zwgjB<5fGXTyVK#4LWCl*^%6N6OKzb`x20LB8DdaFgR>EktevH00^6S7AAo(>jB{__ zZRy#6TJ$j`sOf6oYyM59Xc)-6UTZ7k4o28z9x`O)ny5%B67m;z)ZD5|X2ECCls(v- zvHz>kuudN`?hQ#wCb~DeY&Q`3I{FY2JH4HtSq!8rC5`di3UR*z2a$JCjia3Y?KK_V zEgQ@GTw9Sg@Kgs8a+$3rb!!dB{nAIzsNHK8iP_{}&(CCuMyh)g8gxW<(E3Y-xhyvh z-(~*&nuf@p4%!JUE!Wg`Unos_M0Ex~?|7B@Nyu%@j-c=~10W z?8OA~f<&jzvHUD#?)j+bF$q9ba4QzAVn2L+Yt3cuMVIKNqHjqkZ-?Y{l!aq@nkLP{gP@@YJ$~Y{@W)zOXYvV+Iur`BDcC3yAML^I? z)W5t_ZXCozlcOMshBAShn-hqc1#kXf?d@uwV|UQnUF7h)5ctkhTB3nof3pDgH#kR}dZtxpN36bp-TUGpD`{vD&)_n(3J#W~e9xJ8 zyfv-Sr*Y!ZyqF)zI;AE6bB}2=%Sco4JB@!_SLmGqQN0z0-SAA3#3=GW<>A6G^)cw~ zeqgpfPcM?LBoh_?iM-x!cDSWQqqAQV6Q(7}>!lSeozWq)+(-Fsf^MA&R`VCWFAT(m zqxvRNf|05ty2igxFft%Bp!#emqJ|qH>+s_2RLEcWX^amqkC?7keq@z{$FxUW+Kary z=Pz%f&mv1$A>*_AqOsk!ty+UuR^Ap!m#>FgN|{9)E>;#fwj)u6UR7GfW@;IHJp)gLe7bfZLr(Ocs;*0_v!nX|S#XZQw^ULQ^V zn44g6niUb)71)a4WGCpf{~ZBK$FwDRoBmGf`+LW5iJNFldszR<7DY)Irri;C;_ux_ zhlGxWdcxpwT|8UNDvQa75f`heC8`C zwvc zUL?0I<)un&d%V;&Mkq#OiyU5g_{pG9SDdkmBRNE|7~P6SOE5i%+10x;$qV%d*c z_rYCcXE@pgq`YWl57yNo>wy$)mSK0b%~CU{3ksp3TBV;2tcbW z!>5C_MsM-`k>=E8$A78fzRca9y`NgRpiff>*P9W7M{8#a+bHF_WgFwV!PC^%(Ctwv zfa0!0!dxdMJZMXsbx`km2mFD&cD2juhx1Wl;{s)hJ=qkeF#Y$Y5xKbsT2L0H;r?Ls z6ood16>{cK5C09HJ|z9>57V@H9N~%j(w<2#$Svim0#$4c^Yj5~TSXddyxkTg{Cm(d zQkk+1BBd>MkfK5VejR&By$q1V>=!+M+ zdzQ_n^5w`ehZt~;AKv2LseQQ2FbVNK%T3_1`X?-!wAO2RUC`5thcV-8Rxiu5rp@5K85)eA8`fX))uhF%d2Oq;9o zI-8ZV7U9+I)WG-VhtdruMHx;WHjX%O6c=L@FJw-XOfzh)yWJKb7T6zxG3inCOH+fq zA;C!5R&4wCgj-d|&Wzm6U!1Gn73K`gZ+)7iCL5$m;bqm^k4jdr{#^;cQgscDa1Zv% z#>tm%@6KWn(C9}SY~lL^_uk|cRnMmE-OR{0X+b69DRs=p>QgR1ok9?znK+cXTL`4T zDg=Ju=OwanRI_ej#NH19^JukYsaOU{EEww3EurU7J6ZH5pDfES=<`m>jk1h`UGaQ} zOxS{uGvd-2WS+(OGot9>Ip0}F9=3ycyXqBVu%>*|$f04LnY@nK~WQ$L&k6`uG2d zX-G-`^Yo$+`^nKf0GK_9sVQ^kL)Ev$szEmknS3iM6wpd-eRgd>Q6DieN7be2bQ==K z`k7_W*{o*-ZKh!B6!V7?L91UgX3D*rdJ9DCtp~xR<2wO>4OjLdxuu#L{!T(a%x`~_ zYARgZD9wIfZ^Ab#aUx-L@1F-aoki&sm7*uT1OxErfE_NdRpQO7-y42RJ#z$H zsd__xnurxhR5mxki9SB+-!S`hG|kH7=27`Nqyxp5m#79{G7JO7zZ)qX1Z84!vfE09 zX%=woM(;3QO_+?R7qUx-{R-ipcD8mEx@=ITmvW*R&PiNBvt760*0GLz1I=Gmbow02 zTx!E{qdh5a-$WDC%>?3})5rjJNJsgoe1HInm$BJBx%wltDjB!j3wSH$NVPP*;FXLh zb><>s_L|Ix6!Sc9wweWy+QuJRExN;YJtvA4$AxLjV-Lf|y1%noqjJP-ma8)B;eZ2p za)L;(#dkMpM#VX|^A-cXZ5h8BJWHsjv`*Z{LU+g4rJ%xy0769scS9}dwnAO0O=9e1 z$duTa$)Pn4mR$Kudp@GT|EkseOjlhxdsN(*`ZIxK)6&7b>qz1-F)=Nexel*O!JI~- zZ*GI74{;apL!Wh=V#RILQSn)EONDo0DHK%Qwe*nogh0rG3+TY1&$Qd(VdY8u<7Y$E-%bWl=w8Y>mR%e8T8&0 zH~$^ZbtPCb@Qo_RM)B7)%9s`X(-k#IMS|~@G&$He-s=oV^yY!EnO5SYE7~Pp*TjG= zh#da2<6g`GN{0&8*FQ^CKW)V`JD^^gXWMg`glz;KN$uGb{ zTsJa+6S8+sBUj1x?wrrd z$fW*1zT~p5)^672Y(oPM&s6DTR?`z`|9R51MBaw49nfJ-eIf)G_dKNSchkCaBO0Yd zU)}~Iau)bwgtki6DS^kT|8nOaXRDPBs|reZR)XGwQC;bK4{!0J{z7;S7oy`Qj33_) zO@7EuRyNy{=_y=g<$vFfwQXfwxaL(elgv2=I_6=s@&MhQY4=pg7)~BIW%jybjSmTD zNx;a35XqUg4F$K@4zne{WT338G#C8oIFeU^fzH{ZvSX5;Xl6b?MO?oCIQj?aTU6bB z)Y*R+m~yp)S2XDSq)aM<#EAJqjt8L?=~#kIuUzdy)st)c>vo-9Cg>8SIG=>haR<4Y zNiVaqobG&2CyU8d<tH3T`*SuN=R?pz>SEH^Rx z9Q6u};y6}_SY8YF%HLcKvb2d8(|QoJ#WpT_o!lAC*dZaXZGyO%+8#*@!}@S6>(!_i zy3KJwUqdj8NO%w`fCf+WLPskMjtYSDyp8LTcp++=9qKh!sa5b1jb zYG?K8z_mHni~@uXH#Jf_f{aU>Lz z+=6a;gLL2MjZ4>;H+8NiJQWv(xgKpvkT#tHPr;x+bfsw*6qy48QT?rdR=0!~oAI*6Spg9Lvca7)SCs@WVFZ7K)jV{yd z=;40le7^}^s*3r+RhD#Up#BTqdMpy>bfYBzMD^l6vg9w*u@I@yA4p54(fpMC@z*LIu*^6@?A?+%r86Krn50F19=KSLZ zTTo7ugx`vxlIHS9>@85}v~#mbQKI;rGoXnH!!4qO6~ufYyoS%tWiY4E>>hx`TNhap zWGF;XzXqhd-6#2|`K&0B%e347Sn#jIuc-JXsg6~0AGBvJmJ|s6ene?4n&1`Fp*saV z!oZ9{o&ftq1t)2=ld{qP%*PA~p2NzlS@K2Wk?p29M)cFzbr9aZ8iZ}#9^Esb>SQ2w zJr;d}+H%0Ts} zUk8VwQ!4{6;ZWQvkc@WISGnz=T$j@(C=ac+&YY7x?(SKl-Xne?7pX>k`%ZfnWg`x#V^!MV&kR2Tayy*;??}j6=u#X^1a+)Q-{(2#!pO8nLA}_LYgR?! zU4Dry>ov>avp#f#gDp4KgvnA_v>`3Ejk7cUOed0H|2go)@Sn!x z!q+tf>S>6$V|nfO!P`pw9_r}OCjUQjF_+-rtHqXlQvDK1^zcA<3Sy=N5zY34yV}Zw z*zT9=vS1Fe>`-9#Kvb*hjRakDb2GqO@Y%oJFY41C!pwvc9KExV;iKE5;N%&i%id3S z)X`y_bF^V%pn9X3pmWYCGb~xFZUWEen&F2Mh$>oCx@N2GS22@~9++-KoIZG#H#fJt<`@(%J?oNuc z*`Q+EL&RCk&FIn$nBp6}85Kn~~uS?8=Y$dkL{M^`PyUDqsl>mxUS#fy^ zP{x`RB-|sml(~a)YOKf%pza z2^?&fW@iSU;(`4n+d3t&FEv?%0ds8?~1G zq$I7c^4Lk=>P18DF`QqqN;F*UgvZH;aB8G>z1mFhU4?c;iYNb4CI$_eU-}A-2!RkH zX3VUP0%eWKi~mjd*?+>EX;nI&c-RbRkL4gGk8mU0!e5z0FcB2W{W?lw^Qt5O6&N#L zYW}p>B6Jx=qBT5W&*RP7h$o4CmD=%}{$<^1%%}WqI4)xVfruR;p8i(!Amy=c?>>2h zBNCK#Bp8pUgK&K$t&Y-(Jd-hZZXb6E8SnGH$4LRuI`&~$&-FJ zNs34|=I|U{Dhgy#CDU$GAE_qh;_VoYQ*8q)u?ri24{BQ)*)Euv5vJ)Mq_0(k9Cp(9 z5%IL%>$z&mG{wiZQKzEGZMwqD}eEBsdB=(?p4 zuK^U4jt)IjW`bTws>9=qZ2ZIWIkS)^D>tS^zpjE1jT!Cam@{sRNZIl*Vo|(-v(ATMSIoXs-fb#i< zk6S;kWwzlg3KyE!HX(i;wAdc)(EwdcwvSd%r0SI|e9|k}-f<$yteDB?b?nm(8ba|_ zTid|6nL}8uSyx?;+(Mtr%Qq>9)8G$oN~4v-`?vwr(rzHzu7WMq2L3OljYC{IZBl?# z&Y(%kB@oeJ zjI@2Eb6-5q;3NDcD&zJFt|_iRF82tf`^Hh_U82IrFA}rnIz()7M*Nt54cs8nst9F= zlHVs3?|genYQrj>v6ndv%GK%J&T9Sm-nn2j?CB<8>Z_%ELJd@s$O7@_XkBsNbKqST zbQZcqJl5VjN<^_C&}iBOHqVrO;UK6In>_xqxQP!D$l~% zXTx_F!|o9@7L>)@^@Yj(z&ECJc~8OSB%pTK@v`HF1gCup(L2rs`XBdM)w5|q;wFI9 zxlGm3DzT-Lnh1B*)91}WM31E|t>!OO;~XfNVDQ4~srDT6o}sO27lT_)<%YmvAb9{U z4esvvKl288g~heuU{c?f%ub<7IZXz17<(Eg~vAG5^GHRe-;9I(5REYTG3*a=HpVh{!}X*H=+U zSUjd1s2z!({<*L2T>^<@jk)KinE=NQX_jBSt5lPDkxHNkCjulwmPywbjm9kY{-+ry z*`pDn@Ln-eJ3@=vJVg;VSIwluj!@=Ny}hXE20R|1o1VpssM-OydfdAHNNm0I6RQJ5 zKPdzWMYIzauVqve^sUe!*i<9$wmfsrjxmrDs-Jb}?&0G*ahKVfj$NmhY5LGA+vR7s zon#vH{_w^@xi^)}g$xz=i)50_W@(_>1wL9sap9d)Ofh?KbK|WP#gTYSdg3TNBFx|< zc*uEjFj#=EdDTi~y8@$Y^dtuMmzp~pkW z%)o^b61S|+32BnD3+T76w_X42tNLlgF^sv3HF2%eJ=X%EsK<`eK;McUO8RJKd67;n zrQ_>Dtt&DjTM44F2M;+rp+f+r0eLyh;DEpWv|pxe>d4a%oHn}ROTRbiTSJHV*8^m{ z`!|t_#MI#CjhpSSr}(WFU)rK1#R*{?gU580L+r?F=Js7@kxjTLA2lT>ZEPPyS>b7- zRCj8KH_8ptrB;{dnMi*CSSV(*_?0bzGYp}e!xETJcA>O3!m2!(GBFy_N_1)+rU#ra zh9xz^C>m)>nwH++Yp=l>cM&3324OI!#Po7x>w{{f5JZ}j9p6wE+r_cTp?%4%`Y%Ce zeVZJCgjzoNT z=R%vEGwXNWnfa6#dK9oa#^%4ESKB+Zt$s#?U~tl5#hd}1Y8uG#eeJ`2h{En}5Z+zX z4zz96&ZksDk12w+@=cY-0746wNh4$)AHZbI!|vsE&>(@aEie66VX_%X4!u5MGU-jS zMVH7w4t66523{{IT5zxx%QuETHU4i|Y%i&Eey9)bG8kMrFs|Y zFR}b)Z;lUo4;K^r5)Q(o$J8K`8DFvXB-!xNrq+2`;Gng%}M7S;7!|O+qVPu zk&Bf;xUk5on>{uKg{mu6{|ms&A$!?x=8Gd9Akoke5bRErPw_&!UMDp|ywW^1QBvX6 zbx^5x$Qt6t-@&*UeR(q`Ce2W1yTXTK`6D=x-yPSsi!0i1C*ZKLe~zWbc9qIm^WHb5 zGO0mPU5cGR?IfL?1exL7Q6pf~KurfXb7mESPa4+H)o=qjzi;s688_+;|qfFwX7C3 zM?kyHli5!h=@&0#r3Wi~uP{c~58-o2%|lqjAX9Tz>u2SV-u=b{gpG%A`ApR=a!Kv3W^b z-|#)=`BeiYKA`Nsb^T(3MHKTSI1zax_286}nEZFqw!>JE?ervX(kg4u3x(#cP{IC` zhKArE4f(#yxZ=!QWDErA+2=l~+@#UBPV2^k=-fj%#{<}(B~_f4cDyhyGGsbRSZ$Y1 zR6;Ms-61bk$;>-VSxwWZr%)Y(w(|&gdS;MLS3&|S?X5p7^{4f*QP+HLOdCKqZ&FbJ z(0u~6ZW~U$&3C0r(Moq>TCZyITEKd(fWr5>^3xL0fV2ZJ0TN1{+v(W=A<)yAxw5zq z0UG6Y?2N^EP%a@sm)8Hy@kWLgJ8CTFI;!XDE~twM_NpQW{v<&`*-fe4mzVTyE}SLQ z{cVUv)gELKoCcYrdC9-&hR~1~V?^~mk}V3}jvzrvM_Y!IsMkg9a75=m@b}jY#angE zC$`)0DjMte_Y`j$(?-nav~?QO8r6ywg>+K~s@_S0`>tgQK6Y4t7;AJ3D7GxEykl=RePr z1e?PzZH5x|#Cs(?a|?BueV*E(vF-gASozyh_vm)~e9Zh2d4C9tP8s#5KzI%AWj8Oy z1G9tyP=9`JE{_w1W7qh>Qg0P(_!@&ZZkVoS%#`rAb+Rif?008y z<`PXju`o{#E<2T=u*CWdg+x2CHYT^pPQ=Vj49IsVATn&_eLB~B!5Jlde7Ze_`Q4v| zyXMq;nk!9Bbd1=m6ymoM$q&>akQkw`Q3Exu%q;p}`hp88B(n~%NrMSlQIVrrr>fQT zg;|Yz#Q{96zqQ2a@z@b%0X(N(#I8SY;1KycNy6^XP{K_)37hO8T3O?4j^|aG=t<1S zI>^SSVU69;XXFNVh47Z&pbrhV0 z@V7{A0Wy~ofSJOlA?8F?k>>h(`$_`M6uP$I4irUXIX@keP>g>}SuUG7>WpxRgnsGS zxEwN8;d~Y!YSS9IdOcfxdhZsBW*y$7F+69YSr~SzVGOK?`npJ?`!SsW|(3_DUdyIr1Y1RTK z+;MAKYr-=mj@o%EnX6ZJ7eE|nW$wbPA8raOx-=tk_Pq{=1g+TsI>q0pLEu&A4O$B` zw6VsCK|Ng9n%63AgyEr#;+M^l!deON=pQGZCohuwU4@+N{+)=vcA=`wy5j?bdcN<5k z!hVre(I7PcOV{BdsVI{N_{x`=gLF_~jl zA!$r8rjez@{G&d;R<0F`$=|vldo=<+*XX}!!$TOkRRC>6XdC_O8}es?|J|~8KTg+LkXIvFw;FB!?xl_T zi2=Ob*yl<>WXEU0mCl*zPcr`!Bw}qW$CkVsu*2XzM4~U~-GbQ=bY?8*kQ__KKDyhF zc@;tqVcaQS)={vFdHPl+Yp-j8H!C@CI1U+a=Mo!FZkJ(Ma#X&r5`;`(!yCW=!9Rd{ zpdIpDl=Lc1-uc=dO7)(s@s0<*sPtw$MI=D40?PxXoy$S)D!cQi>@tRS_LUwh8j*er z2*EmKbI<=NWY`*$Gu@Suxm?3YJTp;;E_-T98NUpH;9UyQ*K?(2qUmFx}lbbwKx|T=ok&kZrR)Z z@N#@ul);jT(Yh@nd-t#fg=BJr15(mSC3dyCnO^=rH+&o3%~ewyPf93~CD&^jc^N;{ zI4wIo$O9r*LJ;?ezY3MqJLGnZ%0T;q*+DQ(;hs7bA9V4<2s#irY5QXEuktiQ7nN?m zYcS9hWl!IcC+!RIQ02Z@KYLeh#^Ofm-jpRj(?w@E{M60gi;cX5dMtCdPd}7B$-fR| z^lMlzRxhn07oS^Bz#e356gQ?SRbX1BHRf3ZA1G(cmFl+;!P)Q5Fwx@Sh1vtiy3Rl~ zYrRDnO+D4;vyI-X--Fby`oQfI}ziNg$PdG?yF@cn22CkZ(o)yZ9y_2m)ATs>J!(teAA0Op z{bx0cLw!2$9Um-ume=z;LY5O8Sa~{5N0+RhLwkdmcXQ*WH_`MP%VFY^Cqzv5-Kj=_ zE6a|kNe^fgBlW1GJjaWx1v0C?{N~oq&uB(nS|D(EA8L!?YZY)Q?5}I8OpJSfMe|uP zJ4-YaMcDJ!N_uj|yoO)5bnwBVBYaHT$COq@)hl}?->+xOr6;Q}1U$389aSk-QyF|u zwBAkDSl~7%qR9P}ze(zL&|IUf_Ws?fevc0?7LNbGv#yWM82~(1)HG-JUNaR&>(?UX z8^USE-2^)B7R`tw8^6=ibn=ZP=o+Sn_7M4t2f(nnHNx*;a<~Y86KwG*mvvGL^WI3hdhHylcP8LM+>F zTIB4TrtiR=Px@a5LZ|0x2k(`AG)C`P6qwmn1UCnJ6sw)mdPY=b&r{iaLUT^=j#q+$j7Wu7qI<`YD^ zbL|tW#PkDhr=LqWcCw%1#G(udp|s^YEn!Q z9mTjn$Baz+_!UfD>nD#fb&$nld}l1Eu{wHBdFOn{w8q5yHCt~YJC&nw zJ#OCwI7Y37fm7kaB#C?$TQw1N4B9(2q>f0|nt{(nWQrO(jZ{KL%4W1AvPI4T;o^s# z!W{mX!GeS4p+w#s%thl(exvuXTDC!-uGI?dzL=iQ6+<8X<^u0{S#62+Y#gW&grfE z4QME+^%=4M#OY9R zWi0Qli2~#Y^;Fz5rFpVTL6yyL(2#uULg`+sGI;Ba9pqU`zZ+27%&_m zf;Vs$?of4a*YanCB@f-1rJfw42L6p;A=S<-Tzpd?i5wSf6}=Er4>Ki85X95K+SvEx zKWwv=vlsz5^n)BW$N6_}h29?pAP4%Xq6@&W(fr_*--|4HPHVL@=u&TFxp&w-(${m~ z?Gd^G-BFx!1n)ns8L?Fvey(_hSlck^TBX&XWvyJ?62X!_B>l)u}y#!fSZf>JCWJKJNfq{P1rbZ_zZ7^01 z)Jk|(3thE0_8B1IDs%RepTr4PH8855?#HDL?WW_o`zv^>e0K0C?kOz3XjrRdwqpPB z+yaM?Gw>kK0)>`u#z<~gS9cMPq@0E{x-<^vRR$6y((O+2CcqNsTvfAYuj$UJpi+Q5 zCu65mvQBhKS+a3S&aT*XL1{Lep6$x;TmL@jS^%unDzQehWy6b|eP(qNr896K!T6nS z0Jy5~=kJ*$Ni9*=mJdH^FYv{`@dqYy-+=^)(IbB7-e)@C{a)}VZ?*{Sm(s()oA9EC z6kz`w;22i~p=)-72>+(yRKnDC3J^J=n&0J9O2dx!EG*dP9E#6(AW-);txkFnO5?A} zDu@gNiNkt^?D`yM1`X8XZrak9!)xqG``Wi08jG@R;0aA@38Od37qZ%<7RHDDMRWs`Kt*Vw zr_Y2t#p)7jyBPXS&(>Ms(SxzwIT(!_tj}+*Uu(@8@z25W4a;PCK!P*jB~`fkaqdg< ze!@+TVjw|Qfv{()w;#>_?H@d$-E_p%i;d~6Qhx?E&It?VrEc`&W%@bJlOW>CnGTQn zu9-U0Y>Evl!M%Fz54CLK6O9uMnOdhp^K&?77OIh0U)7TLV+;f=a*#$V1pf>F0M^bb zF8PB+DICw|98*pQ9pVXB_e{-$8OCCrMr6uRfcWcGMhhSQvhGQz*x77l=0e$g-Qi(z z9yhb-+5#(eLhnznN*Og|;brd#)M(^d)fA-^jav3(6G)uf} zfefbM)0ZEYCMoIQnl7d#7GF~ri0K$KA3lGt3#_X#;gsyxIQ5oW@y{lfUuz_ttwXQ! z)Q?%4hxg1z>Giak4kL?V&b<1P;>GV|9m+|z&CV7_X}8t8aHcN>Bw~8_KD%PCf{Aav zJPmEIG#;5fZI>9P!~ch5;^y|=8RTDQNdvJ*2!K4o=P(y5$g#qLB1y}7%PBMXD(4;Y z+o{$2&M$t%`FzISx6_17{F&_rI6oy~EAr5SjJWxDE}k@dk(0;$@p1*Tz=G*R z3fF#NIm&vSB^vFP^+>aTo!Khyb@;Pr@N*+VJiHug$HzK$pFIg6InwYYYHaTSVV$$T z@Y#*#zk+CQPhOy^*kbj(d;zP4h881uFq`R zj;d>89(2wI<9FYzoha84F)a5-{c>5rKW#uuYlZ z=Y4*Ku4#*;V2xj)v$w^~SZAj0kPu>C!^OE6l-@^(i?hLC_a~r^sdg!OmU-seV;LYu z-ZrwlrJQ+%m>_=8qyTT5mLDNj8PvI`g^2aM2ksW|%u;PKn1iX6JyJa|yuC4bcsou+ zq^FKlW_~exx>HT6zvtFWe`jm^Upn8Z`ZV_mmZ&T@+NYJd5OPpENEwG(4YVYL8)sd# z3r@Wr2IGzmThO#|0i_=GBYb;UILRku$7F};Ds9nP9I&BSI5J{`@=cvz+3u7x(SHf^ zVt=x%F@ylXguKwbfVKM85(g>v`umJ+O&^Q<2@r1|C(A+RXE=)6I1-M20ea9asu@_Z z`DSX69(sS+ud7a~pL_0K{Dx;tk;qm*Mcc<*C;Ob4Id4ay#HS|OuMP<5&*~g*zr}Q3 zFMFD6=!*@K(+e44c7RD0#&lN?R5TZ-sqE((KPRyZK}1 z^bZORNRF^Sv<9U*uSEGjY1}#++`FMUMf)OjOJWc$INiH;6|7tSg{XvaEuGU3)tbO` z04!wJqEmUrXD2E0fRR{e4|^gy;Gpuv%vJb=FZ@3yMWvk_L&Ah%I`VQ(?>ZwsuqXX* z?#;NL1oLrBHTs8)tFN#cho~K*c0-7J67&EH1Iy?{_kTT!T52sq_~5(23M^`}7u=qw zej!v40^~D24_{gYD-N;2cb&LGQ#1uOvl#Y_D*j{3>uFmE=j;+aiakpzP%hkT8w6Z9 zASmEi!H@^`&@y|DYqzXBc=0XntqMCqzB)dJJ5>dD$6YlWNZp`Z-#D8Em=#W{ZU9n8 zb5_`g1{$ScV-OZgudZZIcQcbkBB%uHL4Ldr72HO%_WOkf)`>-t z0un|^>>3NddZ@_SLxH2UQ-c!5Jl$N zhJuErL9A#-a6rF8`9ED!&*1q0OJN3i8=92eJkZ3po%P$yyGR{6=WZM_)`7O;C6iwA z{wr9oRZZ+nriav_N&cYyqvH8lWy$(|>OKy51HR5pM8niD8#1F319}F1s$#EiwNHrV z1eiVl)F&h#=M|{+RotzhlLxP)+#H`lcP-g$5uElK+-a+-4?kD2!#*nLFx3*|Nwur3Py!CjtI_ zey>YAGQ@#2q;#enVGmu1P498elB(O0#$XRmR!_;E_Mu#>ap4u3I0)Vg$*Z9EW1_?W zc^0SPwrtBSVZD17Z8%2M7RLMcu)exkI5xhSV(`K}`$|#1raLsz##{b3+P+rO#W99f zc0+g74A6&@r^@iu>(xJsUOK#LDRvN>eVZfk*5R5!EuzKRsq!#Dqr6~ImrOcSD4WBkQg=}^#V3%zkFf4f4euwi}aSd@p;>3>DzPZkyZVb^PzcsHe7?C{W;=_y!ztp%qj0swb% zOhIW@62(qO^6Yruv-~P6kGK{mC5$l5kM zKdtOwvVejo!RnuA^$M897=27dlaIUGv%xBAcC?g&_Jgj*)UF7MsXw2;=T=a&yC`6L zXlg1vI?%IZgR(JAH`OEI3HzYJfh+RB2g~1Sxm?M~4tPFe|7^TUs<6!#aI}E~$j)Sx zcd+Wg_^@TMm4^+-$9}NT=`o6Ng=*`npv6ByRUE1KL_AD0Qz3|)Csz0J74B1EO(?d| zv8*=hh4SH>7|GMc`rkQBk)`;HpFp)w$7@gc>5b*0<0_%jSr25SwZlNJC)rw@zkJGG zO+fPw-x#77{v=JdoX0VgA%*GCecsdzP`{^i$)L6;YKz!^In|ckm0}**Sb!hIjSvcV zh@Rwg8r1v0sC(vQF?)2;ajgVot{>cU{a!go;VD2CK}KUkvKL%CYYjo#+}`Cp zPG?0`HoMXUtwxq;@O|gzX{3PmAHv0|KhiQPD|c9)L5{_i(;H(U#GJ4@i{A!TzS#%W z=I6UjxX82y^Eir#s~hu_2Pp306eGJ#JT@HUi(M^2KcDqT(K#oCO?YTUGd|7jp8rpC zGwa8!d?2@RAC>0Xl~3B4P}JxGwT#fHCMs|>Au3(m?kmS2h_VISabo6})9^0VU+2_* zm~DZVrzu8mUgDQV_eV``(=q#J8GjJ2gWhyNnSy%PwLOdYc2F)mm#5=57v>@CSC@|M z`RYy3P7YRCuyN&HX04`U9V(@NeYtfJ7`dbcNo(#mEMsz&p@rh1Z<_#>```ZUKZ6 z&;`jWmljUPkgrarCsRM+_fr{tMp~kbl8|+r*6@Vqhv`l6@`Wk*4_}w5F(n_Bm*sGe zDW9@tF)(|3FViMKsENnn0F)qTda!C6iE;(T_s!%CnGJblmYptvH8>4FxBVzYmWZ9< z1z1|V`~WvV$iK@xFYN9C4}xKk)06VZ$50@*Wy`^@!#N7-dJClEiYU%n6rMH9v{6nUingWW@;FI2~b{1v`eEInrzK z9l5-KDH~=rIDZNanU~X|;^R9Bcn5#jW7eKc4Bvw_dq*72)n6etiCVQfU4}&wm0hl1 z!@9qv?Td3GE*{KH=JRlP`6K<)WRusV@kjiPvRS`GP->eTzY zgF#2ON;Bf(QHJ4^QW*u6Eoo#hY1U#(^>1lbqcbRuiOO*hZ2PSGNlZFQeI`rAZ; z7cxgb#rc-<#{@`O?%Wrmv=-Gd=o1Y~H~cu3RaD7^b0s>BlC4LM$`gS5j=SNHsUQp6 zd~KB|s_aW~7prSeYn5W>h)Rsg@etHy_$YABD%9g%4QF#f@YeUu;<4Q#L33Bs6rm#s z%}=qG3aOc=bJ>pK^>3z_oB_EyOrh4HyXYe(%rADnG zyax+1klREX%;zz?8VGq&er?Aou4lF+pV172XqY|H6(6hI1#UNJLC1StNh>u81858# zl#n%i&FI-*Iwj72o|cU#9I&hAOm&}YqOsrpbOCz+v^xpZN{4&Ram}qX$BgO8Fe=+P z3gp`5MyO6Sb-ML?gyC>8BfQkWP%xVz5_oe@F6`aYo1Tx4OHW1<=a>gLr33MC9YyC; z_^ut9HszUrj^0jEgCC=0F-m^=*`Whya4qP#EjK;Mz|{W!gRz6 z?w?y~eM?v5%#~^Lu6(`KOc2CwQ-sJv6u4<;@i~MG{C}nsD4R0Ia_F&JhjMGH(ZV(8 ztTdEUi_sT6RV*VOf8_Mbu(-5~h%qGi+#ON9j&kl6Nzwk)vpfOheZCd(Sx1DX03e?$ zRXq(3<-jH|(aQ3}qm$2#1WHq4wl56U4GTT(?TGMc8#6P;Tks)MNWIc-S4!-gpU27Y z2TAg{`OF`Q?j|D~R|r+}s+kKJ7V2esX`MVWs97z)UYtz6AZK#;giW_HBK4U*2}U2! zQhMX*qHj**uIkwFlN4_JGIKV^wb4}`*XDr|rpTR74%(WOmeXO|B%-@>dAoYA$Z2ff zz<7J7Bj?U^UYAezc=t!1gM~l)cL9vGqto?@oj;&lhm_pksO>&4+1cUDZ`^XR828@% z7)3B@Qf1(`6j-bnQFDy67tJaxL7^M$h(A}DLRDq(O z>nJ2qHat!Twta2#Q2JLP@%yl3V!^6Y(Gk$h)bM;w9w0+aBqAHbdnFrsOk?NE_eF7W zB33n{8L)KZwhvxeb3vonHG+_l9ih-)W*5Z}7r!b5L#l|uca!F&=2KJzH5|;oqTB72 zS%XxNcgoLwR)VAqjGL;?0}mK!mL_JQN_CF=1)n+L{LpEJ5sEz|8?BXV>vAwYJk>CJ zN|dtpXR@IHhrlS=Q;&XT-<~$??CA7?c7}~D)%HRbtr}Z>B=X{@ zqO;|czbhfEJGUXflE&*BhvYvt+wmF_Gt$B?^S!V9u|-AU-)c_bjnYQI3z12svW~tY zk+A$L+cBe9@rnDbFeT0=CSgb&?<)rO#-S_)H76_&L&f%vDtcqfF;m%e(SLzaUgug1 ze!);oLh?egAYZ;XijR1~WIEAX2LBm?DNU~>+lXJa^?D6|)my~+Bf(2;l@M3aIf)qZ zi}O%ND}U7jsUPOrC}Rs?TJpC=d_izl1bR&K*fv_fdAuJnniQkuVROU3_$<7m*0+(( z=qXhjF4!+vL%1ZOheH+J+5rID&hg^POndlL2r5t;orzDW79hpRbcQP@55IN8_N}$5 zfM>iC5E+zIzY!o9mOSp!(KA=VUVnD3M6wZx<`nlCN5GdztnevF3^LUnG9?3`k;;KZ zj2(|e7Z;a=qMIq;I`y__I65o^N}mv2MLsgzu)NBVuzACmnZN&AVOwYE#_^AWsibV= zEv^(7*?%nZB#VeDzfRJAWgVl-)@w)u?&q2VAk*2Bsn#!aBOidmN5iOLm!?g@b_$ZCHYnMd$g&Z7l z%jL4Dj2)O2v2o#5*8L-c&5H?7d^ve9r;CM=hBqJ*G(?QNYQIA)Ly-#NVbR}Q(C_~& zDL(;H5Mg=;U~y&2gYB=>yK@eZ(_(w=5+C|>SH6R%6%|N-^MS%6=zRfwVs8li-gez} z?p&)gvZbM&2a;3u7b=JkuW@koz3$KEMQ@U_Bd-nq@EDG)&3o@JPl$9Z!W=O+je}e- zYK4giXj-)Bi3{>1^u?rrS{5{tudPF>Dv-M0q-%?pIHFWh;A4Hl$*q!J9NrF5KEKxu zl!b>87Wn!UOr??*AreT5ubuWUD{^vVUgK;4vHvy?$ivwag+#PS^6<SZpl4 zw;X&2?&@KZuQrCst*7nKbxy&(lZ%fAcVstT*Nnasq^)eaTn|d_L4pKMjPl`M>kAdw z8M6epcI8%J_X;`>^9=j)ShhpX=M|D(WfqRKwqNE64v_Ewg_b|Mg1yb^h(whHY4|RC z45v;k@>h!-Z@ssA=D+n^0WIZiU7}Bz42-x%p-9aRqCPN{!mhyb45}t`)RO1t`e|Jf z;R(#O%@!9j06(WjjA)q(epPOP#DoBg_vd^&UR>hD*BEEj;M49c52ph4 z;Y`25yKMg!Qb9PA`*SNH-`&F-^tt`~n%@*Ip`slUXga;Ye`nI4!K;#Llg5Xn9n;D{ zntP1_l+^Swf6xTt=s{^d&&Rz2>K?*i@>VHnBdV(te$O!Skrkzrqmo#UqGF6JSJ6UQ z2#RlBQlw3my)~r#4>bhnxHy5x9{Rq(1+<@)prvh_godkQYt1Uc>d;-2IV9A8#eoPi z>3UjiEsxMJGlYh-#=`UxEHi$eCZbtK%8nZXk>)WYDf%VCWi$mabf2}qg5wwD`B{L8 zNm%LNo}1woP#h@YBjp6me=Jc?eL2qg?cdR81Q}0g&8?%fFk;Oec{gysC!Mj>F90-)9 z%9~Go4^zaify_tku_~sB6CiQQ$ARiKuTX(_vHyuWa!qzpr{Z!6ig5n%P^&E9-y}A$ z3z`&$aI3nsf0o?ZK_$*o72tO?{~`VW#b+3zjDA zVw+xHXppmQuM8EzC&4L^qJ#-mFp|P3h ziIYHa>|vR?^p|P~d}hR{{NsU1sCuxE#l<@=v(=#l4Vwn{I!Qaiy{M0)v{U}_NN$NW zr1@0rKpnOwUX^Bfq=0g35fn@zj2KXLm3`=CaDUjB{rFP&-W$unSs`+I1g{Q-4Y=*_ zZ*8ZaQ!Ip;{?&*2GL1a2VdgCS?kR|sv422JZ#DIL*i^D)gGOe9XF$>ht||JkK5cv* zN)_^?L!&t5W#5bj-E?-Y8;Xk1JR4Hi}@KW?TC` z{_%L7!S9wqGRwUYQd+)mkdOF1nI07dzh<06%3T3tkpUZ!7Qj&O020##AaogM;lZ)n zb>&nyf3O1`WZ`{NH1N^OHJ`=>3%js#J1k3slLk25xFn{QeKFYSWb@^Q;!1J1=Xfv#hU? zPXo$jk_gz^+)@E@i)6b4OP+yN!?onh^bUT!>r|iM8%dYd5Ur-UQN$_MU;F*O2uE`# zGNKGmFi%H^HlgZ%xO&bw9NGmq2?S!u6b(#>WsJQ5sFnjP_ze-tW;PKC^7n+B(1cB2>Lm5q35>yfdRLuc18&n_56-8si!Q{4AAgzJD8xG%RVcj&^eSnA_=;KT#+m&hH|$*hG10+CNF zJADFY`c*ePZppZv=I(f7fzwoZ630TLRftde~7`NWG75Fg9z&!V#~ zrv~{8PIW6F+tfmJmc=si^eS>@IdqVbIU5-~czQV(;rVT7X2)@T!=*b)?I|f+HPPk1 z-??Dj$i9;=eZ~1MNZ!sRsn0lhzC=!hVU24&AdETQ3jx~(@aGrG0e$O-3ML!EkXKWb zZTs3{(oYL&)LWPScSThQ*Z0>SLFbrLww6Yxuvt6?Y$8LCG9am@AWMbb0_D%PHF|X# zR^+3vaS3G)=DF|(DH905UGy<|nwYT&OU*kZIA0{RJRsoOpeGX1I2qH69{zoh3XxnB zU!Fmc!Mo0s^R43GgF*y{Ybw2p1>tY@nuS_oQ6UU2wnUf_ z);Ieo=#I`|SIM7@kpNpR2ejA(nmB${D?jfi%U&(*cLRBTwOynzp(t13g-MywDNyM; zj3{aZ@98(zfAu9SoLvYFypccQ`Kqm!MIG2BN`$#1dm@rFB7`hs$;SqWFkgZb%WX*j z&c>t*!w(bBifTX()<%x6^c6(XaJB85Xr5UE?#X?bDMi@dgK`@L)yvzwv4%dFfjnA2 zAc7}rghNty15XvVc!d}NajzbDgPJ+_ozOg-MQu+ra-Y}h!ok!7Pi{a3aKE;|b}Vd` z#|1IE1g@u`+uK!k6*$}}2nAYZqfCXj@#4+R#||Cd3}lwzUZ=1@h5UB9&%ZWza;cPT z_U2`UN+9p3S`YoMEX~CS9>=`Ycb?f6ADek8PC0H!o30cr4cNFF|u%GwMxiOZ>`!@~Kv-)sE@QyJoY; z6`5+pcyxy$xY+dmUt@^|d&yjU6w3zGEUsFtFWPrXdVS2~RJIm1o@QdJAP3Q^J$f0N z>&tGy8zv?hv6#^a@=Oy|r?Z@lx&3eIL>L%Ku(?(Jd<-B}mKOvRPS(SGcWBj_^RtYf zrAqFNZB_|q)bC2_e{6;L-CtS~W&nMt5I;nq_WV$j(4+10eLa5OaINnxaiitr`^y1? z^i@)LXRwR24MHUv=+cc7kP$UZ1r7ECV-b;iG6g)N?z-EfnxE}e34{Tcnu<`1Nxz}s z8rO{^_(ve8Nlh6NC2xxUa+6({?-e?L)w-8LAuunXm&}jQM5vZNB-A?%irV`PU_g69 zm+P!I`|Df|#h?T7m+agO13e$^M@Aah3wfiCy9Gi;Nje%8Bsp>CKnMx|LFc8LLC^*2 zV;?itl0owmP?Uq*O|m6h@hNeHQI)_n8Y)wF!&>2GuKTi19D=-0R{potb}k5!dB+YO zO5_*({z`$F%l76Z?~!ay>}%%S5RJ-P37a>`4OY&RdU6L)jcPHvi~)h#hgM!S%-B4y zk(!_O8LMRTCw;VvHZffqq;}0Fci86oCa;3POc9;>QtVH7Gg82_rNbv1o=SnHt!9T{ z4^!?$M*b(`A8_TDG)_EhXaB9FFL{xuv(fb+Jo6>m^7XYk#`nyqJuA1-y^c?FC*f1Q zUM7MdchTn7iv)=qrbd7LAK3vJ4#0)ClR>56kAb8N7$>$wKLsGqAceOJzrLpXoroj7 zEEJav_x*Nm0G{oPsHoJ=cRj3@jf=>&=y?pVAm7<$g^T|b`V(BymGf7SeULk-YhsIX z4~dp-@d8EjEUo;TZj5?=YgUmaH{LQrRj8fSL;&l{$RiK>hQi0GO%wCTCY>u|*Ej$) zB24d_DP6M0v!IR*q!iHD_<{r+?met+PFKFBPX7eoem5(W_q)m=iB_2W;ZX+m-rg}= zpdm6ZzMW@c*199dNMgbcfKyTxVFF~(c&k)6`%!|{f=PEb52J{D809e2C@2CVvc4=Q&g26k|8#7>jnCw-oN=U&(y|dd z?H<``fis>ttkvdfO}yWtB=awZ=C+EZ9Rz?3S*fVJpo*6}pSalqyVcG;89RHL#DUWk z%8eA`sB4Du7m2fo8Qa?!fQoU$-mK@kU8$hlnsSzrmDQvTat9V@iCoU`lA7FbJ32=`Gz^a2pa z`^XG5(yI=A%#>NO?$V*r&}oX}^iQdRh}aa-y`9PR`k2ZJa%ptQ%>>hKnW7QN(-jik zX9|>k+TJ6VA4{yh{$?Tirf}fDwiB_}w_P!xd}N_YD~}!%1zgfvW7|Vz?Q%mF1k!JP zsMVN%te9Y}<4YlippY>jC@2D>z60vjePdRCqwbt^2Ge+Qn(mXq*XfeAiuq;!z9=(^ExX2cuE38(3KS((Dil@+4K%2 zW%9fP-S}rep<)r$u7-_s(NayM&fSxJs;#DzA<;7a7SF(s}fW8Bi`w3 z99Y24#`jN`GE)MJ#O6>3J}ORI_1;Z`b<&+P-TM{#J$Q<0=F?e5-H)QG7&hAx&HTsL zC`ChcMLr^&umegqZMKM4+@_S`n0JISW^?kn=`?-0Rw47ctx!oL#5)(6|1OstjjdvNWs}!{{gw$h!cHB|K>&8CARmXw~HO2!rA*nD_7v_jj0}; zf#m?&&DQH<-JJ67y9+s1MV@_|CYl%VevpXR?h;NN@?zMWSN0cft@E}C*0W*sL^YB< zhj4^tU55+eNf&hF?&Lg#9(v~A48Axcs<>x9*nf^-!$#885AnRd)b?QS$RcsC9P+my z3yPV?aD278(D5km_g~K@Zm>?xL?i+x$0JGZ@4?*B{FbR0if!lp>6uKJsbay%Cx-en zrYpiEhR9MC7YH0RksAq402tG@1xfJ?6`#!|uvcaPjJn^hp88Ii<9bF&?5l?T4#IcW z!*BW2u0aSb*uX)aDfe9?w}8<4(vbV-P-jDVfC2ysJGBd7<)LhHuJ3@1lnU7?#06@D=ma0lr1}r{ zeMG3R1jQuBws!nf#xrOhqo~!uS24cgrFH%b$VWA>+u^7?rA-W!Z*ZgTVa`_asZs*4 z0lONQ19U2qr-JHTx+*B1=8Q_E-NZw1R&e%}7?8SgQi~>Cln>Wm!akB;XBlHtH zGk)xcXDqwhMLx~EJ3MFg{PwJX#IEn6%=Cvs%=er0G1cnRJ)?{4br&pz1M;_5rxfPf z1QY;PrS)IcT_f4p%d1(l-Yw4=S)Ps_zoD+~C|LTL+#NvUf2*mZEHIM@kIR5>u>q-1 z-uh1_GLLN?L%3ohJ@_g*vr9Tky6>3il!>L^;CXr8cOh%o;Cy-yUZHEbhaf}?O_4Hg zRcgFWw2R)*3COR;u?|O?19c<4&?vGZ-G)A}25^|ZZQBS*tJhU8<5gmphrN$H%>te& zd`+$ULBC%tSvMEhXW=U0qYFloKiLduB~s8NN?+1utUqon=8WN0U6l{2c9!{+1(7Oc zQs>lHi-kd6qat18U6AXOl%`=;RiJ;q)fGPxV{DpPO)>wMpVBMlQ3`{F4F;C!elVl4 zm&}uk!fPgu43x245j620xQ?|pcUeuXni2E++Mtz#MbgfbWEmZTyBhonUs7UYV?l~{ z*X77=EBToEszn;I7|15RcsjIs0|!*$sP6lQ1Z5?~s}A@@Q+iHPxF71LZZdIpP68^O8ZAQ|sa-6BPBon}K)5Gz zw}sQX#pY>E5J0rZKK7ZmRz>5xlBqM=CiAxu=F7AxWvZ!_I#A;3D(;D>TlbX4r;cz2 z&o)Z5T7kfGDQ%1yz}p4c*H%Ta)atG1cVQpdmMW%mQLVf|e~k|-uCV7J4c#OxE3*y| zM1x3tf2G8ID^(Gl3bP!TvcyD6jfvmJan(da%@R5JHAAT7Ze7a-45l+kjM_#qzTJh~ z-^o}2&LXwg!|(7ffwKTRd+p4MK&XBr3-7ARDS6J%shpjdPS&PHUm-U) zr1lm8pJkwlhzwILMLB!uB8OSe1qT|-tPE4x|*;XnJ=eOOg zI14NK|0HVlA_s#=r+|B7UX%X~K^4lcpO$Fy`ZoB4#=@09pDMobxeXy^x3k~i(oK- zsuvD%RB;PMCosp+lTyyJ__monmM0JC3#g6L=DB>36SSCG381X`k(V!@t+Z`Gomzc+ z;-6lLtXrjUu0zOBTHKeNh6-^N^E|US@tV?EAkS%+nCLM_FcEFE8mk$?#3CA)1PWA% zNQ23k9`{cNlxj|B<`9Zn?EPz^z;PAqeZV4|SDK(w2+@L>8Gt_z9So5`kUxVWeFU4Y-8B7EXGHT>??hYNE4qvFgbefg>x;`eJ3b6RmR6>hQ zmr2GKOph-3U{76Rj#^QRM^ur2HT%~SYUZzBZ7oAEc`>royidP@{2tcyHktf<;s3<| zS;rdP*;bi!uMr^A3It1EeasJac!|OuTUbjbf=^h^K%KxelACH(OaT%?p9e!sz<7R2E0pN?0_^ zjJfLU;COVP3O+D?ZCwGTJ32*4zKtx^n#&$eCyl~-tiGl(8-puluP#v`)0tY3$hrep zy>a?xD86Gb3+tgR$3wma;doStX^_T^?M3=F_p<*F@^H?>+{pRwA@kT4vxxHDF|s)H z29uYB&$hkvVNwhUrW@xLD>;p1Ddg7S0IfTqFm`Pbk&N%fNm(kXvoGCaPxzwqiuS>{ zkVIU%X;qz_5K)_wDDSBnpt=cRv4OG)JnwPtR@CjLz~zj^sIiqv8#9(w9) zYg}=5`Zd#}LPL>`Un}T+!EwFwxzMGk8)p`QTz1BUfaY>u=l~_xpTU4y1=xRxp4qG4 zmp9#^ONnF9s|w8R_R(n68-_d^cHKYnc8kn5bmSQ`(*QoD@ay^~UN_z~g0P&GDoeIX zYKZ^1N{CfCVKCr=aM*a)48eO{X}| z%h7vRe~QuFyir=nF=CIFRl+?;*2BzeDSnEo09q0+xS0`@RS*k717|In`gEDn`7_3g z|J#v(Tc*&Krln7A`XVuqoZR4BB%BETgmpSrx#9FGS(w6x1D;Nqp@p#q4MKELW*m(9 z=J^&$kMIF1AI~ea*JTnHN|;Q?EAs*SGh&EesXtrd!2t8_gn3nZc+86}108?5M?{y+ z(TZ!_`VLgU|E7WShNBo5>eNbAvb{*;DJiml7X`eE&6qcBj!JoTVs)2`s{P6|W z{8AXl+aU5=FjZtbe3tXA%DcC{x}YWZRYFbUOKt@BCU!C2*)j~P&9-rJ=l*`P(S@q` z%GGbG6V;Fx0kC~+g6qRLC2L?NWgB7%Fh7)hS1#~GF%u%##VnfAk>-dQtX#t#Bbl=H zXeBJM)zonXImSgs`$<_W*HQ!INv0(ojENs3X(#I35ILJDrWb|_m{}|*ac&{SvamjE7^;C{xqpUHtQ<_6wI( zzf7nU*|m$jA|2rNCBfMiOpjy2W55-IhX+woztC>rDet(#IbcqIS3NC1&j-_h&QF4G8g3uP4=!%kwwlTbfWiXmOXQY)!FpfX#y5h zzYklr5&mpXFvtjhaWvrcqHqi8YE%F?@PH5Qkj8RXp3TYkv@X{J8(8AYR$k<(-(xtP z1YwtY!ILUc$P+3eK4{<(ln}c+BQ<$1|7B)$!~n(ucAj~zkG}DL&1VA}oF^KcDriIT za{eN#EwNH~s&0_M>5T1qlgHa$4J}KBH;s`8qKh*QkIgY2gZe|c-0ZNK6fMs8Rx>9Z z{!?kn7cuy1M8I$V{aA-kAhwEX8>mW|27X{XYJvq94AN!Do`&BwLb5MbaOfLK+<0X$ zQD<^M&LZ+j_$R^=XEN=3Jvt^r#yWpF79gOqQVq1p1_j;D!xU?9avpKfN0OfY zUs7U$#-e%ucPGup>!Ir0sj&Pf08OBQ>b~CfPY$8|29rr*z>ydGF;o)vMKkrt_|OdP zZ#c(51s4P975S|tPvSgz{gu$~5xY1xE2Fz#kTbCF!0z{;=1^~U(Blhe1Cd8}OYOkB z-WP~Jg;-S`L>5(5U?c)MGRSj=DogNlaEGjft!|`^=|aZvy_}h&C3p7scUsCeJuwZd zERN1&Qp}c3F-1;BCifySG;)r*FaE^i0w z(`H+51v-sdviFOj;&g!)OU1otmMb9}AkRYF0eSXxc!8$sQfL2eL-^q+Q~D8l$!v1? z!Hojvhx`9brOoB}!-0x5U#IwK3-0s3wqWTZTMaHajQ99HiK+vB9{-qXC@hs)8~H0j zY_$xTbaG2L%ff{ar#lqg7BoKn{38KOKSCXQ2_msf@_&Q`k*Nd*5e3%E(?DpAw-1fu zT;|9^Kw?2*!Ov{D|Ajyl%o6UuA-p2|8ToC@us5ZXbBH5%D20o&0!Q(403C->u-X~J zU;CH$!0h&U0qe&Za^jZ8R*WZD`)f%enNYtLr5L;^b?-56?WUjY7{zAF-K^wm8u2y| z^SEH@8}IJdpX+}{Y1%#OmKyP-IXgxS({C+T?Y>!97Wdh`Me1s|8-w2$hZMcSs(D-!WeT|F>OSuW1=~(qQX;W-WaISE2JYj=#(Ml⁣- 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

Oto59` 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 z�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 <=FJJ3QkB?E|EmdK;(3J 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

-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

3g0-+ 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

g{Kt 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

cDU1p- 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

VCKyRo`2obO9V>~VB2i{R;itDS`08a?(OgA* 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'e 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℘24uL;!=u2Vf4t&|^5ToF- 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

9P$pWX^2RPnjchV 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%0D%sYG*w9|8xg#@xTcUPhNRNza__ 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 =~63tv)0!RM12lE3j@Pv}kE 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

;_60-zmpE^0eIZb_YUm3xVw~X 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ⅇ0SlTW_3=h2?O4YuNW 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

XZELZnL$l+613`_GE8=BXQi00001L7Vb; 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%0ARkz(gBDwO0mECEFdO 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 z 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�$yXtR3v1pnY}& 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

SsTY}=l)TCAuFo5vh_ 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

* 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

Eyb?M=P4xhGB8GPP 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⪼*s7VT@^;T)Q<}9DLi)s& zB}|$)j^e70r`l+q-$7-r(h=_(w*ug2mOZNq&UEB^&l=u8^*f#t>eVo=; zc3UnW7I|@zmFwt{4sBp;Yye(Ye8Knpu;+*0FqhwysUUN_rZ~E5*ED;yOWJ!yWF&VK z`(#@&dp~Y+%V?-thi6^a?wS3Y@nsC_90Fz$IexD7DF=#=*|tub{m0Tb_sIeQV6jgc zK?c5mJ%4!7M_lFN$+?t3BY(O=yB44Cr?ASo zdyiicel(h~&N`NFwX5KRU^W|N=p9$t8%V%ve_XI2Ae5MXr@${5% z67(K9Om%pgbwzq*xy*+-^5(pN3ny4K>HKCAQm(sq3*l+yG*H3c#Ro<76Y?gUREU_9 zW8-BP2TDZr@jz8z>aAd~KAq%i z7`gLV;P{g!LhKn&i+4!SIZrmZa3eZ0h1_gkO8uXim02CgS4GZtYj0pbo4xX71x_GgfGp&zoY3;-Yh#m~w;M@PeVe1N2snNf*0^M2O4WiFVH#d%QxCt8qrDN{f48PTmJoT=Ec({h6B)Gn7&P1(vl>5-0$P z2@3Ka@Z0NoZ)itwb^~V_W0-3TZ|MYBPt9b3L7By4a)1{8{K8N%2j1xHt$aFVQh6O(eU+3>Nsnx)hAvO+m({jkWvbGULo?D^BT zcdqFTMIpZhd2$&QbKMlc1082)d3P{cJQO@^ZX=lJxPAR-1h?JFOpxY44~^Vp`J%G_ zo*r#|!axIHa;*tk{}{Ak<392(3aD zlRZBeK(<2S*s#I%P%^Qk9*j_we@1?54Bpc6f?%F!aV4oJx%Mh!RAM`}IKH>s^f=0h z8qQSWv9*odpxOJmEh`W|mQp86j_(rdqN@o-I}g8BuBVLJvYZFK=6{rGrYAF=S=aP2 z{amYBz=d!Z@%xyF&4_{m$Dh0rwIj{;K2eXDYLqH(W{8id;-8f&AF(CHsx$pxoW7k;92fx32%tS2$cGi6dWh2E5^KHn_KEzk#`jM9P-Mgmt<7Os z#w(MU8CUH`;bidiAMUiT>OO*FsBl=ZQE%b5R?z(}j^~clY2n^k2sexO?$r0(^qN}X z^a6GqKWSWyuF>OklOc=iA^#a>pnd(cHimbLnniDW1QO@ZsYont>v^*4_~~VY~w>H$!8va;!^v8tD~()CefW^NW=8 zW*@Mvm>E@dvFBbdVd0g$Q8z1Pec%F%e*QZO{s*6<01Fs}bZHhqCt&c`#Wl6fX9&*P z(n5lWLjxdMC4Es^*Gd37S@Z&x5B^{vBPJXldhibTwiD>n%j?WFJ`Qsa^X zjKZpxu%mA`Y3l`g%NL;MDGFP!ggHDDj%v%}qiHp?`jvL>P&i4_YMZ&m{;m>;Ri}s% zO55nSIV}l>waxRS8`eTbwqz~RtKf6Am|)C7d+;NBlW<%@N6y*5U6xaf3`&7Y zX6z~MLuW>%xa@`zRnmDfW8ptgZyFZj4GS8P1hKxw5&Xhb7XR}B zkSn5BS~x4L`~sDlnH2k-^H6g~gSd7fC(sfpEdel%0U=>l#>|yN>I?Ny5dj}_Pq_&N zvG0v@{Tgq}ff!%ty;+=5@`QDtOq+QH=+R7TE$;~wG5zpmPuQi+W!tu@(Ndg{sl{pA zFv4zV!gS@AUIMXNG*&;#@uQVMJ+40l0RI3`%hf|n_{`>Dxjz%l)UJuUR|epb(ZJm{ zr-^Iv1{DuDEO_I1nGuy=xx5Fp+(Y>_zw)s&a0I-RfSYIVkSjA)#flm#nO|kA?OJNz zpYQaMj$uX5a8+u%k^cA_>@Gf`-~mtsceXhH6jv-BSR@NuX!g5YVDDL$#>ejii_&Zr zR3r^{D1QZAl$QIsRwSiTZb%!@^&Q0FOzQQE`&gnv?bcCYoOhv4eab9}II3et`6YJ~ zQP4v=f1l%))+NZvTG`lJ;QZ3L*39%vUT>%~u`pFE&>lX|*F&NW`!TH?k$skyn# z04G+!{gQbE3V*bHU9=hIjuZHf8orb%bc@6=jn&I9Q=uB3PN@?LcF4^?K|*X(?eoRX zQhK;TZ2QQ*5gDsREyDX;7Q(zdG4m$GKA|TCX9FD=Jk4%K7B$$_^kg^vMx66piq4!k z0Y09cx=vz3N$DF}o}fn=4fb|!R$e=nGoQA>;&4&)biDC0{p-|s{po}NaN@Y30o+~# z0hmx@93Uo$1mv(1N4pe|Od95Z-m` z)`+2<>4Jbp23Ocu+3TvExox!-BYwTDSRmm!Ffgd1?qO*xG1Z@#V|w>2`LXw?4y;Y= zr;i6t&Ntb#EUA2|68QY`D!Gp6&`sH7e+f(zFJ-!lK-5-6Zd2djK@>2^|9gY>8bJ3!H@3~#d?ZVRMWq1*Blz+LCAh1MTuM(^1B61%T(&x1U0Y0HJ97kiq|bbOCe(1*-q)tU$4ciHV-L)U>@DdL5pQfT$EN;{O#2EC-wR zjd;XAj~Bi1;7e0VGjo1tR;xCWk~QU&g{{=nb$`aPy<8a}iJ;?5f_l<)iDtI&%r;P$D1Xjy_ade|oFyMW^P+orO_Bow$iGn`dV zt=Q8ofep>G1%hW%89a50KF$=%==4m0j!XO3N9gg>2MOS_*d?~}zXRdTXoE^4|20(f`zXPc1$b;_ad)0y zY_eZ+Js|`?h$W{vf8KYR2G4wZc7kd>L1O1X3&+35)jbzSI7Ry*8JK*vU%c<037C_p-P&JR~YL6$Z>(oLwR({muh z37Z}QqmgO2vvvU7V6P@=r=W?69f5A7$D~N|e&%>+RkJARpGf?>QP*DxsTYcXvf1hf zRMR5!?K`ifLk-fm;PyM~cAQffY}IDPl%LA+t9=mW+cm`Yo(`xp<e2GSK=04vT z#8Bk-s|y)WV3XkkAIIGuJZnb_{J`2wjDF+fnGHoFWMS6ogpJfG5vWaF%sW5QhTz&< zZ_#jVmg>e@^}&%A6?=^SSiR~ycRqj-15figd=L0a;v*}o?$|%mV2oVVAgb8&<*W0Q zV&cy)e40MzN7W)$0`ihQB04N&N`tT@4}9p2RcAAdeReNX^C;NbXa zfSvB*Dm3zvNlj-+Z?5>o9~ClM0d=LNjiAa3VPZ%n;K4@fEtPlBj0|T&V*lyEAdn@)^Q3i%Y6N;1dTy*O0BXE)u=$A9CtJtKkUslQ6xBJk($sOL?6_~fae1mbQC&V;K(tUw&5LAuer;Oz=;)i1v&b?5#jLZBr7 zf7c}{fQ70^S^)U=b#XHkEKmdF@@INv$Sjv?9$ssg$h$x=zMkCGUduv}#d^G@S$D>U zR&VJTEGkD@^)9rhcbV~N#f@z%i+5|&0C%)-{hL!>iB2*mBy1dCXVcEF^7VAyQQSH4 zKpcK1yBv7w^r-l(y6(UJ+aV?N~gdFOIE}ey! zezo?F>^zy(`O=a7VN^HUnHo6ge2Yp0=ya>@4>QDOaw*Kzv2 zlRt|&wtBpl=Eu_oAJx(Zrzx8DE|o9r0a2&9=~X-z63@j$bSCvchyhKLTxST$e|Zn& zrw1CqBZ}8n3$XKF+~Z{7gm4kwUzt~?VrDD6iaIauu(?EWjl7!7jS|qsQ=~{fZtocQ z;}>K%Co)REymN`%+9<$GRMEU%WdseeNqpws;*R|tbW12M8s%h)Fpsg~S(O7CW~X&` z;$bP6T)_&uH7m#3MxubteocL>k0_>)p(p<4)vKG{ZhdECwlmdpPrP{go22-a5&G-3 zhBF+Q^}B>S0?lgPq@oKqP*;eNp>BGIR+y9-;3EFLj)yQQsv5 z6~=LuVq~ZTrrb5qRnS330$@PT;h8lLPE)Er8GBP}MZi@EIM25A6sar4E{ddX{myTqjsQY6BGiI_6f^H{=SVtG6WS8jqVzD46{!OOY@=kEO)f};v3Rc$Hv zEulNJSL)IB+0l+H=r=AaeaY%JM>6(3-tgutAkUYLGnDkv&FvF?NWxFu%uN9m76+0_ zI&J;ZTm7K>%}%8;guuJi&dAk!R9I2FP2)S3l6z!A*J3x0ZG%t@k|rqE{dms5>U{{G z>KFj^LYcb#e-^u|w#%|0pKm?H&5RAj^BIS>uLDV4%~6{*YpQ0+pnKuJ_~5M!M&Zi9 z=XkLX=kMN8N8qHp=qVJx%GyLjg)M0%3!kdaE=(n}JaY)7b9g-tQhk5d#c5J-<|X|Y zM`33>KHpmPraKJ71dk)+I5ltGOiNPwRVz9<>fr%Rrv{~JnoMB4uw37)AwbGED7HpEz`mgp8peD^kw1LX)Cib8Cs| z!(@xu37j8) z@@XD=Afv$G-kXK8EGLD10t(1lp^W3l3A3Ne zzU}2E7*M)tErqz8hW*-b8sXXj6S#zDiSxbPlRq2dX2(u_`SVA2_@cys<_W+cq$3{ zX0@^>_LE^3NpL*Ra`Hh9NFJ^ZvZuLrO8o*$F49gZ@=hFUR&?*$Awd5Ft?4fB-x(r= z@o9hsFvuoUTLHd&J>%S}Mb!wm`4N;SQ^psH4@HJGp4fd~)9NMbDRP*9=tIHitLUf@ z!sl@*8<+GZS*y8OeKY?4U8y}DO3~8Iu|4PrM2S?BRHpN_IM;qo=p~LSN}?Spl|v?K zHtXv|=fxEA6vHXhwp`g@)Rhl!&kX)JbXd2Fbu6XmN|QA2?@^%qTO>U>fj+1YxB*Sb zQQ|ktC8Y1Q+#4N=&nFaLe8aYfv#{#lli<)hUOcrm{nz~ADdz_lvMnUP&l6w*Z_$M zNa9`K^Vc&iJWlVixa>U`u{?;2jP!Gh-TS(2)=W!l+HMzmG1;-bMz*&yHQTRxnmH2c z`i~Mv_r*-2jcsu4VKp)t#r4QO)En~m%1yER*3530&*GTO?CmrXG%LpNkv0K}7JB&O zXQihssm6UVK5y(tQgW!P#NO5hr+;phHJ_<-0sR($B58_UG)MYr;_RqyKutA&oBnXC zN?W84GIet$D;Ro+CM#zt?ns5(@0judTjwW3%SS5N)CYF^l<_xa;D0*c036asm^*m8o)MCxikzLSLGRi{B zfy?J-WI=&=(^utVB-Yfl#k79`{E0sSZ~*}O1C<8A8?R?I92%p%I!a-FRGPE&ftP?% z*pD8-X(1R|poPFj%JFtpA=qmua%z4yRmdrlSKlrr*Gvu~Er~JMZ!>1ZoAgtlB8aJE zX(Q??{)Nase!BKtYu&4AL(89R~9N7DnICVKlZ)gV{xoP-#K`G z2NzD-4N4`!+|FDgmDA#3MI@(i&P4<2$27~NG<_X+s9bdbS9$fs;aB4$FZvq_$UmPO z5AfV0_9ZPKAHJ`5xL+J*6><$w?LBzIv@d{fu%$zB}+FLqAY%8JVfbF zr_==7*qdR_Y*HsB(Rn%*y5w3FsR zxq}^(ygz7QNO;w!M&j9YDDEjg3vTqyS3(-SBc(d^v7sLGSH^K+gM=aoHLB&6;*|Q& zR=Ac9^mh_nl@CYup|A zceOSLb#xAoDfSD5%yZn2hW{h#9@qlyx;6l3+qP{@w#~_yY)r;v+qP}nw%ugg_4ORz zyhlG_pR2CAH`cmzuFNsr+|qi*dyldp23v;bC#uUxqxqLSG~dg_`&wodzR1@ZtKXYP z<3{OOOyqU}TSo={N7|6ek0_GzEZ-Faux@sK1{NL26tDveY?KU$n@#v6w$PQ6%Qx_Y zaP|y@G{)*>Fx*HlnRu*3!r@F6Y!Cl=RpUR`y7(THHULP$K>CKpGtwzr8E8L&@$rYF zi;-U62sZMTLk0mLwj-C7%#bSkRXtb(5i1qXuHh&;q_`=)1<8IbSQ;1=wHy1e8^#yR z?~$1A$s?eQF}}9{W|6Yy{&DBXa9h(O&rF3gNQE=$p=s3hN}upi)FdMJdRfrq0K34n zPj|Lr9UqHg?l&NfXZL0^bZny0R}Z63%8a7arKjJZE>q8cL0<7Re=qgx-JCfL-DvPH zJ485Aj>jvGLwDUib4nik?`ZA8rvWjwP_0Qu;<-EIc;$DDCWETAolbVNMW*T2q~e>C zW?`K)VRG;`4fQI|+lM;N%qbFfbic~O&0bgo%v9{%Mb!*_Y`zshsN$?N*x9;GvKFfCb#!3AVS)z9{$ zm$+S%k-~1hVo)g({`Jlr^V~J3@Z|Aw$x~i|y<_{Jp*+KNWnbD#0tkrYgSly41K`3e zbAtc~fjm$Fj5t5+zm*SC;MeV<@U z$R38=qiCFFZ;8x?6sgJWe=-=qx{U1>)1NJ=Ag^|BUc24{?Z*i;tAv~?SQ0nH0Qn4% zR>BSB5dX5MCgPS@H`icr?BHkQiKnZ#ArB-YjywdeedDDiH!t=?E8j2k`j?|D-%bQy zP9j!cbEf9r09fh`X&LVS5*kGn>z4wlWxRXQF`6Wq<#9L&{It}zv3Xe=W?oN6rbe1v zBoZdYM_b4hgrIUA81?`cQM3`w#iF7%2u7=x?(pFpgSy?KXME`-UdSj<9QpwIJZVip zZw3py*4{Mx3_cQ1hyjk-mE9VgKK^3|vX6Q0w_Gk1)wpleuwwqhX??eAP~^}GWBLC6 z9MHa-SM^q#vX{ht=4=gR!%1`I-5S>nXclA8MKk(R#8c#=t@U&+QV-ILEXMIY_q~bz zH%iP%R0$!^=b+iYaN_u0F5%a5HRNIufIe&9JSyQwxq?eP3khASej|`$a>4;4es%YK z9(pQnAdx-=*ImjZ(i}lI1SCQ#@k(u-^JJlXb2DqrU-8DuW{=gN;v(LbEkvT=c0r}q zB@w)-FQy2izTqDj_xtT-9%ayKVa(ejkd&JTGx~%HB5T@*ACSE``$HjZyp0Ici;7PO zYB>zFC#^O*6V+GT$nW3$+Q95}xXp{MmZeAB}QFlCG~|rZdhz!*7^woItkJ^Iq#^ zJD@|<{qd>1!|1MO<5pf|i$IWL?A)d6FW2^*CG1?L)F4oLk&0;at?I#q_#wuB)_(9J zvXX`jOKwUFRBsI!AJG(qFnzsY&#=sw!q|;xRgy-!pjGIa?ed$OiRG%QDcgWNO&i&H z$Xx@8m=~MIiwjwWQxVSnaVHMq1ia{`suiE2ibyWJwDK_ZIBO|W`-dlyl)|dUA=EcogcvM5|XnYCbY;WeGn2mjs0S_;0R=`Mv~L+whM&x4$gHdLxqOnahYa&iWYfRk;`?3{@zDutoBCT-7!8=1Orwgin_?iXz$SY5s37tarOn7g4bdOaiFdMd ztJdT$wmDVPe=-XaDq%}UwYA}Jr@kPArtWCjYO0oe{J{zwrPu zjK|z?9JQz&-kA0m*9J-ve-|1EB*+$^u)vOr3xx2#MmtRsX|!Df_HyqsG{#5=xa)wY zDvkS^<>h2vS86~{C(YhFQIEB_QGrQZ!|3dJGu7aDV5-Mj_kGUeNOCK#e6&fo# zvgB@-IVM1Iqk;y)>)Lk;2D9^$(GaSHZ_3xIuihEziETx`Kymh+1YO=aE0D}YVj}wv zdsEup45VVxi1LWM;Di&o!xfbbDex8YQT*LLS2l;r>(M3TDGNyD6fL_fx?e1=Uh$z>9bOKnDp=a7H!_jObUHR>37+?$IX4NKbG z-u`1PUXP7I)`F%CLMSw0t{JmK`tP@1@|%I|i-G&TD<9}n;jICOie9=tTj^OvR8(8d zPRs*YK(t*Guan};bYmy$=j6E?LAj#03R7l}yMH=?h9rTijcy4QIUFs=Lgxf)HIr5# zfML!y$l#$t01`pdBup2_75I#3Q09)doZ3gIEhD3>shpf(IT3}ws+%riQQ=^-pcqi% zDk8iqYA$p5Qa7&Ga@@}@*gWHG%iS?4Ej2dI7!`bHHh7S~j9Rm<;N)~3-dRvt;BN`> zQ=dz-l|q?%^EoecmGT*x)Vo9LGqfvftR`U)Z{(R8l~nL~mtUt^(dY#EcI4(;5XgfVHC&Qc5)EmIfc- z7a*z6hmRh(v9Jyo`Eu_r7trB0VP&}ZFbBmsQu@4faEj`w7`@S8&7wI#!V8zF7IY+O zpcbIBSh~j%mo#F<;g96WMD{?~NS{2s+5^4mZG#xZZ>Cxp$LD2mK=j`6ASQ0ZJy5`huRO^F{L!$+6`rTku!reB&cCZ6 zUjOOwD}L)yeCZWvwiq96uXzXQsh<@IdxArK)8M;0j7Xj31? z=tWy+B9=xFb7bvV`B?DV>8@$_N9E$CMhms@;?P62&Hd_MY?r$!%tKYPZDwevO?M zsahZ(k%dKY??J2}W6gx^S~+a+7%2FZatpuni6FXOK9?g)PPi`!cV6Sy;}mu&*b`H} zv61v(&wq{ps^5;3Uyc^p&a^p;4;FNdj5Wt zk~ry!M581Y@g)}SGq5+0CC;|yb`c-#f<(23H4XFr5vS{Nht55aL(Y3#SoP!TqLLr=Yj7`ihGD+yM z08e#r14l!W=unBpEJIK8s>`I$xOZW*nJ_1fBPlC5w{Tdk@3yyEE#}VwCF?DT;)K*D z0`x%O%=*EK>!53cYsKp%=II~W!q z?&fRRDqc-Ozv-itApNJ=t?{i%^`*HN8-EP+N%dCQ9Yb`67>^E>@>s)^dd4EO7YdAx z4HHURQg0Qv?l^jU9<6bWrpkM`A(Aw_DmGup3BI%hAujmJD(uVO6(5qzC!lc@gvw@> z-}y(I*?J8Y7R#q&m&3k68il=l^$bdAw*EI22PkhH%HjHPT^8SBI8aamxy?Y!tghLl zw-Qc zG=!5F4)x>mEE%H^lDJtd;C3I=%V7l~Mc>n`iez!&+-^6n%ov^NZnO8w>C%>I|JwJD@0|zcm(X$5)x8v%G~hSU2aP zs^xag9?bO7;ss@;f~z)%d34hp9njI$s(vsGb}!D6_=p$dkZ(Cng0Fuy3+uNGpE0K` z(at0HdIunv(aZ{HS54rP0)^ zO(5M(QuM!I)c?k#`NA``rO*5C5l&hC-=9C_YB}Ex4o{`t?W$|HEyqLKWpslpiMV)- zb}^A+VkAwsvO$hpMNQTm?PEXCp1FV%cc_YSSt6LUZB~JTE^x`7<`a`_#BG%wNoM07o}y#aU!Yv{HAn&ZId*-O!I9zlqr8A@ zJa+{B0`mxwOnLmP&YmHlab6B*#u$OlS{h3!I^QBPhT6}*?vs$HoeEB+*93G|=-R-& zWDtZofCNmVqFrj>Qn+_u=ytrQ%bUPxh0qW`2|t6@VP=yV^|1bT1ePhvnVS#RMf88wweJe8+ zU6~|?@shet+UHe0Lry~_lBn5z7L6cVd%)_Uql`?&V4zXe!3WLVR8B1|LHS_1^RvA! zK!!@@GIMI;q%D`L8SUO4wN&d0XN~8r0z>EL-#WcJ^3B>RzzL(>X_r_I{Il~XIxAc+ zACA;D*=M+X4MM<)G~CWPo1_=WS7v3$UFbDAa(lN(>RNq9`ZUqzcAyJYd#I*Jc9Q7e zLGWzBlL$Ae&4_^UNrx}xXfaHIK15_gcsMr^SjQW7SOqab+)N*sC2XTbRxSs%9a*^_kBl%=}AG;EsKX{{@=q zHxS(y5T=_K3DD>2TLaY6euyG+v$U*t*Ui^pD1!r3ws=R^Dr&MDM!aE5Sf!=ZQ(`TCjSrj@9{q6qf5BPGg zhW^BoQFs9!t#;ftmgm`$N>(2b^J6Bv{3;pNV}-g*AI!dHPHIew7!F@!?kq63aI& z1J+S3)9^%)y$&69M-~$wb~}WluvNUYF)Ajs!qCA$WW5*kVq)>O z6GT&-xK|Z!Ky$$s;?gTuOb^|?efg`52#z;WUE$QH>8b3bv=GE(QJqmc6b++yrcI^y zq{SJ8*&aL2+3zSeDEES&8*GzU3~E06xAVXhM~{e_bg%nIBzjw+KAyCR>efIv)$N54iCl3y_UIM$9ATL0aS!Hz z!k>dn;>nkez0r|D#tgVj5yMj>f)}>v-n6zTv}iuLK8I#*9XXvP1sic6#UH3!pZ#9f%JJVM0`8k`DTM!CVmO&MX;dR0~X)D?Ohv=VY>V!JzJw?$&yU1xKp|*?$pEcgZt_{h|vYK1zq}ruRLnVnYypKdzLt**r{1ISPCH%DT zdQ|RAD$JOKM*7KJoCBDn;i zNXY{aG_4}?1GbX=7g#*qE!4e>bn$iO<7FXXC%0obBt~3D^RbObkr&`Pn73tz-*N4H z#sisug(j#RNo_;?`%ZTJW@7we>h@hE`fux(BDCJh2+*Y61>dS+lb*ehKc(}rMtt~S z;C{#%vZ*J_G#*k9iYHa&8Gw+rktVP9DGxIm!=7w<*B+dFCrp1Vw z_Hk$%MaxrarH#ssTa(x7rbr^<8z!hEB8BQrkrS>k+~GH_(NRR$hJcH#tr|N)b*z9F z?FlI$?rf1m&3j#EA@d#+sln!z>!q^&8HCjC3GZcKkV^SqaSdW~YP1wyZhSNl3A`w4 za7qu&g48k0sHkUeEuCAxq8neGGY#j}6*ZF;Jx6aUl?|YCz@8!RKz0BynhBf7);H&5 z5Z2^clrtnnSO`w~GC%$-=)vz%G^4(4i^m|aJK#Y9*}DGwkaqrdV)}Bb(MkMjxJUd3 zp}`4zHW(kh1U*Ez$*j*gMQAmjhwmXk6r}FiI&gP}FwTT$gtj^;5X4ojvTvWX#~$kX z+-UG?WeXnPqyKA~i08%9*`K3qHh5279Aylv1z*B3qd*qYY&%|KHwIg3%Rz!)86sWA z)6EQhC$4--;~-_(Vvyw8(3Wa+lSS{uLRiTmkE_3tjx+{p0Wc@E~W31dQy5(zgWVX;5_Uzs}ZB1PF zIfTH1r;^fYW_i-V1NtCb z5x1=EfVnv4I0ZkMI$^{=XO7h}^>xY-m;>+_uJ=FbSl^${tmb;VV=+>M1nw`#Bboz1n2lUy(*&jE=kA zNm~i{SCe1&1!VEzdX8S4=x%N8oR2g;>p{wz!`4U42LIj(hH~b}+MlFkeD##@Nc;nW ze$LSx-$B}3%;MzK{9rQ*Jiqy9Zlcb{AD53yJ9YS$a*BYa8bAia>h(PE%hves|8 zxgsK^rH1o_7hF9qd?EJJ`~_dQ2n618^>eJ&H2yjdI3%B)sSM?DSAu>t4c#^(5hdWN ziyOc<5A+FBbS+{2cR3^>jeEw<6QLe4J??^0M$8r{RQS;Hj?|yaQoFRz`t;H`HM{A% zf4S-RT_D!40x=RHpaVX?0RZ+XX`sQU8!94dZm+rb()4`W)2C~%`p=Gv^b4yEFYD+KuZ z_Xf~y7alo*9R$mCRRpVdF=YFM@fQ9V2D@^ow^vqWOf~!+qH7R*{b6IpY7cPoK&6Ti zg?3WX`d>%}extB`p-hkKR07_lCLvHlNLcK%-(X##NN-LD(0BBCe$w^?!hII5vk&+itU9@q#kEG_zhi%FaIo7k;quc)Guofw^4Ahu`O@eQI6%apIRj-1P58?GH4@Lb_V^l{1iCVFn zn+VqSkML)}s5#mwA4T2=gcH%Bb9^|V9IeF&b^IW2XQPThfnmaL$7Jn;_cvmp+>VfH zhzP;|=kN*r=3xKgFw!Pu2fTc%%-^8y5;Izt(h0~?`mHI$A-Xv~Q|HtPW0QJbp5pHK z>g3hJmiwMHA4U!xe~u-nC+tN^PW@96s`DzMyJ;iKkJgveZ#XP0eGYy~mVi@p_tYEe z36hgZqBfr6h%x2}$u?`POApDH7ozjxP%OGPbYlir#Mxa3I_mCtE-g2`SHPu{mIsm`bvq9_0FbMV&b4dDMJpqBlkT zVcW*ID9%Xjbm*n{v?xCalEx0;Cl(*mfwQBYf;p}6*WbKCq(_@nCeY!A0?m^l&<@T- zP<$tDU?NbGBL^3eZG%vAEYCVhq~dQL)f1|E+@qQRKUP|uEas+~$E@v71Nv~(LX9=} z1T8iFS{^+V%1`4rwkD6k-?7jU!CO8$Zhv@=?K!0gL8O5j$&?$QhMO-w_-;JF1!iRR ze9G2X7&wr?1%ifH2Hftl-BK!RLhXq=|HQQ2*Rwa*0cTGQ*fbe8y9LsjCS#y*=j_SgM#QXT*{2ld9G&%cRwAB&84tn6G&bYk_Gknc>yJ2e7MU-% zcWJj$|5pf_LupGk+l#mQ>}$99@y}HiKYp1GU*>scw(_WTN@6u}PaHY^s47btMpKvu zNl8a|cPyob+fJpn{&O;4vNy~~%!cV_L#|bK%53e?*>DkFpjc=tx`yzr&MB5;Q50Go zY(D=o9LJ{aNznAkQVIn^<_u&HH|#&U?u2hTt}i-}*OaeOF$`}_s%Ffk*OzR;OZ1Dh z2o@q2U6+r->>#2sPSNb*)r%KmqNiuw+ZV4VW&E@<+fK(QoC)ZvVzk%MPw4j~Qm#%}J{|nhLq(kT z(cKQZ!ciE=uLQfip+5Y-{o`WYL|<*u4SJq4iQn^E)%wNs{K2Vcnr<%M zN-ZF^Jk4luB&R*dT2508#g0R4=YjHQ2fH|yyH7)C#d#P40n9n3tk?|9u=2J8(H$}Rq2D_(6ml*)MMTB0Zn^}gTv16Qs;|jmlggr3IBms$dr+HJZ@8@Elh6!1Kfqbj zdl)!+);wyxcbe3MKKCS80?X|k)~IqgX~$f}PU?z)h)GF#>ZN2{RxttHCo#?FCv9D* zd!V%{48i^!R;~^5zU(1c4AJ=n*613nd_@b<%)SCMp(}xk=ZV^+z<7Q3SOm7K3KHIh zqODw?@enB24p(T0IPiy_#71RctsIXVRm|7VnN)rG_IPR!9JIy-gsW>p;gy;?#wxN#7B0s8=F#a(o_U(%U~6!K43h|;dAt{29- z!WjHOf(4?!q`l7av6e9@*$OpB;w3-B(!P z1Q^1Gq26!c>JC7iO=hPx?&q@Fj`2oV9*3ZQiSU7&T?uu=y*?-(S z4@*l{$nfq`k-A;dEdYsu4rew2AazwMSSr%JfqoKhEJOYo7Mxb>^}dGIpX@84rJF!` zWY#)N?GLJ8A|@?sW!;kj@}J?~0HURy+yJoZr(hU!Iy{?#gI{L_%AUEMpO8}X_ZSrw zqNs&o@KqVE8)=;o-a`}~pIHrVvFup>a79wfiMS{_ann{R2vX@%(57tD$dZK=>(SYd2U6oKGgJ1=;x#tm@&qMbIIoN1sbJj5#7mUkVvMNK4 zT~wu&DILF%CvlztxBFjo7JnP^eHn7NvH$+>aJs5)dctyS4t!|mZ8JL7SdhL zvYU{bozfrau2+JT;Iz52-R#Lc$6|mhJ9iGH6@j!Z)>o8(rc2I^`m2gec0 z1AbMxqQ4Rs5pnqPI!bcZIvL_heHE&spY_q8k%YS z9t+XM0}kXN1(uiH2!O|HeohfOLfM*J)Q+Yv?U!DA+BM}^+B}7Ak3zKS$5tkaw&B< z=P9YhDnZF=zzR=MbjNftBAv~0Gn36M8=`O7-5{>^Yia+XJdkaGJR=-Gog8ix53jHZ z6LMS2R`}f$O9etvWX#l&I!k_4GOZji$H3Pc45ALVZC-d!bGD znq_p8pPojUi!P zmih+?ZTs;==IVWI(bFo}MijR$b@)y)<4J|N=5nE^c7+Nt$5#t6ZB>PFX154v-M%eb zy;(g0hKT18OJUUE!3SZ3iV7@BG-Q+%=vG}jT%BAZx#T4~n0wq^RJ*uyRorAX$Z^yN zve2mnG^JZca71yw_-F>tK z_3+k@e9RQnl|M!w!mqs_s%&#@Xe#aA_Lll~wzoqNjJVwWsOsvK9v)RgM}^*bmcs9} z$Zh!agERn44i_3%Lo)H}oW8F8)P;z|dhLmHU>K7sLV(G`#{K=-w}#}4d4v4k0U@w~ ze<`juD6k32aOfct&TcmwhcyEvR-&Lkh$Ev&u61EMjMzwoP}CUqB@_irMjSa1abc}V zYcLWu1WM=dTVMdrtEWO*;%NgJ1ls}y4Ib=}Dn+yPF4_ZH#|b8iW+ofskXhPqulF)D z_V0$^RL`?a0T27{ZA~}Aj`{h59tsJ`I zagZF*))!M*18MHXuRGG@#wb!29hgflPUW<-!PFmVPf|_0rkoIv|1U4=zC8uMJkxHJ zE`UDE-@3*yyDNmz#uz|N3QoB%O6^A<-tmPr;+}umt6NHv3J|v5*x?5)n2*wxyOYWy zo(ADAqHA4!Ba(B@Rqq2 zhXlmxw9)k!zE~5;2Ena{FG5h0L-bEyAaKh~lsBngsXEE!b-LB!y0P@6qLLXIy5gpZxrcJM8NM?5Rz2bmpxy`@f=nobZXU9ZzE}Ttw zdESyTB!$;10AEl3h-ex3995bQom~Jc9PWSY{w=T~YCCgY=rzl|r?u zkG-v)$G>NJ`0Qa+^A@`$;6Vy`K*M8C*A8`g%cWN>Q3Wz@w@jP<+hZWYWV-Bd6*X~L zm4Q1<*cHPBe?O*DuOeJQY25?w^}&au)ERihI(yjR^6q(?^|pWR=b@stH;uZmqr0a% zn54q$F7f*50a=!}5`&TTQb}3j1v!LF(oAg6&rl`Z*QRKyN*I%b(BSGdHV0JP;1?u!D ztdbEBiwCxKCERk*i~P-dBocYKi7Mv>d+V*SiyYUHYQM`@J_k};K_e<&tI2!__DJz) zC(%E8(0HhaL}UKW`>>a~G7t#&mMqaNK%OPJ1))sVw|sLZfIeQ?$%g~Ks6+bF~KeLZ+(;j2U?wQN!!<0yYbZ5cS&?D=X;AGNk zm9E*{o9<5k{?p@c`_>cw(pxBf`FeBO^yVG54Sjixw+sNTHcS{wx%c?7H~oC9Oe`qzOX<*T9in`+%}>yL95?>z;9)r);7@6HDCd96YPHwJ!x@qof1}9{Wio zRzMhx4Hl(0fP(t#kC8`?hqXVgWsv=p;-<6dO}S$4O2nu%I!s;q(V}w?km(SuB&<(% zY5l&rP`TO$6{c|pOMbq%*YCDU3Fn=q z-EF-$#+lQ%V<}E<9-r)tx7ubdj;GA90r&`ES>0r!C#x_J4G8v^?YHrX!)2%H4?QPE zD6$Ef;hdBPEguSSVeZQbk8#H`Rs0U+3mG4@uOiP*yv;rSX@Ov@*VF0LkoU1ilLmHdF}A zlkRl>LU!{gBgg0`X^{`d|5}W8_kJUZej#BAS=auzSJPgS4t6BYvuB)vw8Jd9R&3*J zn^=I7!yMeBlG4pbzOGi|s&WY6H;TL&(X?iYU@PJ`c}LhU-fmBl*kXg4EYza!p;3S; zY)8bwTk03q_*T==~^vesXtWJnh*)xqFqmTR6F+DI9g z>_m$2L}aWo$jX=PlFt9HJ<|_=!c8uxSBb$_`A7MJUEI2A|ccH_6rXY=4bghs!U^T+IprZnBIURdfUc(nzF+k^KHdi(A4<4WTSNm!9+{^*Lau@J)v>gbz0 zf9QvRy0`w(`3!#3iG9)4^l`(KHY?{HnEjp%fE~)TAa;_0|U@^zpZk zCtcLL5|LDw0HX0+6YgOB;dm>{I?;^;fTEEeH`CW?g@^e}Vziy|bZxxPx*T*mGIHmz z1TG9hbpx#diExz}xk=so$?@PD5_y5wBOLy~mbt3vo2i%WA3m%ap~Rw3TF7JE25tnh zoYe&pCiZ@l%l?&-23lA96!6=@Vbn8ojx8VN&=++Q$&e~R$Fd_LzaK$*n@{|UkCIyT>l=gYt&QFo1VhFSYSj@yUJ!gO zxCArDn6@=^A;Y@dqb!CDZHB+(7}ET428986ODMBcNun7LMK#{|BD;BL1gFbk{!Neg zpSZDQ^sD(se17tW0}>4NoMPLG$E3G~Uo!A)K9|x~JS2rP{_@_?upsyBQJAlOBJyVO zR_!854w9t+TU_7=(5Gs{Xke>6UM}h65b?ywJ(6Oy;*t90&5D)*kG{dI-V$yQ;h7r0 zJ3M}GW+*(Xq@N_GA&I9?QmSLA1??m@$4Di|q|0(yAlB(x1t6j=BV`<6z)7iN{}Tk5 z{1%k>68wy^pa1U~S9{jy?ntB@b08&+t>C1?F`42B0UsE*GubMbt|VT}akSeg;6S6* zkqec5`a~}|x=v7NkIR`OMpZ6`vJ(Q9LJQY$Tm&${7;B%QmL1Yq9y4zG#L?@3V2+~Lx!ISlu(7OkZ%Ek2gt~YD4C|@`qXK`I=y|ZO1}TS6;64$6JjwEDwDW zpuRZgO$foLSvjTB!TyD)^%AS$tu>g+|x0P3e1y;GhBFGqBb49_Sg z10-qhi9*DYNIsz2xUi1>Vtc2-QFF1ZmmZe6r8Apt?|DVE`L>>*%(O}>xS6_K#Q&mg z;Tucp3oDScf#bi)$Zd>VCZHVB&6GSGp=!}^w7UqCIm#eEpFdex=WScZg6I}n>q;D$ zg-b%wMw_~A$B`$gG<2p$;iXe8Nq)j(D0E40H^%)`TGep z7#fjDGy7V_z*G8n^tcOR_2aj!?h?zZCb$8#b6F0$*Kf?5fxatJh=swHLWJN*8~O=2);A)hnw~Qv?!VN zf}$a)d`%s-lxD`E8yW0VYGi>CK~@rt)01%5d6g0W%d?elGwClgC49&Tpij)V);GUF zjHhr!;*Ga-x;%43kTX0aLzIpS-HAj)4~nT>C(j=k&!Q7v*BJAYe})~kAwp!qHSqAp zh4Hfh!4FiOL!OR{X3b$ias=jqMPCWM_26gIEXK{+$h{qBe$a)8ux>tyO}FLj0pzQc zOFCir5a3q(mHHEI2{n=*A`pgYDd0wc{RVkQ@8On4_e)6LYsDd8Xp2ewAT@jWNLr#1 zG-}jsE3mf@XmlmD?Td zJ)J~W)dP&s$ElII`7_g%Di@lx7W6OzHchTr-{PkXPQ^=ulAcl9C*zbLXH4$wojduP zt3RrUtq+n^VT5kg5mCjzA2fiCZ#0=NG;=s)TcFS7H+RH9*M`caVj`bwR|LP@1tlc` z^@u6!aG{1p1+2r>HQm!+jR$53D`qPx);`6V@uMnaPiv}SW7wga3KEbqXvk)1=gy6> zGe-V~GfJfe^5s+XW-H4$1=Gmk3VQ1hd%?ok&L}f&5O#2BR#&6}Dkvjl01?Lh^bNlz zC~E9%tb!|=saf@sj9D%>%c?n8W9wN;fJ`tbO^Braz;ql&QFP3ZZRZtaKO;4it1i}2 z_>u#`$%QAT&JEzZqg1!opFNFwBfaGMN9E_JlUN5omCnCLiMU=Kno?2;e(YOL# zU?Gj`!%mm*k?b;0aY{^ zT(oc>+^QxEo7_2iADHE+Eo;@eY!zr;uO}i~t`6CHOA$PfPj-#JSQNMY^T$BmCh$zF zhMx1;r7~E-5!8IWu_SIh3Ssvi7w!eQx;~4_E0^L6f^Pn*7Clyqf3Fcp32HNA0Cs+) zzn*pbOV3ooRRGyus+6G9W$?ydSaqOf!FpG(0B%wmrBHMRoy+h}?mkb1#>)Ov-QO@! zOumSjU{c2J<$W;EU;_#Bbov>t!?u4W%9pi>GzR)k1EbEL|Nld^d;7cQvR^eP`SDTk z)wUf?Ap$P^jNIv|M`oxST>Gz2vTUemnd-qAZH#`U5cG;vCfw5qAl{^&$kG(?4C98t zQ=_8-72NYq2JG*EUC2c0BeBFsN7UqR#%jb)9^5qpPK5FgSzbYDzAHd?wbAoHu7JW4u6uN8UBQV?Fm1}R znJ*pACcs>K#h{NVNL|g=rzkb+T%TFKL>dtg*jDEbt^fh0Bl9zq5(C+IHva;|=kK?R z+?NYm5*FH5cV9L>_}sZQu~>#?Ni%~Xu5rQUyp_%cQm$oN*W3pe`) za&YW{%t?Wet3?I}_~|FmTwx$y!`873sIg;W+7%(|knz+l>O&dH&4)BoTF=e=9W58j zU$>p?hh*OFQ4Zv>W_f^JEi9;TR%N))Q|(;M`nZxNf31JR=FwG5Pt(Eaa=bINp(=q9 z&xrM*^y2rXHqNCE$*QM8^;l*0-;_!TpqT2dvv)_tKIundQv>pmK~O z9Y+Pp^c^Y&bTnG^P|97?798*%$DCo>pGRx7`8;Lfa$P89wv_w$w845MF#3h z7M7O>&teU$BuDTOkG;H{941OfswP(lOfuNeatbRJkx_Myda^G%|z5 zrMjAsjt`=oFF@H!e@4%Th3aVbxT<^p8kD-uU9)m-hVz6lyBH)chVsnaH@UK*g$!h5 zX@sE2CS$DP2i-1E);XAmzgKw4uhM4O(8_;T0?ximsPI)na&>YR!1K4(%e)SZ60IHa zjM}BsjyS15oA^BXeyn|~+RoQHsX}A)x_lS_Qp(w;MAj_q8L<;0(BSH3TA`Od{N0x_$sm~Icif2hvX@GEIEMuRHcKkW zk><=A`n1;1I+cE6SJS7o0E>aYyQnLHH7^jT9^=d9-zw?MHV-22mmpqK1zP#Z&zBC* zu3BuTaKA@altf^7iR-ydn#8M6QSgTSwn>BgD{q)#b1|-?!`fmDqmf@UV%1y`W`CWq zX3^L}frwY!V&vIU9(K2_OKxQUFZ?dQAr!wL9D=Zbfj&vz7<4g69P%Rp7^`j)AVq$8 zyWfDT!P-ssW6KpAkJ+ZW_cw z;tc&gMa&;NcB8D?&UHr4ddbA}s{C4VH+UULT29=-CIWq(ppaHnR(1>!BjQ0Hw{%ZC zY=*4l;0@56CPZvPM+)c*;wGxN3)`v{_Q(K5tf@GY=q7x__$ueMP>q3)(R9+W4140S zd~=QZy7`prl2crsjZRrlu-M|&?sqDq=e5dzZmI)6Duxu{GKzI&W61yssWS96efAQd zG=}HE7o%4zJhLe@qFOZ2WtB3ep^TpLa#@}dvuwh)86+^`7Lwa{6)H7eF!DAtb&L!d z+imTTGty^`0_XpTx(CL%qU~?Mv2EM7t;R`X+h!Zvwr$&NlE$`^CXH?5o%_GNZ{{1! zvu1v0pS{mn>*h|;cVdw+6uma!mk_8q)CE+mL8PZ_X8z1cpI}pTMXyEJ;rq+0yXe;ZHyO5i z&$pn=W|V5slQz*Bu%CA8q9Qa23UO_}i4PkyN=-nEoUVJA`)`e-F0g~>Swg#9^V*7E zJE-9Kt6d6uKT~l2#?)H%5$L>RpCf|*E-A6L-5crhFEt;(q*MS>(Cg5E z=B0FRoe0TTt*_W7guirtCqX)^y2kwg=NnVyZlRn}<;1mEs$IXcw?K8-_uu(DN#J7? zV`iI>P|z*#3wUHX`RntwA4q+h`mclX~jMV)mnwbgm06QzATzJDPc6 z_$0@oX~Z$m)~9M0|EN*Dl)G-!{Ggz`Ua9UlxbTNfs=sWvS9@b{=wxaS0-ABNb8nF-% zq4Cj6a5mv>l!zrR71y6mgYA=p#AW<Cf*5RWI*n8jV$2$vLpv266xXu|Th1j;a91 zV>qqa|GUdReU|popEnwh{1Xt=G@ULuncMVWYfGm7Dnv^sYkGf=>I;@7O zNq=#a3-Xnd2Qhkjll{X<>MpXgxEPX)q=$pi0OMNbJ2@IQODvSM!+EF>fQI}-D1d&t zO;g(~f6%Wy-ROjDCV9MX`;pf?A9wM%z=XL4okf9E*e}7|7HzNy!8??|oU@)9BjiZ- z+^*cZ-dG!A=68Yx0?94pL__3hHvhE6U$(9X4Ml=G5>=)+3WkY!-5*qq{t{1lyAcfSZiYTTTq40 z+8q!kq*AKbj`el&Zj5i5VE_H-etmplsR6LQont)zXEu6Q5}muo6HjM0UktBQa9BvZ zV!g(JK{p-2dQyms+1OBR-uf9M&@>HmTS!zKWIUNeTq8S5Lq-hx#Viz^ZFTz!&~Z+V z2fhT3Je)?fLh~)#JgO}<+w~5Gk7LWEWH(Ftb6IUiK(f-->o~{lNt@95j1?VhF+$O(X_D>#Kk~1VduCP2$@?aZfsmwt*aWj zaQgk2qoo}reNPpT+@wET8)MmQDs|Kia9=1b=<>@#d&nO6TFlb_5&40T{{Q}`>Hs3WKV^^q zw=LapT5kSVWKr1I7=%wkI(;CzhWt`s5zJN;5wU=!sKLyq>->AOvFb-XXmMh^jYPF& z#-T-56x(kLG%rZ88>LDO$rrfXEx4syaVwD;kJb3Vqx&;C7>Y`HPvk(dk+y>Ur+$8q zQ`uwMngM?Ei9j683~nAaPV#I09E9M`?GfS2#RrrkkXX)AlAqR8yso9W(DR6k4qA(T z`4%Fx_~#TM{M2W2+fTNw`Y1c?vGLyYLe9XX;-n)z>R3s5sxV5ks)_N=Q3Ucyr89(v zT}xp>hW)ypygpAyUcXONPq>!PGji_s43e^?R8wJ-ol-LX7#|05{PvHVjG5+U!}v{5 z?JSFAb@wm4Aij_^07z>#4K0ARa7*|f1teA~uQ|TP3i+}sv!WAEOt8t*aV3A7q<;7e zjQrL;42jiG!96bP6ytL&&JYtrS-w;?GhY2<8+4QEa=I};e$}pt=diJBMe)KBnW&!a;5^dwJs_T7i@WI&(f5j zmc7;D<~@NCG3gAIHA}0URhXE5-qYS?#}%W#&uL`Mx`G|3+yrHl!VOnqHI|0n?aT4` z`6iex*(=_BKoIb60kP}1Kx*8R;ILOZGaI%qDKX_kaI5Rg)Pjuq+*Wiz?C*BO;`7Vs zIUFnxr*xW^m^%~*um6Psg872c1i(BqG5{VBBzUXXjw=6s_o~xAyr6+0zG{d^SUjl2 zt)OIt+BbawiaCAKjGJj1=kz@818rHE$w5bdDpA zq6Vm@r}9Y=S%|V8H1#n7&Gbdq*@y%_!(~&a-}m=YzkFDF+T8UC?;6qQxOgcfGuR&4K0aOy8unbs>}4j(wCv+p}&WGHbT?>TWZ$1}j* zoZ6fjn>pE=JqptYcE*;kDx3*jq~DtPEH7}xB>%3@sh^>o%M+)>JCe+cKU~zHE^=Nc zPQ&4G0^VCEXWL;vqV^(q4x#=n9-9$5T{*Cts&TIJ(H&@_?)zS0YM*jFFzvaVDv*RWCx4wLhzbQ~&7vkiY1( z0d!C{h=5t)xBxnNEjc}H7v-e(COS09U&KrhYWxtx1)nn?F*a)S^*Njz+b#*53erf1 z<$eup`Y0_rvp*;m`F(!Kf+I1P(dek;=d+EvI-BsB2|Md@O1zIhkAs@YK9KH}5rbQ8 z{6URPRS>&B2wNE^E_f~Qz_YN+Dl(#PXIQDiZ-kCOi>E!e0d^0UKCQ?u2vLb@TrrB$8IV0AD9d8TRqF5O?Pv+)rFpHxBhWxh0IJ}kJ$kpi3GDF}ID%#uT@ z>e0EXg3CBUq||`Qcm~b4omj*h+b>Kx6Br&@j^yQEh@pS6=>XW6=$vi;^IxqQaCH~> z&aCGd&8)cueBcH85*mihVny|hn*01npATn@Cc_4ujHY>YI7J(=Rq#Gbv*C#Wg8-P) zUSNP_Y>5Z;`l0P*L-i*2rUU;)+YC8}UP6}Wp+{z#JkavJWie0m&v%;=LX-5?L&?<1 z4zaT4s)k6~C4`w8nrIPkwoq=n^?e4-qbipC zILAUE*YI@&z^uoHj$sa%ig2>Ve6~VHUui_(-8xK|z#Hp83F7rhboes=CB5PgCcKXa zH#4}8x3hvOn4LQo(UsxIkY3B97;MQ7wRT03UQbnbR0H--DFQ2Lr9rrlce^`d zl2NNDNnFJoUJTg%X$CJn$EjHz;3-7I=gaQ1^|~g6APyyb+k#p-z{G1yY{mItz0&@s zp$6q6>=6@x+^i&0qF@K4#;)7I=H7?W^9OQ5)ZOV>$J4|i(-O0f7ZbQNo=3m@M9DEy z@c2CxQH1B*pyk+UghyNZ6ryue>Ly+4TljYe;4s>2m$NW^>RW}kL)o#PZZ~z6-ka4g zWrcG=L66MC1hyDWs-MRlqll$9aUFq^Dr8F^^71OxMV-TreF9koa`2~8DB*?M`_|(~ zoA-tC2sge%Di%edn~Um2=Fp0l5aB1f6IK|(Z_6))fz>g``My+wm%BI9-Qu{@%pX9E zkiL8p|9*47@V?~q0CGM%)Waa3L~k{j*CA_3(bS;zOo~jDLPla}o}XG-Qvm|@BXSx; zDO!4+75fw#C}J~1@9k?G&+tcbUcC&-6JE&5KQ5|haL=l-*^gzl!FY`4wp9X+Y2H36 z^r9!?T_rCOFl!9>5t#DeI5RA^ixGH8VRg=1H30XCu*MD|oh-MYg{Ca<^qX`Dqj<4YkMR z@--hw(V8hrT`$J{VeWi%svEHUCx>(g+AGcXS6ErhQ zrTv_b;crtKPIrno+oaYQW!Il3fvA5%daBG9V@R$uz8P>km^8F^SM2JiJx0X+FXo88 z?DPS4j`R@hAfME4)o2xFF7%-UQBP?nF!t6V!Brr8h2Pv*qpTZY~%ygG8#Nl;8BQZ9fG zp;huMmmD+Bh1?Ico%~}q!%3WfnF=rYwM$FjGI<2q7c~^ zq5%NWbc4nPn`QCINSz%rAjr?Txw;Gv1?446dS z!~VW2@{0V{dQ*j)++XFX69z5=9Tl#0fdk%U-(8Y?oLw^?E+EEa%|q0a?Z2M!NJ_T7 zSNhFl8H(&KmnFRkgWP!zHIi!`j~_x=k)%n_mLon5^2CpbixWMI^qypdDtzh$h0d8D zc%L$456I`1VW|bX9YmGC(%jk>Jn)IModqb-VjRi|YF@(+DxV@k#1gtBB~u7%)fabP zff6N^)mr+ioG=H76_Dmm1wS<1gBf%dGv2uN*M2sUbHFmP^-v3~r+7QqvFzOR32i3n zE+iOwB>L?&2iM;h0NzH!_L-JS>=u`2YCt?Ki!K!Ccmopw;?d<2pJ@@DrUl=jHM25G zvI~Rpsj9`4$ex^3W!-OmMCSds_yhDS`p<{%fNs_hV0(9BIsx*D`c~2Ll-&GVy;jB@ z8&|@;$8(3R>CUEx*JcORjd3Nl*RS!AWUz+K^C|1hCbW{kfdxT4+0>;3f{nTjbnTn_ z_Ypd6z72w8`X4%sbrZvswKD^Wd^{Vu^66dGCx)UQu@=o;RhZ(&TA9GVA2{{_z@wqAri{EnS!Foe?H< zB_NZf!dFNnQg~zbCSVX2B7*nSBE+S+guyr(6+0eyFfcRjbIev)PQG1MUHFD@ehmu|#~2FNGhTP@3^T}J1;d#JS!g|kpq#@g>SGvelk)7gx7zRK0YbcOc|PBzHI8A2d`0E{ya=B}2lS#-Ubyuc7P|-%+p9c9-+rHA7^~(n zEWw~bq^z_*Yi^j&%-f4**aI}k2=j$H%^kfIg9K|)f`gdGPmiXq&ME>2Hp`?Q+oMilRH3qoSuhc63=XAIxWU;QyA=QT;wy9p4 zD_%R3o_FJRYwAzUl+iGoR`iUTWvx1?b3`+F9x#G#bvAIW&V~M|Nr|6?=ulZ_;xPSR zJoo_!v&$L1=bzrnU2mw}SCaT|ziGowIrWAZ9X%W(`<5@-$8)`S{T{Ye-e!LEk1B2X zi_7~mc`C;O&CAmyuweU}?)LmP|Hg(bj1-vs9_#=#hHgP!v_ zT@Pvo?D=mk$ogew0x-+!3eo((9$MyU_pa?E$fU+lL!Eht6F1I*TbfBfh3};9$Pd$3 zk#CTBeKOwhbo{rM`n1Iek>i9;Kel3g+@`GG!!3AjRoa8+{Fjg2v z94NM_SnGZRt8_C8EI^s*tb{G-rdj1yW;@Q1e4s7Gf1#)?V>u^rL0UB3S(~ilk89!& z`-7I)bAJ|ueG09bJ`3ZGll~w@?P?h58U`L1h}MYkJC|jMr$Q+2P(PL|1l^fY+R!#^ z=bIlb`OBpW`~jb^jlb+pIk)&S>=1Yc$9Ql$KL=k1bSgr5P4oXjTVPOf8^N}*EB znQ&?9(yzvEp@|qY!2YKR#QCLZ3ec1p=K^dqvA{PX8ioy?q5v7M!d5@@LqPFk7l%1dF3Ro2M#SkDx!SYZii-_0 zErj*8e9{&I`vWSR_b?#(tzY0q``qeCA97#6w|>x(Ng``G<+Z(le9yu$-k0j|jf($n zSHU*CPqP?PAIQ+3yA-QF3SToQ;g%BX;8LrMCxTt#4tX>iW07CbHKo*Fy2D;?f(2ye zIft%oRLaN5Puc&o@X#*UK1v;RQslX|^dIOio-a@{0H_@$J`2bv>svLF?iN9(Sjc(O z3GgC(4mHhpV5DiN(qXu=BzH2ksMJt8tr^25MG5Ub(@M?XvNSNKz&?&)j(wZK0aOmW zpZy$z)CbBR;{~qx-{GJ1HC51sm+l&qdO0_$xE(f1IdzGPv{S0oO|!i+&eS^BUc*E= zmN@4!|A@}^LkPzfd1HpNf>AaaimS>HGkpwR=1^Y75Ez!0A{=M_xjjvXXT_*STsO2wH0m%qOBAugpz{>(LpA13u)g|Nl+}#%Yfy3L zi_!_$bbjdr9gx|DRLi)H32UJSt^$yrf_gh*5&nDO4`Dy#6==r6IfhvjxjuR|e<2-* zfFj|d78uH@esgZ4a>_VrJuBMc4x?gxC< zkAWzC#CcS{5Vk`b`?3*-h*p$UyCP81-0Tu7uNWQAPb0DcW=T4@ZE1`J0X$u{y}DJPqO#L8Q5WhqO`&AHuCVWh*6JMz&#v2^@&@q%TJO{)LeUh z*9?{C4L3e%GLp#tt{&>K8h9CNRl1*D_cYqty*#PWB{Y_xr$2{K{d#p)18>XXNkp2} z?1nEO(MqT;j!S2Sd%;VS7t|R&NK&%Nc@~>W8fep<2)mbw7W}S(4moupB+zhb7T$(Q zp~WhVgFoG>eA4C63FB9BPK^`*UN`hN0B^~ALpJvl|9^og@@NPj&Iy^z@rws=FJD*YqJfS^Bdr~j9QhbbWd$DfmeV4Gus~c}Av%UojT&>|` zQG8IX#|MsctG!n4ak43LXU+}9cVn_|K+Efg*@(-Rg;0A@ahAi96Av}K`TeLbhvof9tLYq0afxF> zrkaAiCSCBEERo+xx*(MsXH@swQTogR%Tm1u%wwyJ(6Z<^75s%+%l7nxfc120bivQ) zZwiC)l79()){;hTrAO&*l=!GPrVPNCr@$ObFKDC=**@$HaqXoX*p^ewnPThlvc=wn z5VpGi-ZP8cp2?Np4TjJXYW0WyUo=X5xmg0-pcBQ@{`2}**U-;RU(xKRQ?rf)@~tII(`FEjgQdl` z`rDLvm>sUSF#JqXv!^q(j6W=jaN-TtAWH^S+sNAfqF4df4Qk<64y@@bKk4srhb7JU z6o-V>iC^Kl#`QD>%nYByuu1F~<-$ZBc`anL7@DRTQX(B$unt6Ce#YATin25VkgKB|9%{%ZwHB9~Y!vu45+ z)+gb&(tgx+sE?CO6r_b;+4{dUl=?EX0vIxZMOOdkkhYSu?b8gS{_|iBZY6?D zv-{Sn?3cS3s--TVS1kj*x*kg-kQmV^8Bdx{5jz3G7BUA1CGC-`mV3A!v6G+D&!vTf zL$el*b#&>ZFfSCMluPSnT|rUtpIHvSxdOJTb40!?wuBXK$i>M7R}G{4#v7)a3_8Gc zT$OW3Nw#{-HNw_ONrJT%7HNwx43mR~R$Gkp5 zgt*?rU=}WZNA}{?^vmPAjbbPT3!VbZ33eS0rO?L+OtYZC&taSxzKeM)&^Fic9K9-; zObAmuN_g%%3{q*!C^E$jm2Bxg9TRRUv#003_O`*ELP_-~UN5x0YDPE;cS)sQA*dZr z=^PrQD!?sdl$d`1U;N2^`B?+}XhHS=uOnPjaefrRt{q8zyzx`Hv@cqMmOI=jsbtb| zGH|b!Q=6*C0jG>)(~Ri5toWE%gl|P%S#r|p64`DkI0BV;|BAbM#!^_dGsf~o=|`QU zgcPjs0@GD+J&7ng9^C?MB;^ji{bW1$0vx z`ShVVv&j17rtM#sj)6!(Jugx%r>*9uJ#m`6k+-^=VQa=Yvf524wmu z)Q+ML<_16xCI%&_&H%f_5G+^LgMPCJOABNqkJgSn3X1EHpT~2lf9KI>bsi9g)WEUf z#bRvZM$?<(pyi&pJg3v3-8uk9zxI=2Gnj>p_TAS#unFw+E86c2V@h{x9$PG&8?nE; zBnO`MOk+=++6iCrg*m~NtNG;Rwl;#>B5?j*RsF35u^tY^&wKgrq&yw?)LG^#A&G9o zf{6ZXjjQtY6j~<(peK*mGuP~y3TFhXs4FV(#T%R8`6k}ZN@3m69+xURmpzr?bs9^Q z8#v{CBhw$^{CtdPcgIgJ=` zf9{c_TPE|dUeY>X)!q|=BlR|?QSphd!rJH2B;P1O&e`;y|9vJrw9uh4(kxk-S7)zV z)Pwfa&o-8Do<^5Oe|nz62b0QmENR=9A34w#6)zs=YQ}}E`eKEL0qz=k(Tpkd)uKVe zeB&@zP{0V$m3Ld?Qk*POmrJr4X-#g!n(>h?qSI2I@YP(qi?}072l84&OHQjCb&CtP zqix0O?p`}zA_LzIm|}%mpnUP1(0KrHEr_*Ll3)1B@9lsBD~m|A1j9u@xHpGsKyKGj z&qrz0{oP%Kg{1jRJ^x@De@zav+a&yV;PKhvp>52zOr{#`)8y-jjOnL&*#wt`tB%$9 zdjONbuQe>#iquJBJs!%Xdafbn*1fpIq@*yyM}OrE`1K03cI~?1MIGMhqWDG?l^A^P zcUxhNWBHvKJydJl2wATok&W15qscP3Z%Dv9|7&Hg`c)}gK&2X@)ByYWCT~qgCY5{P zv=hf-(W{+SXW+G9)#hbAM8oK&xd(>!mgy`zs+S}leskhTI4R}+N=ilZh4Ku)EnM^t zv$rBf7*q=a5Y^F=^D4N8PS$XxKm;>k4>&wMS`^N@7#Hx;YO@mT>yhp%s=`j8@ISn5 zXYn=yx{7F;s$nsg(IS9qSayJIIT23_I&Z#FHdgJnzV+}B;E4P|Cf1ZBSlqT!gvcptgRr2)-j4i)O5(uRRY)V{@w9t8_dJhy5>TDOqQMeM=e)sVHIxO z0fD#?iLiUI3OymQ>+`+Q63Ifl-2#e9Ea@+{VT?IY7ZC($-a(;w_ci)&YQYbcddrsd zfkPr><12mfwj=73R}sml#}|v=#9_(pLv6~emfkPxv}+u{jql><4;Ej1Hza`99wAQa zDDI$d33LeZC+TBWbVXmX>}b}iY;q=3Ey{OL5V=>>-9Bj=dH-!pXngtG0sL_XPXH}| zk>4t+Wd^YWd9>aOWJ3?=E`383vaLcK9r^Z5baY$TA8*&spq)O-MHhh8NORZ8yf)XP z6fQSYuEZAfW1zbDMbaHvgahd-F*gxN#2{USqvU=(w@=W;>$g#`|1Q3%{5IUPB>t8T zONZ?4N@^h|iwt~z=a2i*6#Z+|+5rDD{ZXjS<8;tpt)P#-E%~mK9UHr{{`p)KQ@2M9 zuIJ&qK>S4aLlo}e(%`%p92xJcs+kM%1rQ{!?{hy%KBpo!Le;0CPst7eWUT?oya(b; zn9-&zv1F)Rf|Bn9WHJeJ#PRW~a5Y*!JL@9^6F$TRvAo=0M zfe{@A|AO-BpJ8%i+0>L{)FEPUCk69FwO}T%*D*QDL9z_V;slzny=9&DQce(gZ|MbN zaPA>crj75&B?b>VTc7leS&9_HV+am&m)#C9rf-xOuiU~19G_!4q{Clpf8~{BEo;DQ zSp4$=6U@imr$O&F%KC>Ik;S9H&}@$wT}msRT+5&gcZy*8cRFo>z5|L}nSXRZCSP=p z0J=*&3BVglSZ`JAw2+CAz{7-m>?!ff{K3l!a-G7Wy~anAXen@YuT!8C%pjf%e9lNF z#h}yToUob8ET}(x?eE1}AwfiKk?<2yMedu``X$ENNctJjvp@qO zs5aC{evI8y_|rj{TUJo?(w~a&aCxm)p5WuBs^8;UE(%&Z{_qS-3LD9Dl~`l>4%_g{ zk3iZcEIYa=NcfD{+ukMbr%9^HIWUPn!U4nxH=c!&)XI9?MRo;eqKsQ^A6n+hLjPr_ zKf^q;K~)c30VDpsG9ADBpq3_0J~)kIx)>qvKeJ!vUuI4KGt#ODz|s%nt&)Gaevmzq z%&B$Y;>OFGrL!q3NEufDeisXq=6HY)(2z24cy@F)_G+Vq`3K3yOR&Y{+~<~F#c(;M zt&u4Or~>foXJr{)8iiyL{Iy8ueNK;GE>{qW)crfHVX$nL^C|Lj;g!} z%9Fp`Df~sa#27`XfM{%MVd9KA++p$Ssg?g8Z0LUjJ;M8fo@Ci$r_+kHa;G9vh-;_C2jpG<`E3oSR z#N%!Qc*$|+P~f#*SZw%3BcZ%KSikrQ7768J1*3gYGbU-1^IO%NU5NHnPW&fr?lUUx z$V9$rxcP8@uxJiy2KMnikH^Bw8R0SM<`jdIxaJl5E_DW3Y(bqY=ReFXF zLMs#f9ZxTStKLW0_w;%Yqr6eoQjxv?#o$d44p+ls=RQ!j$K5Q>VvsApWjz;$SELb( z^yyI%GPTPmqEVant3MMIC`9LAilx&uz3KEy6@K3r9y8-jZO9M0+F9t zZ^9+ms{fyQ3l>@C#x~HUEPev}Ks-YFMOyU1)xYbo>rZ8NbmJ2(wexlZSXwc#9l>aV zgUceqlNsSC=IEojAUFEXt=;U(3#G+4o2xDPy2(N*vrLM442>q%{TxT^8W~d?`#DH_ zkS#TKTTDeu9KE^T%<=fA47T!Qg!;wRki9<%@r23IB_y*h2W94PJ<-wQ2o=SE@-+(@?*X0}*Zm3fFH~m~);-xaRkMr4^z6=eP%s>)cXg z{vXdtRSK1jz!c8tP#TMG9PgrfvRS_Iw|>7OfRgYZDQ@!dW;-&LLGGLsw})22z*CDv z*fK4kia=yC>W0p~bylbs(031JNBXb1Yr;r|W~=6CH2fV2ySG*OA>L9K z$HaNl&P%4`(X3e0%&TdFvmxJU3bl!_EDH0JNL^vp5{IR*)_a8Lr(av5IqV=|g@CJm zUW6v9BYx#T_t8p1;%5pkI#N7U1T6?B#}_th>W&@B!MBQd)A#sGmSrxdee9p?X1 z@5dM34*;G+qYDbi=Qlv=t+sGA?TA;#b<2T}Nb+wBPwJ2AI~c#6=5b80(v0Xrq0B?N zL*z9rs>`BhR6Z_=Tyx_}%+F!w8sp46nfC;_x%K=W_*`{UZ!l+2qp*jAW!|IbUL~v^`RZOCkN)TWkt=*yqOq z5R{hR1mEUgQIN;*O+z@&YOQL^GdzfUgQ|2(jK9Z{pP)ECLAAeuKMu_qS8xoUG^YjW z|Kt(WI}n9i=MO^*%H8hnZAzQ{%3OLA7Zh9d75?xen-`TG!aSmk$AykR#2hQJwwsKd zE7#mURif@IL&i9Y;o(2k1DTV$!()sqHW96}<&YeDh9e21k$&~ROCg(vlxkyf$WboS zuM_u{y0wXF`B3MX({mFnv50MMF_vKWQzy*)N9^bEMf?*$+`H$A{C~?p6Kn36yJUzJ z!qv%P;ryE2>gK$TjYp_%EFgOM$TKU|OxxPQpm z_p<)!3Rv*#Bxw*2VYjR_qz^*l>UwoMfVoo^p&uYJ#p%bb;EAO0QrAD@uV7oQt|Z*->Z9ON_at*PW!LJPeTZ@8!q$dW+X z&p)E{krO(6kxmV_WZ1B&TbXR|Hg4L{XauF(WLi#D!Klr~DKeC=?~Is!!s>a_5CvrJ zN2W}X85$Hd+rCEO2+&3Xhk`GP37jOa@u;8dhba<$!dfV2FssZ>!c1}U_T^PcSyPtL zZ?on~3u!_9*9&z&xr}CqP%97nrrLw|=D6FsfLee+5{0s0Xbn1+sPC&2w*9iRNn6qv zL{l_JTjF_Ol}Ru;4;$eRmMv9h6JSfu6HMp&HV;S``v%*x7_GH(JUlVK6WzQH?Lrp? zZt_KQI!lN$7klerTK?D}R{qHg;TWKB_?u@}QMhGx>+RDPn9uTL*-VIxXGCA+_zU0l5)cd&sAMa z!Eqwt9$WohM3;`hfz3zDPtvk&yj@KppujW!$SS9^^nGK^|DrkIt9I^y+A$gzEC1(- zVN4CUL@r5;E^r`gQJ$jy#yB`~CSxvmLPknA7V+z{&txFJg{G}&%-{0hX!Q~Y=|qb6 z2iBc(-BKrOm+KbgI(a!vqF`emgx{tFLYcU=z#GSNH5uiKudjkM%#rfr8(}IQ@hw8- zj$O`pMn-4F{(~BhbJAtl94v?W9$EZUy$zyYIe@(4w3gE$}|R{3>S`0 znj%%UNU?S9g>b}z4l%-U8=P5+DSd%fi>8Ck)4ka=65GG(R4MV_qALQn4+rgnCz|BO zaI3Lf+b2rZ94&}ck;#rTjg5toq%4K4C&Sb1_1C%*vTbZ{b!b!NwhLl8I9LjjuzWQm zmxHAb_;*ho{DtHJKmuW8Bm{nX0|7f7Atv4PIladk+jt+tZ=5?rkNjS^qEYL{P$T$V z`GGn&4H0Kh=Ng^Ct|yGOd6W5@Ly};^gts@DtpJ8Ms#++9g3vO@4f57J90VfG29>S= z!N&M_r>$^-ng?&*EA?wp!GRfO7ska{1 z?F;#|4~luul<>%)6)`d){u}LB5!w0oBCp0xcXPL$LVa4>yM0RI!|y%;#_?L+%z=Zrga~1gI4Fr4NWA5E8#F64S0_HE*>Q=?2!1s|@HH_}A zzR|sdE`o?f;Rf>lP75OWs5@*Ge)I0vLL@cg*s@9n*Iq^MH8eQob*gZ?jz=VGOic=; z!)B=L{{2BO)aE1%NLcehPVe-{JNn^go;^kf37=oDsP`n<2*i;~&{VR_*NE~A;pAum zNRo7U)rBu&NeNYXHq6Z5jG-ef8USrBw1sc(3x~)rGb&4wY-#nH&I_DOx5|Xics{IiWfiyU+Jj|_~Q+P;YWHPXU)kkO(M|;3kGaozoS=W zySR~ufJwZqx*y{8Lx}<7B{_%o8Op2N?a3aLBwaJaZI_=l&=wXa$fDdNJ^M=AvhF%QdEg$PU{ zfQxAi*SYYf{$RYTkrh{ASZr2%-AMcU$%T!(djui3L?XAUBM&)RCg>0lSHyPsS#5RW z=FThIFt7tPMb>uC{q14Pf8CtnE42}t`WH0uUozeRnV^mXK#v5#S_?}Q_Iu8IY}mOY z@$K@wcMUm-*WR&yF<%I#GsF$#0v4~kkwP#bchQ4>X!^*DkuG9l!Y7`D*269cSb{b4 z;kh3z1xnj9oUm9&D*~*hAd!QQy)R+pIdt7*9a!dcpv?^MUC4`tBqL)oHyJnqEZ;2; z%htFQXW(hS%Nd`(9lhDqm zE@7uy4qeOS>PUWcY&TJG$+9kW*oW)8#16m%k;2}t9kJG3E>Ew3t?Bv~Gf7`WJ^-RS zI(ku%Ptdo@f*BrxGfWGe5zqCV1vthZ#gxxOY`-J;*`Dk0)`f$x|AY$TjlP9}JVc7X zysAeFU^ABXJYHZkD;$y1_+htCf8L9f zn6)*NB7#(S4=%u@;Nkh8vh_q$3`vasr&k=C;*Sd#WyMr zF*l9zkH;_Vi^mth1H1o?6690&t%)L<#WA{L5=FaWkd&l?beKmuh!00N+LEmgK~#|m zlbZ~h+hQiAC4ub;smdM_3OoC<;hEgQ zG-Kh)2SgOvW@f+r(tbX!*Kyab)tLq#eeD3~^3gZM#FGaMo4-Z2fYPasek0oe6ya1^GZqzjv@EfWb_R!8rzc38w^$k;$K*s#iP(CUiAd-4#G{E zFd9i!tRTlv1YV~OkH~i8okGf!LSqX8>Q+M-lA))Igw`8b8=cVt5=yFhMa1?I9{Cjp zMAc+t3Nr%e4P5s(u&Bv=Kl-OE$dzCGj}&Gx8;y_50R5hEjyrIWN|E(lQn% z4x=cGZ)QYB3kg~A{!s+wlV*2?x*&!j)TrhX6>GCe4A+z2Ur*l7Pb&i%oCTkI1B7ND zYwty5|G^7oeZl(y;Cre4fPrt`Dn_Nr+V&5s$g$*R!nE@>Lqt>K1VQ!Ve=&r*s~60+ zq$sbgxLWA7aH5_VSSJ`YV5=45f`F6bnJcv=4;YrEswke)+;yDiE!M=QhBpC@pbr9l zL2lGGS*Z((*{Y#U&nsrp+bq!zH=|iG9;~w_TUqZ;xbtypA_)-TkvGQKSy-nV*x5VJ z#~!A`%^2@EeGnz@61>};Tv}CK@&C@(iv(Ta-We?Xu$zSqka*!1*0gmwEK}EK`x&2O zbIKdjMWv@j**?xAFpk9*T#IffmJmXj#m#@8`yrXy{X$@J?qeAewv!O8&vCcaPy9!8 z;hzRj-j{|yK!X+pFjMHW`OQ=MP=D4wxE~SIDkZz*O>x0l{7zB0hH2=Kt0w&*n+`c3 zFu$y^7JpD<2EIhH^Y^_TOpZQZRh|6?dsXJaElxz{@OX> zmN*}z6AZjdEACjG-uF>@LmNDhZ&>p)f25rBEvGRV)v%^h^-)-5Qs2ZCxvJY(_y7D%HMVIyOceZ|C|5 z$av>RZ~Ubd+OCGHbNZCciQP~`Dl6;en7avT%%~aM_yA(2P5%KdZ06p8&9H&`7jZw+ ze|oGdbd?RZ9|Ng@b=)2SlfR>{;8>48n59B+{zjg-e2l+CC2;pZFzfV|{(*eLoRz+o zWf88nYc&HEd`-Z4hE~~Qul<#w5Vh7!1>T4gTOR`n+x)$0;n+8!odHAkC+ zvM1)lAL;62Nh2k<>1gDqwhED3LQ7F&s=t_lUMV${TVFWJ5N&lF7Hly+`f0H2wa=yv zT{6;lKk}Oz8LeV)IxX%`iq=SskSntNyx^j9h#9=x_-<-qxn$dS<_7;j96>kL6kfRT z0@1Q*R=6QXcArxYqyGG0m1q`?&q>9feiW`o!nUBFdHC)%3i~@&p*pdCo2ivK{Gyts zE!qhT?S?ChR6BB3ERv0>CU${sY#Mh-xbb1gHwWRWLWcxeqJo&_4bK9%8261g)rtDY zwxY9-dKZkCxXm8W&3=kk)=4X`asKJy|Vq-!m?1x7G5pr6E|KW{iT@!RHH!qPPsQhV2)yIjDII&OYs?dmGx+% zhEyX^!=JLKWTz?!!>^ov#$62)6YI<0$w;~+L z&tHSxt@Hq}-dFTQF%h7pTl%b}}=Kiz*8iXy&9QBdg5?+icI=LDoW zDKmns@U`>7Da+~!X+5FP-nE=@p6|l{R9RIQ>>J7iPB%cVMyS4RbyC(Z40B%i~Ng6giVJ!9q14(snhllz4 z-J`U-ItWtGCP<$^0r%VDyTUfFUq;=9Pjegx=le)zaW(qW`?TQ7U8P}SsDE31hx`Hz z^mIuz&q8iDL#rK~p-+3U__Q__snQ0K5um}Cx6L9=Wu;{scBSmiia)3q*!F*GCRF)V zvmijtqT^F>K|bT(JUN|Jx)q>+IchA9gTwy_Zg#PWdKH>Y#rB-&s&lF9Z;+jRAdev2 zRkhuo3*CbSa-bW7m0>XR9A>j7!%oS) zpT>W}%+3<5n8V|R?I#>?Xs=6&FH&(Od;AsAK5&9TAa!e9x0J@d2f+kVOh{0NtKY$z zhd%6lb_*R$_Ya}(l?yjPflH&yB+z0Vfhzyg)*pTDRt&NTzrc3IWVvCTjV3 zSQSBG>eWm;V2XI40)0K)m4bLWKF9^DQD6?VgTPFz#q5@FvCW?A`DlS?PMO{c<96GzzWI0uRJtwG}n}h^f!b4T-W)lSNN}!nj>~pgOV1I_$nc>#SxOj z3v(9nL4{3D#|qCLdi)B%8KrsseS8lMGP-zse2<_e2pTx8qEl}{=u^H|&1ym4{}v0V z_N!RIfMW4YZTtfH6n^uBO&6zc{Y#sqtVLTyPx5wn_K%^yC%6#5$28nM;ILSahU=RIb~j9C2man|fF#LT zxUH^sE%@V&jcqt?w$fZlt~$^jf<9WU}?Q+B=hjYsF@-I(u2TVRGbFIAqa5 zku(9Lx|}G5E`5wKKWA2-0-pgw;z#1XuG0|fhrg=kcmeL0+3E&s#NiNv&hb2XT;BWq zd*5ZQ5|{OS@O0k)5#TPcY- zK`UJIDQG84?YiKPHHzSkXOFk2n7k9c`m-h_Ns@@5)L}%2%`{J8c<&QsOr{w?Ux8__ zd#wrsRjFl|q=}_-Q^wx+jlbFYy_DoT9vg>?R39JT1J?ufS^KsKncT&-Kl)O}ZcBKn;d( z+zy_k{@`DFw|ss7P{8*m;8p?j@IrfAb$d;d&1}fMuRk!Y8Rt)y5K9VyvJ#3FvMe5v zN-0%c1qY448>$CAS`+z^-X$CSn{z#|fN58E^w&z=PGv~e?Ub0fHYrn+FuD%>He6s4 z3TM1#U7r8Vx9tk4d+1vCaf|UtivLH{Juv3AbPE8EZCj0PHnwfEQ5)N~ZQHidxM|SX zwypb~^PS%Og=eo>vu5_pno8j)ykd=!Nc(_=-O30R|I8Vh?~7sM2$+(`vkwRr{^`cg zKp|+1iwNDhD;u>L!Iji73KRrQ(=~5eMigsRLOKP&c#=p8wnubl?pA{z%D~86iIup? zP~hVD!cyU{CKm0^!m?ZuHYz^?hI^dNu~PzVZEou+;FPt};yhmM%Y|Wx>-KvrpDESZ zzVH%I0iNxZj%>;mG`;@7z5+UYD*2J%=GgBO3ZAu=W&gL;aQmlMD8TF5Y8o5(@^z(U zDc-4di3LuG)=+%I@7eUFHuM|uJ>>v*e+=k?P@cpetn>A3kL5B%e5#$BL0Hm?pzZ2X z?hk7)6KLrkLz*LeR&GHV0IcKi`uZ`crNHQt(ZDKrIJ6$Fsbpl!KU`K2|!hQQ5Cgjod< zB6(8OBRcmB&;SGTrz?>1OkM zh%;~bLpthXv#lrZDu<{JQU^|?+=MCj;E|$AIHLl2-Olaa1|1d%NHQ2%voY#Z{RBW{ zD1#9pLtulGEEL%H}J$ChV+l&g@_`}Zc8oZru~;$`aX5S0XjDg zguwqir8!KT5saZNdfg;YLc4f+XMZ@m2a0sS9dlv1`&jy0l@brB42aoC=3Pd9YRPig zB(u{rX4X{Es*T0cg$I*|xjMckYT5W;yKYP*?7_2{3w<#s^CKm5+zh4hr;5|48NGjt zq$*c+#Ee@Cl6I@Jg_%<*t_8N*9lM;ll1cISrl8_7k4K1T^N=Z`35{Z_41w^60tjti^QFxIV^tA{f&XFzJc)dcb(pWE6mk{aLAw}xwr35^1RB1AqDzQxIocq>D zW&!kIw2xiTFh?WrZ%d7#Pp1fg6F4IQKGGyH&lh zT%EecBb#NmkEKmR#7I21YsX{waJ_bcZjN{OQ<0wt0%vF4k1|%!BAWpu2=XS;M@|t^ zE+{Z%z{5p;G}8f1-z8CdC(}xj_7S##FBsj|>{y(+m2^xZAWqWKa@tWZ1eZvJ(G6H! zBromo%gw-*7~Gb3bAX-ls`jztej=Ec8PY*F$)vm*@UEeg&TQ-1$MaA5?|@?C{fR_P ztb*rt*Ofm)@VV--6L*ezw{I$y>)^yE4?CyR@iRq@LMO;T(euu1`tORc!04w|BtVOX zM2YU7TZ9Z++-^Q=I$2`DK6mQDv^72v1~vnLH+ygU?;dU@jO1gV5sxghBz@kP(08@d zTKUT9c6Ft0SOZ%itYGkX(m?2O7}32~vYXunSuH`QCML(zK&yZ+36N6b&TUwrJIg%7 z%fqZvcOoZ4a**m+NCy<`o!~nP>Ilv#St;$yR@(H}2Z%^vAPpxyx{#OgD5KFrM&jV3 ztD0?pZ-4t%>ph^c^8qGk_=47zbCU$KhPzZ^-OAegY=xD(J?oy-e>Tq505@o9Ou2B0 z;p?j;(}hg4B_Loq*{6704`$%fB1_+y`B2K-PHp0Vg2 z7?O6|T2VKq)c1wibg(+b5a@9+xpiM{5?HOA=sS_D(rXcV38VR&!}p6i^A99B(#v7L zp-YpA32y7=KhVe?4g|{|5HAf)P`|(YSS=&!E|R717i{aPArI69TEP#NNBjws-=&4W zsN-M#M+CgQPmhX)8YJ0!zgLZ#LHMo+wWsHo_vq#VDsjTPS`r5q%$H)ZW;F31vWz#d z9Ca+Nwax(;9n24Ee36%5JQlEqMsvaLZae{3-&!1= zrOc{bCN3WHrB+3YLfo9=?&DDKVz&bg??%|CK#dv(m+4i*Gil0q&AcU$ekeJy z2>HM7{Wl_er$5o60cdm9?)d*40~)v>JZ(0ne#bgg=Wd7@#_87o^X>Otu_$Y&Qyerj zNpqp!$_NTZcroI-9K2H|cap#2J%=`8a_NmxefW4nl4K5rW)M*d(@8H4TDE8oo_s^v z5zICBh;nJUSUhfNh0Pz^qq()gZ}>V4MbRsmp79!I^@(nJhK71;rO5`)mt;Tl&e65S zg?{+4Sp~p%3O0;rPhZyH;QBsvDpr$CXj;wQ*C&wWOu|pKKeOa4lgCe~$s_@Tt`u@|6bpFPtO(RQ4g|Z-|I{8tBM2;>A*l!&7mJb4x2Wed}i;;v( zWkd_J0-Xv36cg0eZjx2$0@WNAFIKpxn0K;Lhbs0~uK z3&EE~HP6Ifr87*Z62Ay;T|J+742Y6Qnta%a-rbnbp{R00Q%=$HF!?I8yH9FM4QX&p=2 zuT#|twJ3_-f_mmFQtRV_NB@ZlEPjf`0>nBS;?e$T5-|u{qF9Eu081l`h5k^P!p1^@ zR=k=QlJ-m~l8#bs=gYB#Bqm`vAo{rtu{rmaV1}##o7%p#2GNOGsflCDj=Do+z<5pT11? zu5|M2Q;GB#Hh|VZZO>BB2)Fn zbx^=H5r|2?e$V<`xO~z@7EpQJ#ARPyFK%a3m24U%M$nbD@IRrk*P}8dTyJug70d}v zsz3dTm|xn3%^Va(YIP?@6TfO|!imEBt|foFl7QO14`z9ou1A(`9=WPQE8w9qhvNA%kz*&`Cg&F{w>}bK###HJ#$z~n7(FJDW#d^t&`!`b>)ZikKEF%uTAYL14S0)I zJJt@=ptY+mNnPXp6tt&3_XFIc6G@Mh914GI&In@7xcyEvBgQhDiN8(cn z2&sTD5oMPeIA08TkD-(d~TVB>?ay zW5WO&M_*nCEj*KFa}#+53Q3FTIlx9{`!-(lxK|l*TL!~TXl>}qT{?xLNjrf0<$LX* zLOXZFPgy)hd#5~q*)pK`+^_MJc_ez#Js`d>uqrSmAsmvxIw4D)g?#Z8;>5Z&HSad7 z1{|-+-Kp9~*~?yXuRkOX=e1Y6rNi*szzy0OP4khkEi>?zA?UBZwr}%?QRHJPyJu5m z2)*0msE2K)u>JIlz6|c~kA71-Nmne#8UI=_;jcvI z{UjDJ^^zs<4U0+#rYmcWa}AOc2&0h|i(u4r&eT_a^*~baf3x1fCto6f&m0};@*lo| zSz6r%M}H65J;P3IM9Y&w-nX%|vSRB@2kDx^NWNo@HHq*gcTk zKd#63UJC!*O2DX)`wk@B3pih7f#&hUbzCT>qd$K)2~Wpqi8{BvW_*IEyQf#`&)sGk#RZCj5chc{XG{>P3w0P%0hTHT#3-}r)!CQ z9rlfItKu4zhX?^pTUq@pRp4X+#JPY4)7$b+)QO{J1_sse9{QV?%Lsg(8MjI@zs5Ll@}elP?LtXX}K70rG(kh`!$w*Y_kY zdr-D}uK!Cgmx0EtNIY)M47Tasr`Vkhcpm z=F|)pLQl1>zjcr;iEy#WD>i4_>3aIu>`G&2z?GV})rkWgi&0TS=rP)gbkHkPmB$Mw zQ0I0A)GA6H_?3_QA0XZYnhz5MnSK}Wh9+>HtulHsgWpSsyw#Y^l(VPdM_tc!z>*WS z=tZdN$e#}}?bp&^uV)sM9j^B~dEh8i^8u2)}?^n_VUH8#N6GEzOvTRtGJJvxqa;pUvH95y_@~T;U$^!8yk*jn7uf z8&n@O2SIlFZw-3>36=~1^N+d)GmCybKN*n4vgETQH#iamLP4TxgKMwUfsx6U$l{rlTHk&I` zn$hGNR(--&_2G-m$oS|Ml~u4)FGYU(s`2$QTBKacv)8JC_n$!Nk3Cc+G`ezs)5Q_g zD`K;9bW19NFXP!nr-SsiTXwOs2{atmp{G})5=jiKLNm>%5S?yU|B3lte~P65#NIeN zEI>Z+0VA}kMem9K(AG%YuEmqRB~bagf{#lc`wVhTWYQ@$udP_tuoGr&nDwUVM<}WD zZ3xed>y8>MIo=4Bxd!OsYbvrOmX;qe)ZG@Nym%&-F#F%THkc3`mDq-g=~}8yvOX6 z<$}n`m&A7?1i#bJ%u5lZ$d&19`7^1B!Hmepl#*=XdXXbPjBIL3&{U`c^^ubz-R#PM zd5B_>9xH7aG0hV=oQA3&VhjatD9dWVG;{xj6S)6`O9jA1T{OLbd|=x58J4Q6hT1Plb&Yanom1D=SOH0;R}5(Vn9t7CRT2 zn*0KaVKo2CO%Np*pUayd(S@rLNgP8I@d2iEFH@6I7!#Sy&B#_n->MXA=J>+C6e&Vx z^kZ+p>kw(8zY(M586tnc1Y14>&MWT8iyvn^cYRI)ieV(9rF~)6cnsZ)&D1(FlOVAi ziJ!sXlr9nNR_Mbn9z*>o-e2^+idkB>A(>C+`HCS5IraczIuqm4Qog^o>xb_w#qne? z+7;eo&Qtbp=G8)?Q+FQH)fo+3y&Ho8@pB(;W<=%RB3mw&zP$OK-DfJ_y0J%;z=nji zR~eJF0{1%3{);uB=TEXU09l{95nyzp0m%F=JE)pxWiPqEsZHvTd2`NtsVw-rtF1@| z<*CA(FB*B_h5O}B!Y~d%)r7e{tk-lvMIBM`JAE&}fp-&O%}DF;Zs0OAzG##=F`h=J zPLYhlgR+Ah^a(z9w;VMHrq50p<#1x%*Z4t)l0x^35@SqRyBD9ff_p}7hgizB1Fv?O z$F*)?J?>hXSYpeCWRFpAGKEfG3zzIy%+&yaNAzg^jkBi5Qx!Cnq-eNWc$^z69KXjf z3FKDJM6xYE#5F#Dy#78J(CT}WAc+YkP~u)I_pnP#u4*mMLM&6Bs?REgP0DNR$iwiw zgI+W)5O|rc%e%u`&Zbi$$oYL*@PIYUQaFm_*QFJEDT(nLkH22MZ=YW20I$;*sm1?g zGj1;1);y6)#4#o#NRtR7k(atu(^voxxPI zfe%!IPTmuuaCbR3EHt$d5k~1Dw%VMU5XzlKIabDvZi0EK(=;Jtu^$_=5TG+LyS~je?uN>3W7XvyoPC zr$h~wF1P38!d!NT<2DT7?&fc+%`5P?S707xnmG+X1k_o>DNpC{?90)1!;r+0m&)m{ zZ-D>tvZ02T<~#Jk5SiG0&a`YFqL{7isUg2s^U{mZG|YZ5Gwy|2aUWJ^GgfP`~lY;>AGsZc@gY-uzyS8T|Dbo_rJ z1OGJ31el4_>T3OCHfXiNf?hDzg_1PUIX0F4`n8F+61G%N$c=YzYwLSa@I7VHB+psB z8>_KZagZ}PH2-^uU!r`M?ov;1QHUB$=L2K%0J->;#VkH&UAH4a+ihmi&ozV?mvJkC?L_hIdmgU z31-VMvpsOql}o;}ZsZCN76$m$hekRd6nj+20)e0^-~cfN`$4q_H4dfji4@DTFf5np znN>W>FzsQ>ZyAFq<)1s8d@)a5?wwd*gTl_;k%s@9$)G>YvH)iL(4B+-B>zE+OweYz zgTgoOZ#uwzO|-&p4hdveo|3mgTf_QlQMCleFqt2(DD-;L2*kJynY>v$qazCD*CL{3etU(JFtUIT# zR}U;>QvFmDaq@}ej_t2tW1@XT z1$*M7blB~w(6-ynq>(WE)x^^&{X^O4B))!@Q1TLrym=)T`1ToGk#V~gL%KK3HGHt3 zl0Tk)%0PSKkB-|NaZ&L{s4FX&s9#03Xyf)(51pMbLHSEfkuypJv z^jxD;=0eQQi2q-*Ubs)PYyjE7&<3EE#RZU!tOwh$v}zFvoWk~AElXQ3p{O)>mKg2L zBRyRzqGpCBP8#pt)l_Pi41?%CO)Rs3yse3g%og%aIh3 zPiK08j}fSmVL4Hl;2dy@JOa)@*17jZ*9M}CM6Qggc5J#$?T>!zzCtKLl?@b{x}My$ zkArz(%ZmOW5)A%!PQqEq2c>Gwr0~QYD7gT{&9=cx@v_LHCpe;1Y82YI;9oXhq))aS z02>{dYUDrl*`UROSSz1*J{FF{JHPJ@&LiequI8j|P24#vE?4s}iIPb?4TWwmd}_|% z$FryoahS_@7t^eFPMBc)o<&qP;Fnzg6zVf)cZ?y7;DwDI&MV_JbK6rM{<{`zaKaG5 z6!jR*E#d}KJZ85X8{>vzGY7TT+Q`gwo&7onr6?6&jbClGLcwe96d_D{F%EQcRU=z6 z>2u)|%sjk)n;bb~7P=&Fkg9!O!R%Rcg&f~f5HEhU+vweBU`iRjvm;t;*7z?E)ens??Qgx|m4Mk%IwFhLA|oOmj)* zY>~!iWX?<{;;ppjgYf8UM|g=Q*&eI%jo}HgB9Swf+V7}(88Y;#y}WT!#tk=Y>Jl8m zpy88tQcq|!3N-TmUMgVZbTlJ05R{TpqdjsYF1grNa@bBBujD};>u4Z_q^kJXGz;x< z;%LFEKIv=5a1ni)`}&s+2llOAzM|wUt{fiory&3BJggdC*yE{{&#i030q;X`Me~c=LZK-_<-R z8DoaiD(=j~+;Q6qA8f}i^Hs29grQhW#)3qNJd3x~ zwHt@*(yhGKv9C-e!4-7*GPMgDgwV|xS%sbox247^BIv!=l~iY$tQsf@r^OxJ@9#xBw#oNMP^e`X#RslZ+?^fVh}1tkZ%8a80Z zrb8Var@fi!LZ$X$DqcrhU#ae?3-(iy4;e-OZaX|WOqDn`R%x#wxs=x`n5JmZB-jRb z&2YrWoh8dNB!W39!$0=hCa!6q6;8O%TNog*cW-Q3hF|M9v~F3wd8wHYwK$(Q2oVx6 zeRJd3tf?Ta9T>RBTmVP#-u!xf!iix1#zQGrPO#`^6Jq3IPdiF!N^h&_k>I*MUS$Q5C2)1|T^%ZR%O7=6+M!Ns4G{UQ zdIj54ebUBzl)x;Q)#fj|{d;|oB8LhE2ZIVQ1fp1jBw8`Vr&qYk!!iT5$dFI=%haXDud^Oc`XO)965)afj(B@P9IB#5F9!zxTJ@5+RDKT42LVSTuMiwAsb8 zUT1yhUj)pgS$@nM=f9c!1ir^s>C2!;ABnwNn=T&WTFx12>>RX^wo}lDA^Dt1WT&Su3$qbe_6{nI{fE(E;i}O-*8u(eK0iq8l})cSK~Y%Y~Rz5=<*HWgMQ? zStslhK(<^(y=(+l1oL5=1KceWCT-A?4-);uGBnrZSCnn$YMeI$UPbyGcCB&`@x||# zo8Fu?hhFS4xL?`FWEGmpzWP1M>v|1U%%iM|`Nhn=Jjj;B-(dB#+XJ0#bqu*dn^Mn5 z=x2b*#trn#Q^N9fq(@Mb+JB2=Pz?rnInG1#((=gj#c`ug)4&#;B~ zZ1*tZHVE^kC#(yp#@iB&?j7X`-<5;eXERGMxLE5^@RE8Jay%3Thr@rErE=0vV^;uGCcly$!QDFK|<{g zB!=HPne(HSBa^_seT#+=aWLIOWB!FUc)8NIP6jq=h^CF@KX&u9ugquDxasw$n;=E- zghDon446LF7+#uL7cKtqa!Nb)YeD!xZP&4f9D$jl9E=K>izP@&Qz;&84D-ql^#@ik zd-{j-M>-~Z&*j2V&FuN=H7toIOq_;mK|x|-=g6D8okjUyxd9K(l3ZOk6CTSl*C9gW zhLbY;oQKZdhu(-JTb0p$rm3g9%fi)~y6+-L)GVL_%q!Vc(}&pe&_uV=V61aqBf{&4ufe? zCwf@l(C}UFi&2_AgqTB^x|te8N!^oC$equ!_$|U6R23Plzy;CZ46Wp5vsxP=#C-A) zVMX6)PrSGJ<9xSLTX2sqe7kLxua6Aclsj^;O_-m{WaXvMv9kG35rS|Z%(a@%gIEF! z2?7ncRU=3N0elc@HYMrz$%VSi;-!;o>w6Ua@G~G&BhP%ZCki1;v>I|hrn0W;ovpdK z$Ov8Ie0hexoo zz`x=Kpjz~`h4upyK{$_;)%3*E-4?psRyhWUPmD|l34nX|LF!}zEENAnQZMxjK%sPQeCl*hf(1LXT4rT{Kh79Xz;*jvv%vkZLVDkO0 z^lcmT&;q%Wy>;Wt)=Q<`IXyKpBGD9P*!K(g8HWx*WO%hu2`DW#m-GIH5gojG+l53b z9Y+$|X71kP&TWn@SXH@l<5~qNryF>)xKm7f83@}Zqj0pxYvk4_8*H*MsJP&0C94J7 zcSaSurEyJrI1FZjuM2rp(Xwg7)a?o-ZzdC#@joEU#W39RDzoKc#^~=bms&W@!{~C9 zgdprv3E$Cz{5akt51bsr#r&DfZRs4Jzn8ZUT@vrl8G8gWQ_q!D)@rO?r7Gz-z|rqy zFNZ+Gq5mZUv?BQbWoik4sMcR5_Md}9C(JPPdz*^-!6ynLF*F!3e%seOFCB-b$6Yur zNFxyyv~F8q4&V89FSOfeYJTabTf>L?-~O8EM~J6k#`96F%EDdE(v|#Wr&RPOy5%PAJ0{osl}+hTg(o@U54Iw@FWk1hIVk zME*>~tnxx=E?{NWGl6DQ<9a5KiggDKC=u823f$M=v`XVEN)V0~&*^V=^GrHl<$2^` zg}u6tHNJXjkZq0#l7V|1s-L=lbp)6{bxHv`rN!HpARpkbCk+0N+ZI@i0Wf*)glX6H z(Zxe9%`HrV0)^19x?#b%grPb12k(u-3*`Z7EB&1q26wmA4>aQa;JGaGmVsAva?Ceq zJ4Hu!y{w32yr@ZShG~;l%$QR8>i80egCsjBaPxvWzW3+GN1})Wmb`8#p4K2m=TRE? zWEaj;B)}SmRwsYr9@ZAiH8f9Yh|W@i~~-rBt4BOW$`qelvn2{nJqP zgsWK&YePnmLrqBC$Pa?eBzuoZW(ZZoG7*H1pm>EalSkH_?cKzd@9kWAvook+OuN}T z0EtRv(`8PTUf8#mf}}RQb^#e95GfT_d+3_H>8Neor|_HGxm&3<4<_l8%YH1xO{ z>QmPkNp|d`*ETHG3;C8BN%xI>oEM1RWoA-zfoa5)!}wreAwlD#7MPw9>ku2l9rar( z_->bo>IG8WPmCK*%vd5AHP%%5ri~~#LW{T znv?56$75SRuxCvSi=Gf_oE~dC5yOBKUnqHskc$(2EBg!D%k>Fb z4gf8P^$YtqWpORB)Xn8!EBV4EtbnMass2fFYglqQ2n$I!p{)5e-lwP7>eDa??(_^Q z4M|NXSW}rG4lHS3NnJXSrxjLF(8q7UC|wxv^1(Ty$b1^&?ixykntU^}=W6 z{PJqY4~O8eI?;OhBwTqD7U&nnv~OSdR($j9^Sv|Osa5rYZxzCR1(MM0`}bCA3R46m zga)XkqKQmJ;r-0PblHT!<1(n0_=H3>dH(8UVj4GIofhgIQo{u}dOjWnWh0sI0|}NR zX^E@qGyA8PpsxNM8$X5X?Cd-V&~(sC-S~#nFc{nM;N6BPsN5Z3M-Tu{YI3$R`fI!) zZb1ZsjB}zU{U!3{{UoXY5bcqxf`NQM0aDiBHGW6!vktvZySpUAOR8vEf7HC(S|1Ur z1pzD*=kU}o_vJ+3So_sfjGV9i{Q=b?NCOF?+duK+N{~!eYo()4hZ#7MyIuIf6D(%hMvBsL?A^lj@gPiQchV;SoHFyq}(LGE6uJNr8+TdG-=a} zrNNBlMTh+oW>8H?qp-0_UUBjEA}l~@e}YA^-nkM`PYfOu#7pL*1aCQuVVKRaxNV3J zw~}=xVl6vhuMpO7r zCiB;Y`}2F^LJI2P(=1oOUPr?Du#hx(k>>v=F2PTuN`MjWq8j+W1p?hBwx{t}A&2k~ zbl`wsSxvHbTyY0sYPVUOg`tn}3wBIW)2uSCxpu)HYB?tN5!Kj3f=S}t>;AOGxFXQF z*opH+{KJqXx>sO3jx?%oNt3>K!^2_O?PMG7e>w+*G+0T^r`Zr7*g4QljVD)*TJ3*P z9N3t-Y*&uG*45$E1|X*Em=SN-?n=&T8pltCv(TQl?YfdG71t&(FZ#TSZZTRs7a%Oe zJIJRV_EwxwU6l5bn-iS>l#_b#>Cg26o#u(xTP*oq`<2@}^R+Sz(M$WVdYf0B#d zODcwjeb+ut2-3a!I%2rlc=f(=Lff%P>pW89po|MbPNDxsjW^Z}d^;5b@9{T%iGDIw z0hqc~hD!boC9ZoWxFT>YL_3~`q^cgM^y=auTqA@w`ex=d@FHr7X030UdBFGy82FUF zgF4^klKSN1(eyWT$^17JP8iD5bRgv?`JjCa)GAkIIDMXI=UOS&ht^GJS__Q*CSkEu z6(q-2YQWqDy>u6SVpc+Opnq%zf%kt5^f}9)@y-OE2_q=s6>kE~S9HB#;5^*Lpgre@%a)J-R#uRN#gEuGB>$87k_@OKMb1H?@*fmYf zPLCF@_9er7^H6tl+HLl&0@V7}PsA;}dSLbF9_<&C72;n$5C&Xzd7ETPYF;tKbqvg# zPM9F^zX%daT224A&Xf2wss_pSK>Ie9! zeEqo4CGwd^?Uz-g-$Zq@JHq+AKbSIE@ZC!KI~6LR>`A!#;A^a+$)ssHB%|KC)8&E< z*U{MMc7gBwA|e7$pU@!rX7 zkgJrd1Z9i*rR{e3pSVMF0}P<;ob5su_4KCbI*O}en2fzsBZOvw%lgmog#e~ z9B;7w&D5NH-5@X&0QqxrK^<6>>sfYuBx=q5EA?Bnl-Q~(wFmaMAIz@5M~|83VbfX8 zI30Gn)PlFAmX=ChH*c@LS`1B7rU$!&YhdYwxccktC-B9vAR=4xzl@bDXo3m>Dbl~{ zcI3&y1zl)xlZ8>uwnr?XU-h7JEo)kk;RlgyI3lymR7`4uq?~-Xh+QzFg=K-QJfL=U z6_%L|n-y}F9^Wo{1qDu6D?g%~>=k|kHqlT&Pv#QQ=G+j=CzNZsEx#+hx~b6p>m;D? z=~N4F0-6^E9MyvX6bqf(J5=MWV}@@UubuHqE>&!q*MY&5pqz~~nY8w6{->SuAngWp z3yo0XqI(A2$*f2|@=30>S)+;H*&tFC={CiG(H!pi$9~5B}<^`#5ot|(UWdl$m39CLNO<^9MNz5M{ zq@(4GG}K>4l>1zsU6p6hn&mDqW=&Iby+_j}YtZ^>#-u`t_@&tSqup}2!r$87w)Bfd zzC4~JKe5mrS$xBk-3s>$bGqCFry`m)vP)*Y0>Nt<_#n75XaHl6DrdlG;UIon2u`Lr ze`ilOfNA{w9nPMI9emzu=D%~ZDxXSq0HtOgSHQjsVEM}_g!-go7}S(kQDBZdi_(Pf z9N=Q0Ze@s+=SCpV?;^Q%JnN`n{zb!?vEq)~nyi6R3z8#L9)rh0<%}^utGWzYjJvkv z<$d_c8=1{Aak-@?C)sFi3(J88Db=0tPiif_u}*EP zyD>925V9L*)-RR{9VPvds`AsL=F~_1uYGd(MKBqznC&RT+)yLQk`9H!UJ%0+Sg!WZ zCMykxyKM9*@qXkqp>)+1;|L1ZPZ>p)my{S;ed=74m0u!znnX)le(Ub&T29%OV>rx% zS8k*YY3_Ecu_D1L<@blaKvTlt^ru6(rDim9Sc(_0*8L5nUiD9+dH~Va@#kv5D~Bh{ zljERphAO_dZTG(o=A9;=`D1Bwmqpc&?tyW|LVw3<#>-akMc`hp7qr}c=im0|ru0HY@mKFl`KLwh}f=$)S_o?DyCLcy!vsIVVjPW#OoJdUqjB?WxlY6D8f2aE6rzK3U z`kLuilZH7T_M>QA6hTX0wS)5hR7;)iaJib`>SA?va%g{R$(d3!bgRGnOWL0>4FDK( z#4NzIkbkyLYF8+u1z`zqw2QIUXotT&y{k;8ApNQMGpPnHOqD${2K<=b)l?)TKl2k} zp7CzgoC8$sO8CoVZvW=G)r14OPln*`$c57rd9f3!yGR}>;e$-%} zYi|kl9BtBhLL=f%6kMo?ddIb@1T>WccT`eL5lE8FMLs4zU;`f|{%w1=1{0y*V7YpB z!y+$u&oiNct;*$pE{Faka=U+ZjERh}o0W)pk{+uBe80K{M-7RrchcbZHLt7@ zlbvp?17xe&{|zqz{ZFDs0FfpRsL;Pt{?|s2+I`#MrP$I_<=V`XA~Ght0&DlFb=gQ> zKc3D5n(?xsKW8T)fY;6SWKYq*$Hit_vJA;;@2TOALYJh)aZbsh+a z6-1J6pTH5@3FNsSC76Q+A;pMMfrOTDImgboxQFX&-lhQ@g6^U3qNR-D>yI*>&`2$k z3wa^?yQcz|h1r%AGG$L9f+76rUE1Uuoli$0AS?dLIBFk_f!HB1PSG=+l#T3t9>f{ZkNVQKCe!Q<9@5msDDmN4ma zqRvsO_jTwMo-?9pWN~;`3g)QwDoDU{Xh&MbI@KyZFR>E_A;E7H_@L|#nK_+gzltL7 z^m2;106`CtmNUz=rhx$V)Ol1BAM<`(Dcg3Hm-{GLb+D|3ATmgej$Jes8oRU%`VPbE zH|j|j?W)GjLhOBThVy$uJLxIyoQ>x3hVR7G%_zFiC~pEXk@n=jDC#x)6lw+txohJa z{coWCZa^~(@1xQs-C9l;+{1)Tsiw`JX^sY-*1{AFDMS-t;&@qL2*~_z7F?Z-;CtLzbH$AU& zGq9#gwWQ4BBgvQh(s&y~;}|62U2_f~Gf0vYtm+SQ-huZs2-%-j+Lz-nb%+Wp-`#$F zS2$Q{@Lkjgg$j*zDuG9&HGxaR*_~??C*)MQ?s7awI&LCPsiykDKTMc;xlvS$6qf)_ ziuZR3;%oUy)B+${X;%RJmhP3&j_O7v zm>Cr&p#{a6)m%kEbo`{f}`1Kt)i%VtAb5w+Pre4opWKC%K@r`}Z<@`I%{ z0|<^Oh;952Dh5WgIjctgOAG2*7fjgY$$#^`z>iO-R)7;*(vOdSL+NW{vISuCh2%L0 zXf!1$+#tLCCL6*Os?1_4NvD(Ac0}AqG@`y?a$a4cV+9AspoQ?*z(eJyMsDBCB|W8a z4!lvmw}p}_v2vYF$4Ja27)kAQu;fcbi4GUGw#QzV8F=Du{R%C4{ddG~vkT1nD6yga zl$>oQy=4AASOQ@07+0H%iCHm?)=@V;rfAZJ!ZcJuGZx2^u36^bKzMP^S*E(vSR*cH zls6ktVD6hBRz$3)p?v$}ul0GhHKnZ-hB?TXF5^!NxkGAq5$Nl}YC=UQiDY87V13Za zV~wuCTJDb{UR_KfL<6t$Iho})93IUi!mMwF@sJT^R?x#_<7Jnde~EzXKZ)7^L|G)| zzN;An!b z(gG0Mx>(1xa_<@@{DRSzKRB@c=gFh5jH>TqA5O~n7vo~4o2`S~;b`(B(M#5nK@w{? zrxZ3Z5Yxa+{u6@7uAs-RWWPp*4f!ljB>ZO{gh^ody(uHCS;g*Y5%wJ@^D#!JVD28# z8oe;iOb%?DeoA51|0K`1hLWzr_J77ypN1UBNgCAgb7Iz?-@1gYz=^&4joDs?GQ6_o z>Rf9ix>tkH!2w_TU8YAohn^hWRr%(}FT&jVYK(PQ1U&OR>|EN_zS4K4bWV+aPv_sQ zz`Vduf6K;Rr%$7HfKgRJpVU7MmQJ_}&cka=lUk3>3RVe4FJO`xtTRhYu^E35J|$k;#po!mRq~L z=l%^>FCICSERZ`g%Bm5S!G)%VX!ObxO7ZHqbgU#|)HhMbPyTv95wf;Jw%d zStW&BkhA($2Xvfx^yHpHjUpr=<^6c0M)n<>-w%B(ZJX{2V1-gsEH78U!NmkieRn&R zGwmH@9qb;2H!7Ga9c${QU>M85srds zLQeB)+heRS*(e0uPeZCG&5@fCxVL43E)ISFPG4&VRfBdxyLwf)?Ezuj*k*6F_T{>tS44T@PD&luhTa88 zAtM`6Y7wytiQw_7azX?MrKXU^+knWp?WAapb7QM|N8=}J(~D_1s}bHmV34yWW^6z4~+-VOIA?|USta&`m?e>px=laZ4^L%^mFx460)5Ok3b zRJc$fzbA1e_MhiKA(hj!|IRuEB60k~5c=($@_V3BkkQcm_PkGR2}l7Of;j#6=@NVA z7*`XLU^$HpMqg@7j-M~?1K-Ef=1}(+x$5dJX?Ub3u5O6X@(UCEI2XJ_YH>f-B|}fD zc=w@gi=9&WDu|-oYxr`^0=R=8Qonr=jSTQ0*@>(o@fZ~|K1PN=2bT(dbc158Mt^`+ zycD`ZrE(AR6fT9!+zmLsXp4Cm)RfbkxGXp`^WS4k$!%1pg>jw`huYNS^VZLDzbEj% z{w3-S{3PlI5JhK}aQs^@a;(Ni*g8}&1(!u)8;G}=qpnN&Ep({)P-IlO+U2go*CRR}J3_?L(;MeFJ3n_|BL zQ0rp-5ng@CkVYf*I2|`L3Ug#*=5~ULwo7mEE^av>^Rg~EG^u`KcdZW=(k$=Wqu|-I z9PUtLoLSWh6f#w9n_T`~vHFI73iSYlz&NK#K|X*1n`r6iuy&RTZ%fof5J!(Rvm}3B;bSM?A%F=po0y+f#T1HnwZ9MO#>jYzBe{$pn#WL-}=70`cU8T?ED9 zv|)aB{7B3DJ~9fNPWN#@y2Zw`Kvw6=w15w{F2eCHsLj+oulS&8u&(w8X1-o=21!tEs zHQNw4cUF%bFEX8-=w8~d5xPwIJ4WvM8qv#XE|000Z>nJ?uyd`V&reIeESqqOL{;gidG-P-pj5C|VLE^2kKb~C` zWdDydb`lg<)=wAyBCml!4zkiNak_qB%jE_TJsg91*}j*~_&z)(+3;OoS#Zek%BMtp z>Oash0aRaNq{D9lgw(u@u5!@%YW-4#F8mUE%ELPwTwSaf!nb#j4C_R|>!|M0f%m!8p;m*V!n*(s#qg3v-|KEkX_dPRC~5&8p6lA6nZfSm?ElW|Wn zD1RSzxGgjAYmum%kFiOBIt|sn@zrfGn;2Eloai``QF8F$T@T_S=S+IUU)aHX<(M&l zW_%Lx0z={3`YxXp>7TR(j_f>j4&*UjjpFtyyM*lVO)D*6lFbcggd3_Zh(8y}d2?Nm zF`?AAUlHynkJo{{_s)1-hDK?1{fZA-YF?3WV$AGb{FL@dvbD+QiSmCQJ~waUG^@YG zWHP5Q@JCwx*Mze-;ghK!z-0FHe?;8_V(1nd+XN>S{Wx|9i#Q=jRz|K`9h66u@4#H)rkFh8QxctmoAC9KU7G zVdB;I5rcHlVG#&+0!8x_v{)O*pz#C>0m+-Z&akWwkGre;PFLA(`y%oD?nBxAq z9+njf8vHyX%xrqUH7EeJfvm{1eR3!D5z^;U>pVGL%nv%xGx-o0Q_e0(Y--=3zAu*Z zP+eVpsgJ}rON9}$RIVd$LSSAqM);0Blb$TIT!y3 zLDm_MG6}l;17@?&qDe2;>fi5G-;^($J^)Tt^$p<6FW@&n2P*<^tfwKKcuhp4;Tr2- z(bJcG7)PhKnZLr5s|s{r z96Srqcv+|n5|M*JEVrm}V8GU-t6S?zRR<5VRNd+Pf8jbH`C~#q+(tSYQRyrZr&EIb zakQ)})z6n8hjHGmHnz;!SB)>xC!Uevveq8Mm2&jj8rpENA=)5kB`mvb>@N^|6f?qX z)S!-Kq=t$jX>l?(@>jj%@+eLLAxwY43sEB#N0KjTZMAE1*pE(zE+1C-P7jNrIP z$}VzL(Jx>)yU0snYJ;{7hsiBG$ynGQ?V2;GB!NHF04?d#uCQQuF7qgtnRx6<7#JC_aIKgpDuO*-+n zvuLN?@ILSO6p6QdphkD$H{wQet&Lr;>1V#K$qaejBypb-X#~;$i6;`d@UaOLjmVRP ze%;RtejGER#}fPn0ejLkYWD4hxm+=@@3`Ht$manx?-%g9%dur)u`%fPSp`v^^fXo*ac zU1K{?Z=#7mAX{j6>>ML5*RHp`FiMqKQ|xp|F?sRzl??eMiAB99f0o9Czb84d59!oLIKCq zIt)o;x?7@cH}LM_7qCI&6&?Yw(-0*kqpbbODbw3H9ATE4niu$$E!o$)#ORFb!!p0( zx2_*@#(R6JT(h49xbg{U+^1X^A^mitCrc$Eglbh(PDpGMNc*G1TERH2Q6RDxWW3Yu zGmf;^8!Wop7ZzJ|BM)SZ^azL40x5#Ixpo7%DhUZiXje8NHbDcjsc(W-~7&B{v|UEkZE&cf&{(=$YfXb z6`Z1!m{8<|c^W9`+VhedbGoEjy%piht%Gd(`spCo9hQUHN7P!@H zhYy~pnrqBql6w5uk)rPM1A(S5bc8goHs{!pxrNEwgG%r1;Py6>_2yC+0Qb7w=_B`( zf+B;F?MgU^`Ev!{PlE6F7kg3mw0HgIiONXQ(LseU0C{b&*z-6`wwC+B9Jo^$xnh|&5hBt`-xq1)5Gd%uc}_R~W;iJoHU^k_vDRbU0V z-bwylwVW7z%AOkZY*VUSH@(WpY;!ZgLsH&yzF64Y`TZk=5Z96)DXDWR2r7{h&3svX z!3NXI{zo7uCkL5RzB~<}KR8bmq4@7!xPuktOA=w%|5Uo`zEnm5Diik2zksg+8Kwzh zdbr7rXWeB|%~V-jJZyI{Vm0fE7x+4r&*zKm9}73DV|;ipY) z-@w_(LYTt?9?QlVYO1_v@@QSCP=90|n z?{UFD*BUpkZd)P_*kxKvh_zq+@3itNQCP^17I3gM892U6ka72E6p&{r#Ul}CZg(lO zYWtce)C8y7i+xi5{unhKJnP~nKD31qJne0R?#N1u-92V*I}92IJ;qsG{DUSBkA3x- zyfd-85OT$5U09~^M=ab1W2SgVZSCLs4F1+HjBxC-x&zX_B5YDJJ*Qg0%j z2qpe+5+%Q0AXFhUh8&F`$DE>@WLpb7;vWsEu(u_!^9>OJ?)!=;7B0O69bd z+Y*LCzzIP?R5z%suQ4>D@W^Xke3MK8gD__UpB;;=67Q17!owHzDPO?*_tJW>aOI z3e+ozNPl~Z%N-H^`|}CZ@ue^UP{>NjiUB?cJQ7^~x6CJ00U&-|A{a`D!8$5t?`q%6 zcGa8jtEtrNHRIso*{?z#u+rQM+92jBSug4zgrlx`c7Fa*L8mW1CZ?2iPw&jIb1kqc ze{$!)WM|eo(773{&F)wYmm=}NNuEF}00O(?6(8BX@oWz)2ynl~4=P%Z=YM)61`pUh zC|Gk7p8DoA(yGc~3RPr_zyyPID4=bVs-eQ+fPD#amIjj2tP7Hr`;6*j8jpI_za?=I ze}Wu+{U7bA&zTSPk!6^Nwc|y7DXVy6Z!c- z?x6m#`EbO>B0sRZrFZ>aQxb7Yi5LW~kOge4`27478v;-g9RyyCy4V&D#ie3YLcXpg zXiE*RPSePu^4H8?Qe+$LcVd=M(WEGEw7eT;Axt)%QwycKvWJC~fY%)yw=5e!M~_wi zI#`;!bMV3nr>A51XyN24S;G4WVSYf?FHyJG-uvgvuqhyD+CZl4jF<(oA(-u-D;m&R zq~O!kNmQ}6zZrWeOmIXa!aJDo7q{Pds~T5`hbF_fo3^b_|9cL6`@e=i1sJ}M8N@O0 zK4A7(S*2|*Kika{Eu3h657TXuKW*=ot5kal=)M;v6w=hz+Rk_t>Sh+naB+5a9{s&& zwlJPz6DGL4>&$18t+*z@$8M8|SCs-wslyP>oH-)l!0Z`_j8;5jm^8vQ*$C*lSrq@H z^;AuM;c<<$ym~=tsb?wWwfJK`n~k7YS<>(UH#RbsWeO`n+kcE8P-4_S!jJIVaBE?9 zN0s2}=@3-FYYuc&G+ZcYhTzsqUZ5BPp_YFxH=Bpai!?a`DjcKg9F}YgW{a{`Vf=HC zcRR?wL?(WxMJ?~rpvHE8>pQ>Ne~Wbf;V*z`0DyMC)*7Iz>PcnD8rNl2y6ON`lHNcg zv|dolQ2{Ddvo^z7YSKrp1FEKl4ZIn3qzs_rVE|UWpEQRO{cl z)ot7n+iL!6U>faj+%Qs#^%s>J;Nu#WZgq)~8KrK&w6F{3G?a89C9dN5trLq2lTz~= zcRQ7D82Ks4bq*aUOT01*W6(|TUOb@AFZ5aG5azBZzFjUyNk5R{(kUH~^Zp*Qo3zjCbT|LLsC4q#1buh`bGVK#7fl!#n7K*8i&vi70iP4&&A)O^R^(17g=ck_FMB1}TX z$Kr)KP%Y3Af_dSr~rc`S!Qch_T-Z+)*pfoG3ulM=*J(~^9P zmfG4yQUzv&gi^92d%`3hKb19%)&$#TkO#zJ?48&U-NdgP%^lCM$%YSG`$j7lw7gG2 z$Rt5V@0fGJtbZp*vvNqP4TAV4_tGYCE{Xo)G+{6v7i!!>(6m4p_zkv8(aLP~O8Ng+ z4O3qjvj7Ypi7kbH_VrKd_!DT6;V+4~*=vB|%csOuH~Q(E!~fJHv$*gUP$XZQb^u=k z3xlw$7}vCWG0We#r7tRqaoNPQx93-1Ss9<`!b%TC)V)&9x%$rSovqgl6~v=mDMBtwtjABm=VY~T$1Mqi z_e=2-E=+EcT=$MPI>h$HF7lIe4}$-QHz$DF^CJiBXAM;m{%|jiy?Tb5IK!c#QZv*y zitU)Rza(uZn*>StYhN=%+Tv7d{(prazH?tTa{wD$M(YRQOTfIT5^BXo0B4TS?cKfXv5pjzs7j=qKhx8Q zQ)Pnw%BK~UlA^`OnAZfoi&fCHaZ3%TUav&c6TUZ@(K9IZ>Rddpjb1db)*7}Y_-DMgewrdB z)~~UD*HipUUlj8I3QW>+H{i3klZoefBUr>VCyjb0Hb#8*I!h2k&>3-SxW;egGaxhx z8H(f$KI-OJ?Kjexn_8?GA^Ym&f`jxyfaMX+24qml7D}oOf$OiM2WP&>)04mUa`jv+ zry_g=N2&}ekyyXX!6P_sbrQxRGN-RI!bfKe;)(y}m|rm3lI@*Xb0}o+72JdNhxPE8w6wOBSquY9crZK6G?I#7 z2MLyE#jUXL{?Q>&2#nH1T6No?H&to~?;vd|np{ZPbB%Uy%QE;5m$SwFKsP+x&3zFwn9nnE)G-|eIXUgn z?d~cRKMR_ki>eOlCCMU6smA00rL9Nf*hm#i{QDI zvG5w$`LlKNgHwUhA%HXxHG4)_ZB@D`Mj>h3qYJL0`4UeMqhHrpvMJ^>tzY zEh_8Y_)=K}s0=ER4uO1{zOBez3XIhlPiEa$FH>iaR3S>T^w9ih!btNxOzu0Oc}*zN znG8CMX<#+4=+2I$&aXL1NvQ*~+u&G{UG?K}exQ<38L3u=z|Gy!#&bwh`c)L;F7W-n zk3%9J8$D$jng0QPs0@6Ke5L+$!jrLYMA1B46ch(3g;fVF1ZKoi?i=yCO;g;3@<<*G z+q0fm#I9*GHY;Brdh!SNEgb*CF1OHX++XOfi=;zby=e!|-{~IMVFcyj?!2Q6^iaD=V?U<4ETB;MKS~l}!tG@3WJ?MJ(Eno5xM$kxf?;PUT)lTiL!PqBE_FO0z9Cf5oew*Wof~0>e0k3p z)W<@!eu^c5U37B5h;K-fU;SWxGBFEsGrfmBSk1~^bgtnq?`wG@_Du(VoF}oy zYzLC!gfuh-Y)O~+s-X9vbNH)x*L*; zeTfG`NZ=8){ecS~#B7|$VM)A*3h5HX?Gcm`JEZT|)l(UPNb%H;i7NZ|yHl=;kw42) zblhvofb*fL<lqiC=FVqio_cEB`EG+LBt^%+ zD62w23sN3vLX*LeeX8_Co53yXik$27NaWs71Sf3C`K+jEWpH~hGCWz_4PNev*sF~z z_-u#gveRE#KJsB$zZo^vN;gLB{$#QjP}hw_S)*6VM2eULT`-nf4bC%E)aAKm>=nDo zISdqf7}b-bX-V4-;{yt;N4fYPNB8*`$0~qh^9^SU_yWKowK?MZKI4KKiVs6l$-Yst z(2kU%d_6)dwHw)68z(B%hkRpcp`al7L7frGW7N#L@?+Xcp~|<;Bt+9OyRS(&K=j%K z5uyW@gEG+-2WT^`Af6`SUZ;$o^7C)N+#%YTsw=BLm5kM+@z|!GNMAazsURBnGM_G zTu>dq=u$@G@A|Nhvb$}{O)BArN|PEMU>!_6qo+(DP4B5>9`NKmGOiQ>MO4(B;a=l% z^mKc3i4q|1UZs(DRL)#rrU8m1-)=0f1Q>`y3!pOE{5U}P zz{=#OiaytaCVE)n^gbYyMSRe#&o)JrO!?xa5VoFP8aS&-;0h@_X*p$~zz`s|?@|`M zuZI@qZL^)|+u6`7?y4^q0x}Q-`3fMJ5YW=_Cg#{R-lhNjx&hyD0xQ$wU2kTi#WP|S|N z0`U7rqr)duhJmpz?>nkF=qh3Sj$zCyz0k#=*V`rmoi|7&^I7_RH}KS*gGe^sCM-b{ zpeurC4A}-{9nsJJZaFET>Uj)enmUCi@-~Nc@oV$4$F!vI1IyIs1^ub-k{;!tD|+q6 z2hP;L3MzzFwzj?xm^P75PR+~a{-*-;{H3xEP>FP}yaql6%s2Pz{J}+@ouDgo(|t4~ zD0x3|j!cz(tt~!ip{s$bl(a}7*`cdAsgzSlanGoBB2t^w=OtWV` zGa)k@?SNyX&HOKWynQij02oA)OE!VG-&Qo-xMNcm)y+_<@x^Jxj@grgvt<7I)1u%f z@Np&>oWv}CI2&DT36H>4g+%ch1@LI6&3fqA|7;u;^TNuji_G$7B{#^hQb=nC0&CqF zOh`5((B{?sfP=`idP&E!9jX&cY$JYI)1x!*?KEQK&0(0i!6_CKcV)^>C?NFu1c zf3U34kv(m;v6|_Hy_7yn7NBZ|G2O7MWt7Vvc^K|*{}F35y%o`d^IyUMBK`jd!#4qg zm-0s21^MK8^HE+OaB33R$-}P%Es&tT1Kt;N<)vc(b8!r%`;N757=^bG0;(+msWCZ( zffp^XrZ?ksRMej!dScsHr7Jk=VSbaH4p!2t<}0Zs8$y7s^NjUld29uU0fGLf-Y;u` z7ogUR--w55N6^Id#4rtQY*(L#e%*{0Ab+<-O(Y4>k~6Y>X9*aiKyaOng+B?-gkw*k z$f}vz^qDH>KNo#Pni;TkRcOsS+Fki(co`WQ8GX#Wvl?axg2qd1m$Tyh0X5@8)wQ!U zAR=x8yd+LE#y`Ta55J{MKl&tS%M?Rmma^f#>jlG`)Yq^hE{$XQKWnnN9M;d%X*x7U z;5(ZJc%4z+SW`Qj|iGgTvAt4>yHN#%M z5oYsj8(w1T8u|ya7^|##RT86DYv_Xv-jfT0tJ(X2%GUeesjKxPEOt}iTG=1ZpjA=V zK6X=Cl&uT!5nGLk(Mdv@fPUM$lDv%^o*QzSwP6Aird-6O%v&NH#7b>Y#^wwVQ>HQGF>iX z{@--~Ymhsv$7X@4d=D2=A0Rsd;=%-Zu^z4Li)Or~+!8 zLfB9kUcXmQ(citBhy-!Erp)gQCx;HulTFnd94M6O_HM;-2HkA@>6Y(}Dr(}NNsvEQ z0@gT-8*6lxEIHgD!B)*dGUO?)MPph?f{d#4;|Skp?bptf;hA7uRFD)H3Wck z|MaEzE8>EhTc;DthPl)EVcF`{{Do>6d8^e!#xVQMu^||Nc!cpoa|02=q()-vHqP4t z=8?JlsmlaS(dxLB{}?P0oqn4TxLBTl5I*z}%TP&>w-PtF3O{2TBk~)^vFe9$zEg@C zM7)!fN^;Yv%DL1Vv5Xmbo_*8T7Qp>@N4q3rV_r|=0*N&f8BCweFUUU!s-+&?>%HuIH&3t= z99v4+L84fq;K<(f`xxHUjx0>;L#}+uWBd6h3efW?S#bAHfCYEb8SfF^?pLyj2hz7;ApgAifzg&?>A45$Q<~K(}FDgVkUq zYMrFUGV+YBV0cJhY3X(H$h{;N(x@uhQR&c~<{2&WEnVQeFs}A`r%bW!Bnk3-tqOef z@f+_=<}95v6HVj)+v zNw09Xhc#CgtDnTArr_rS(m;MJgm6K5ecA1asTbqR3cjLpTi6_9qKS>WV2~8`70V%J z^Bm5AU;@!`-zXMzy8A@?N^KrL5&=QmC@0`BN-ImI)%eOOhiO{zDDn2u%3Ox}9yQXk838@HVHSTCs$@od#H8E4 z6A?c_kQakmJNfGI$DKZ=g^BKx6wg$h!cO3M-`46mr>>0xQ4ljarKs7Ky~QIUj#NOgxdR$ z9%8K-eCg?NCHAuYaZ`K+lFvmLEOU5b9HqxJf7YfoPqND@hJOH62>rI~F?`z7$cB&< zRH&{NLzSL?<^NHaD|ec>VnZl_j&@7Z8qv$hA%^$<`l`~!(`?W+@N&4G>Bjl~-5n_# zCW}FB)ho4tDqzSP%-JsYqN~oWPVuGCiVnIwTb6umQ+MdH;S>xbf$1E7!CcwDV#0E`$2mFeErT;@Cn)npUK!fBk?kzWnw9eyoTt`XHZP zZ=(heksk3eL39`MUK)es^#uf{eYxSu_}glHPKT??8_&G?JXD@JGSrHITeJ^^pXesE zMk5gphIX|~NN+d^KH9|??XsZki(Qt$qFRu@oq=le=#XVd(qB?w4bBOBFnkz%a=HJB zINi8zq!A8ON<3Zo%NP82$8&xv(?+$6E8#S3Olt^RJA+d5^o$yTEtzM%Q(ld^6b*2i zJ$u2*C4uXJPldYb#<3BIThc-TX{b{Im=;^OPz&laQ?CR|P*yILHn~C%C1!1uA}eitPTJ=%9Qxgvl&}6rMR+W4-(ryPH=M%KtA_OYMF<)? zkTPs7@@^)L^m3giWV7!!j>lrk+xO>&-*z)i+?^ExfB z{U2F`id^w0EFC=1a1Kj%eYwEoUhT<~M(n1}9Uf-ra#8)b;UY=;lP0ySst^#QPXB-` ztvSzfY2Oxgb1KYp0DU}z{~MTY?61Ha00Q$}j)vf$LrC2aua??6X;^g?VIlT^Ege(d zslY-aedta0@^D9PIxz(GQGcsaUw9SZ8L{pQtq&{19`ki}t6cZ<&Eyo8d_3{4ERSzt z@%H1pt33zB#^)!_P(|8}!c7{$6@x>Az%Ug+__C>lLI;R<%mI80Z7mBTHN?dP8p=-$J(pAvA!C*!SUNI~DDrWl=kcic_ zcSw2eNOj@*Q6GOqNVO10ywDF!<052h?a!k~eELXT9O_nf1#GI&^TZn5I9Cr^YeEPp&}Jmqr;orgZY0dC|&b&e^Qg>3zz z-zmZnu)L(FMbQ^t*8PTDUJbK>Y%R>9fk;QUG8uO5Bx8XuGK5&4(|6gr$+#@jnH79b z%VKj2BEh8{!s1;zSF#U%z>pRWH&owIEm03|67vmz(n9p$GrNCeU_@GZcm=I#nK;w= zQ?l|m4Rb<-T1uSmOjM;>j*mDU5m=@@r}fmxBC^!cheV~M9PYcxP$lHt+FaGYSFG|A zf5999U`9VOa{zw{OiAM9W-)MCF1O3S-8OF4SwN-~3|ckFQo}V7CGdBn+8Ey8bRTvx z&KYdq#z{*0Ry|aV*NN6sX0utnvqUrNVAewU%KIfSw_I<6>SKo0Kr0>+3;71~&dq0M zLRU2k6Kmdi35YS3=0Zx~E#d*&8gsop3oav$Bt$y?%^|4j!Ok!nqrEyuX_C5GX&)Z8 zZ@{_n(po?d@=-$+*iemtU zc)pAj$fx6*XY-HRrbW7nIO>Du($+JROUAV1TR2PsxQ1!dbSB&B{Xb4KJ_{`weXlUi z2`Jw)^WHY?$bl`h3Y2dX@!lzRygI2$jfO{fo*5{jEB}I>oxG$N?f(me3hPW{bv27=lyVi#ZNE>zr zajUrCdp@I6);Pq{6MpTj8$N8gtR8tQd}60Sc(k{KJ|-t$v7ChJJ>h_TCj6b$bEIOp zz|I}))$+{mKdH~9!BXuV|bAqH*THask-d>ZfvIFK^j;f!t{AHixQSI@u_D{PFnt z?J!|4>jpWYIdF-#fAqqqlOUl03gpk`QI7)#IK?D#u1AAtBR6eFguwb)GsEvBymxBb zo-(Wqc3(t1h{Zsh@c8!Bq^&i<90#I78-|N82((+QLC@8Gvq(Twr$80;%$>z=mujgr ze8LxdNc@tZb9^nS>hSp6vBkbf2GMPT^o+?7m=UeXl3)k?$T zDl~@M*Mx*|w#gqeub>pnf8H`aAJAq(X_1vMs1T-n5&Eox@EDku>0G$QUv{|dF14wINYjDFKZNs^Ey0S zynu;P{wT|qlFVxi8dDSmJ}(q-!N?zXs6Wo+x)@dkwVTd2X=R7|xh~oXv58~67s-o- zhHc{hu6;*?#XY6`Hdz_FRHgq+;OV!%h5iudqs*W?&vavM>QH^QHVXtgyG^SfL=!KmvZ_I&@H3>z_ z&DMq*q;LFAqtN#WaY}KzBCf=Md}?PO(T0GWhIj5?-wFldtrz6T%CIe|_XHP30&?zF zftVc*Rv}Fw2=r}z+^>g6562!lM{kqWPBO&=&bx;YD(N|MyiRTx;t99l9*OT3-!2!< zHn7igZh^zTwJ<(xTWmI?j_nNV$@H$z%1F1}l{62cc>XA!sVBGWUQUgzJ9h;-q%C^kgH&tMjX~$Hn{GkVX?=0%W@~G*v zgg@JUB$5(!J5e2H+YA(;zQmQDdop<*dW8AHl&Yb_4NZ4+d^oAQ<6xeF6DK{Gw=pR6 z859E6J3+R&uBE@hkUDpdcmg`O7*GsANkaxV`tURt`dWC7+4XC&XbFOc>`+N2+k(}x zVLgaeuD*Z9-Tv%olVBuD)(ag6PpI=@wnmAk6gn-xxk&R{B3;k^Xc{1q#ecJ|exMIW zeat)P%qxfnqBSjCY!~l>d$o7ksAh3ms*BTWtMdLy>C8>yWO5H4JWXGajon)^l1X%P zjU1|KdIT#+ljaXiN`{(GejF-YYyOug$m?eVHlB@!3ON_k9t6~Ll$*i7l-=xKlotR> z-pwk&>0aWuzURx|cb21SB25RuPSg@ME*UER&A$yPgEcnpYZN8WZFBAR3L2C^blf^MFFLXmN{f1 z>1BFDD^IDt;w#VHzcb5AE;X#K*|0u*bB43h^6%4xU~CnyfZjM_8_$NPVc_6YAKlyL zzgWdNHaont6W?Ys6)_n%QrhJbC$&&msewru?q`TKF*0o)3D6JrN9^-ASA6%V`re}=z zt5`pgExn4G_VT2_38S2{>zR6QSk|#C_cxGPy8!mszhgz!8+eIoET7A^o47&nJ578` z#z4@`n%FL&fvEZ))E!kARpef9lsS{&MuDQWKlK8V8;~>;Bc%ql-du{PVzr3I(Uue~ zl;TUabIW{FDql@-*?S+jAAZpagJWzwJ(ms(&$f8x1jS5xh<0N%A)hY$nnJe$^KmN6Sh5JY#&Wz2BPPjd$E~rcR-x0;Zz|19g&(Po<(1L*0`_X$!2=i@Jss@oANPzzG zrJlVD(NdLR=?8wbD`d_HJxheWz_?tV?LWvEgpgab!anK4z=T2Xp38{mh6_|Q5>L%8 z86%k%a?Rq%3+Soh5Tv>4L@6de%F{7OGJI95mIZA(kf%P;q2z9iMjX0$MuE94Ub6Qv z1139Uf!G0+I#)L^%c5HNF9iyJ*oe)XkS8}GpH(iUF)TE#?R zdIT;=b2&ik^4#{m7ttYBCK=j&FnR`q) zF;%;RVK2}tcz50YhvxCGU%6S3D|I`uzovn4s3I>N=9qGlZzh;q>EpFFwc2q0qpxj@ zwHQsqvGJ4{%hlqppR--+YP;PO7Q!baHp&yWM<==)WJ zWsR^K(*b1;A|W$Sk-W6X9?%IBb&>_fxuzh+XnMOUv?R?o&B}#hrP{3G0kTM z_WHtqNtM^!|Ce3GzM^ykh!U$w`qe*I*dzC%--J)7*D38ms3g`K7IrQ*IZ0`D_O8y$ z;PuMea1C~$<~wt)5fyo{yEB6dD9^qYs3#@Dx>^l>ztQebavtgq;J~I4h~gQrQ+X^; zcfF$EgeBF^?nTJ;^5J!}1ZI7U?b1_i3RovjFWL%DU|+)6{Tb%#I|Y?A?>JS*jqb3E z$A~0lKN)F?E3b@fe3&5e{Z*i8K1y8m6#JdRkW;0+kLg!6!g$9($UZLN)>HCidZEbu z`TLXYwW?j~*g3M_LA5Sf17W`HuNKD^km9*8QH9l$9lJ?L!6NkKop|rDpiff*gC*ux zorV>}dR$rJ$cdOY3t~-EcxlJ38{aFY72`h{7~ebNesq;nLbW}B%wq||x(T_aC|;;$ zlswFHX%V?6lH`a)aVl|5E)u`0>oeHXgrnljx1wHw%l^Ccl={NG1>k;@|111YeH}d} zQE}~B9|V_Eg>*x1oJWF0noNM|azAWjxj9^Mf*;RYtx@P+-zrvn?yDHMl z2S6XoB&6-*DRFuq(t3bm2k5Sw5hMr5773J<@t5?cGXhKb^!7y47scbR26%DO1=;SK z@Q3KLM|8s|FrxR$e*bj6e9cX~a@F*UC{lIJNyAGqMP+H))!*>|BCg?>!L?PNVN*m; zNGnd2DN;MQA??yZk3&_SRven{lk)!Ax0PJa_sIn+g7>hO;GzISf2FYK_3xgd+?UNA zz(&qV=M?0#`pvVZ6Dl765DfGooCDIEvnq>5yf9`!)i>e;#?i(~6FDuu@<a9h^r)fCR7;jHOuq&oQxBEuTTEYMwGLqp1j0%Y=X=YjiWLP@{zQ%AGM135G+-#T z###Sq&*y<8uDe9IX|!|*g6N=Qj4b>2!(ZtulJ|f}VtGKh0DhA9%^8Z>{+ZIs0P7@9 zOU~OLOE@lcCw*|o`axeOG;^w{AU@RAD1P&aDpjx z6TQKk@F$hU>3MU>Xg*O@j1I~%=@rJtg{Cs^FJ2J9Yl}ZS{i@(n8Xv!BzTabYm{8RW zcVy20*|g}#OnX*bEW5_r@FX@4i9YOCVTu~1#kZeXmcvA&gfL4~eHLQ}89^iS*`8E& z_dqc|RBae`IsGfrQbF9GkDF$(z82=HR2Nvnq8v=W)cv{O9IYbsV0>whZUs$VJ(0F_;M4W9q|p|!~0H2L<8 z@|?MMF;-$lvE?P-w1)A=BwU+RWiu={lutDsdd&jwlR6@s0Sfuhoa1cnUL!AcYd7na z+UmYxcNL*_oBEr}RJw8T>ng_`7RF)xZT)_oZ;&7AX1Jt<;(TG@lYu#iKQqY{#LqPf z&7w#k3x384qYL2G!IU`)Pi}W#Cja8F2tLJN+I{wwhcH~;N&dD`IUD%2ZbBlkh#+C7 zx7ST419^|YdzM? zg%h%vJXNB`zq96qkm^mlhq4Gp^x*eaZUUy7f|n;_%EnOh?ZW&PG)8~{$^$#D9h-jb z;$^wpXFVJLer}-1VrYReTSK|jg3bFq)>KR!JXYi~+vzhayO_5GeJ-!O`1eGT=#KjE zHg_a}E1IB#_;(ZdiU_Gwe)q0r1LVgaFO90oUiQ65v!c9#ouf?{19QOU74jJ=e%-f<5Pe*UT*wQSPa- z^UXuw1J3I@8#9Amjo#Kj+5E=Np7UIM3%c5U16e%tVUjYcXfFEh?pP1;dm@+MnWx!F zz)45?DsGq?;k)Uu_e9?#1DRfxC^Tin?!m!+{SwDFdgpat>4)(9$&>}jXf)FOPnZg#GF zPie^BEti-81Xbh{4=lN&^Om`ScdQEYOa8fUwIW&Z4|gs}WGQ@wLurT&XMDbJibOPS zzo;47q~9am3$;hJhp@bQ_oJQRXsX4aj4)3PVYRXP$4JL&%>F~cvs zX8@jEn%@q{=kK>s-k`hU&v%1ZbDN?z%j%r7Nx}76>kqV2@jc3)Udz`EoW-m~2(>ux zA=HK#3p!yzBPb{P3C%L%VmS5x#EDLp3n^;#4W^z$)Ox53 zg)cxpB~IA>*0)gZr^wQoagEb;{3kL7Paz>SG*`)Xw&Ad5-vVrv&ccAaNQB7?=!S*n zs7dIxs9$07ubFKx;;@%oTPw{Ikj#og2JcHSabJI>+V=Zg71s#^tBi9Ug4u@A!1wO- z5a7%XAfV!`iJ?^SRi~S$Jpt?ND{DU?{*wi5$1ola^Wnp0;XszUfkjIL2}c*5 z4~xI&e@;hO3ej>z`*@x#ga6tpP43!VZXs*Hsebt*30z;2nw0RZZP{v)_A)K4A`V}uP^IXk z4MYsOMK@sT;+Hd_Gz=9|neMrbMlA$K@HVhBG`n5zzY*;=`HJWZAfl?bTTK774e1Um zy``7Li!D3ibe}N9zZ9gGq-`NSS!MjA>VfV2qbnn8F-gaP<5(sE496y!P!|XRm?hx3 z2h|eX@w*$SgH?vxMoi4q04LPRFzYCO!?zC`ObBp4Uc~mt1TgBz4#M~m!syg%AomeY zbhxysJ<%hZoQ`DODd0g&TJpr8=jFvUCnagO;o8H{@TjmR2710vL_px}UfM?!1Fmo+V6fmCq*O|=$Z zJg)$r3~FiSe-5S7O1;3!>mUUDU}fK@ojtq6M}W1!fL4qT_UqgZ!i~m-aRK`CPA;th zDMh9J8`1;|$`oS@`N@|YF^f#Z*}`5V^F9Xt>XE(w@{cg;N4oITHnaf>66ZPrlk;w{ zDiqCO=*}L0rx*Siher*_G2~$#Yj*6kKzf?#syV|WWg6V}J1crY;VN$8)~RC#4uLFc zpp)k^_K_oI!K z=Aj#pc1KSidsj#tj8|}_C!1!jpEVu?Lp}7r^FIwpeW#6vos(c3>sK`VdwpSBo_Fz$cb7k=;Q&FQJtm;uK^nde$H zbm%{avOD$5im%5nl*`QqX5-0STgayZB zxiQKRB>o+WYj<7*=956z^#6#u2lqUmu5H+{tv0sp#z|w_ww)$v)Y!Idn~iPTw(ak_ zpSQm+|H3}!cAT@#%vwu-3|S~jP@rWx`W<@GBo%}m(m#YUaq?XHns@GzCzqa7GndNf zlMse4CV@H;EZ(QO&>mP>V+?FrLUd@JhS5%Cf!!**%1+}x*cw*M1DZr|0#lcj<@E-` zfDDPvL&|Mi;CE zrSSEDpPST=nhc+syERO!{B^pE|DZnVhWTG)+JEW119aviUpWAmf{Z0W69?_cDU(x$ z`+a83SPPijTcI})(!Zs8Gt6IBiB=vraul7)y{NaTT8?Z-!$E5Uz^C>HFdO2;7mP2R z7Je1!7_Y)+)Bh$X2F2UMG1v0hBV{rzUbnk(qKZ&E4<;LdeV!W+aWyz#_g@hLC9ztH zk!5frPH-aLUi8Y{8v0E8$ch<)8z9Vswxim2O|&JD1K6#?bl7IF%kHVxuUEP_TjKV526$kquf(fE-ynfT|{~V!{CEqf-$!r6p zRrBK)WSk3F`J-NzyqM5k_f$yK7bEidI;Jk5@{Qz(=<6kP1W}?#9dfJaV7W_L$d3wl z@q~5XS7a#Z>_j`~XEyvwXq!NOwOCY6X&DP)|I&-P{+)j@G)c_NvZ|49PbRU%di!p^ zDb({q&6Ec;l@AB5>x^ds6=Q@4PMqA%(tIQ_Ty0Mt^H6xM@RRS{2zHw*_JMsRLQ|eUnzXL?RMp3PF5<Tsl^| z(E=~`-YaaOJ5?*(P`UJ4x<|igmIu9sx?C$)-AEL_Q2Vkew<=%Mq-C;Q&+2c;4R@R1 zZE|A4%H3gyyzdkp#699KMBi2K^(go%u-6^=V0FZ}6}zR!A<~R~#UHC0a=hJC+ul~B zEU{c_42;f6I?$`@TRGj2r1@FM72&-1W*vfVP7z6s!UblD-gM#qq-L=UFf{lvG>X3j3U?zXSCmO--s^mYTkBbRPt;Y%m5*qfW8^opBQZ(D(f*5{~oG~JW zu7PsR32c2|doBA|;-;-o?5jx;AI&kd#iAe&Hqiw2D-^a~FV97Gv@nlG} z+aj|@RevdoG2iQNv*2O8sIG_`G0zcKV5_bxQP1^*EeEVa*(4)D^GNpvB2Av&n=Hwu zJsITXkPV(cr@Q?PT|d6_#`up;T}i8k<9(VpjoK_fG;5gWf@eD#UUz^bTM++CbC0hj z0)hbos``2D2q^vRBV6gZVDnj1yyc@296UaU>g*IK{n2;s%OR`^?lGFwUKbzL%qc~J z`+jd<2v;60Kco`8#|H$XZ)Wp1cz?tTX0D!kKqp%CvssqRxlpHNGH=|hXH526|03U}eOG5RzjbruU5Ksk5(}G|`P&Te0Jubj5C;%p zh-2m~oX1SR$AM_?{toPx)IPP?t@blXX3roMc!cxHQr7ec<0>SJlpbr!zwq|{!T<(f zKnF(ve*eI4eZfG%nvmRV8*7YoV0nog?n_)56GOVfO3Q!SznLm zD6c%<)HXYg9|ZeFy6I|gcf|X@M?48XB=RM6c^}|fdAl8xcTJNQX@GA?FBSw_r|}Oc zG$2Z>COHs7`iUFUD=Mkkz0aSRp$6JGZqgfA_N#{s<0V_Oi)rRq1mt|ePOK@OU8|Wn ztg*I29zK;XV6DCBDF1T=_W*Nw}DOiFgDpL+bw zGJ!+0RiBMx{mpM9F85O7Y%4L1ms{; z2$=1Q8b5mq*+R-!w&0<7zwHi3KSno1zI znTOuq0|DF+CJTxiZ9coAvu>Q|G70SkY31v2F~~nG@1QR%Pym)HUM<1@x;yrnbu&|Z zhkcl$G(J&B8E^-UHS;nI6c_d4cvjYjqMDHM92*+9pU=E%mO#2J7Y8}5#e-z8-CxCx zz&>bgmZrnIA5x@ap#y)yTH;Q=H`0>^@;FeJ;o1bP{4hy`TiV-6-s`PvQ?o@RIE@5o*9+jT+bdcG+JxsO?W7ib7YNoCX09r-m& zVpTaFr^Nl@WL7^s1&^z%7kAUb!9KYU!Kg4#JgbyHhJXlz`T)# zm%&`tupbYvMC)w$M0~ljbUhfYLo)s5nyU#8*ue12~9s@r?#r?MpGbfq*QN(}O zLs4HS-~g1`X@m^`3L~Vwc(8O8PH23ZuE_Cf!!^2)d*lM!@9@kk$w+(W;%hf#w5sft ziN7=623V(D?usG2XTxc@pu+QzgNu8y7+Qmr&IT_Q&KmnCO{fdM4Zj1AvD#rBGfq4C ziK0J88H?GBRxjssy>xjTo4j;7_&jnKa_=5-)%7}XNdI{rgNC6JL4b?_D&(e;XnPNu?J=s zekf8_J8oXE+MG`#!d{;E5Jw#MxKZLz=i;62@=!-ww!!$~v@|YHKRZdQ==N`((y8^6 z1p>?c+gBxii9iBGETN-N03!2$$p1cT+PJ7|&5@D_?2}auX~!W7az9%B5DPDTdfa64 z{PY=M#bW6$ok(&MeUu!I%ziIm@2~2t-#YKO81d#TJ$>g(agV19#lN-7Z6>Hm@ij_q z7Rx9XQv1DKPpxCTGg+tyu0PEm%uF{gml&x!tVDvWG_FL7wUc)Ygq#YhdLQ${j{!tP z!BFw+GYO{U8p1R2I~8->Y#FO~u#8h{7WY*5LDUt)wgF2<}oUy>6o5pHS<_2tJaJaPA> zu!ZH82fh=GEEjiF~}A>Z51VpSf>{l-64h;4@5 znOnT{lE@$iBY-kLIlStajH9qYksBf=R_ey9SJbOJ-Bj5v6a9+;_h!1Xj9lo^>b|D8 zi5!KfR_FPChFO(N>h8MK8yRHZ0V%R8Y354GE#OrC5rx2<3jvtX=3R@tQ^4EPy}vbh6!QF$#JkV2s*`b70dR4dc3+`F#nJ13&6PU znBc_xB#(`Z&`-7QA|RpwYM`xA^60DL-`UPxBZ9+@%@Iu()$Sr?Rdg@fsk&GrG zTU}2y5=3;vPZ`8IbYZu%&1AOye&Vc2VRmJH`~hbWBp~axZ8nqxY_}(CI;Qp3hq?q& z_JZPfBREpFHQp@SKr11IQoog!$(+FHd$ejlk+o^pKXmUP#7Go%VD*+wIJ0(z?1AZg z*rp%}(FO^{^3ZHkI*6{$vECA&c?A~r!qa{3H^jTMVhk5{$yimLhw>Mu#aJQ~gZT;k&oUlst& zu=p1s1z#Sp01q2ZWmCW@1?`(sht&k*C>&tG%+60ic#I+9YHP;iOc0Km7|-Cg6n7wl z0U;Tq;216TGazqeJ?^^d)K82EOj=BxUjC0QTu?IK! zKO`EcPSXi}Eaf?N6n zNxmuT!NGh+2%OypbI^qPMW$cgA=rn*)aRSZ}``?@Owg5dt$mY z>J$pTcWqt`Hrb~bqm;jJG+n-qNihlC(y&SU0ItvtMawzGDwH=evywFrat^r$S|?mp45O1 z?fw)1iHC$#pd3oD_0Z&uP?G?|aV5TiGE|Qb`Hq5v%# zzDE87e1kpGy=k-1^O=O??c^yUrKK9P{?4Jsn=hQ<)0iqi5#)#)XII z)eJiv0yOhrFcG8td6R~I98z^xB9!H(j}3E28=bay-ssDmP1V8~UUmgHe^$Tzj;6`uTfs3i&&5XAjd|Qzut)oq<^vb(dX;iL9 zSPl;%<7?4=&lrav%$j##_M5fy?cfNmV%DIk&eX*=iE4zMhY9zXYF3^`F90FZEp0F!TE}3pN2pNyG5>Z zb%CU#_MY5og3t%RQSpQB21e}PCg~-ZDVR)WfYXF#sl%AIesPK6l?^mDhhvSzXZn>) znJeQ?VAVpLm=KZhg3Cxp3U43hl~p0VZbMM}`14}RyeviM0dpH&z7v>tYGKU5b)TY5 zcJs^m;>^Y4Jl*_b;cxz8K?1O3k~^~kn(-}Dd*zr95Ak<=l>S_Gu-{<7^Q$s4jxDfn z%w)+7CA=ooQxg$q(WQ`%-bKM&vI?{NSu2NvmX6>VJ$=2Mt9f|Bgi)Y;fw})fg)9=< zAX!u4nXKQ9Xb@ow9`n{$X)Tmx38K2eeGP8I;y3GDuNSnU!uGGwZ!PekUf?hoTxB|U zq^Mcy`{LR3Q}~wez7sTI1mtSx9vEh@Sb7*+{?j+Me{5e7k?^XQz)$`ZlwFNk_BQ@y zAYlMyTeVRlUu_^cO6Rkdux*Y~UgVaQ^4rM*{b5|dZh@NtfdTxdC@*`6@3s?iBc3LO zw&&-+e-qH(F9>7+M72#vF8~6nWo8W|UnOa+#2Jw}J}m|_KBwzkCyH}WBdy!jU#Fz7 zr7K}Am`e@VgJdi|SjJqH-z3n97=L!BIyC|=igcIwb}tq@;^|^o$jg6PKz(LWjm9d| z{c5nv-T89r3?0%1t-+Atbx)ivNl>}G*00N6DNm8WrTCX~OYIn0R=NT5It ziFPW0JGZh(j!&FlgbY*HexiuJcGxLt#?ic4k0U=z)t}eB;?kRLD19L&o)T`rT#CRM zw&5+O_Xw9$k^4%HIvKe@X^;DTCxv*m(cc8pWLaHtM+o9m(2UtWKHbwUVa?=UqI7;i zpa39fn27;z1@+!MN-m_ayhEJ0rMe#kk?Y@{uNVE|i*(_TtkUOG=bBg|Wxh`)&&1nji+=ugVVNy9Qd;_f) zXZaN`A&x3`HtOKeD!aIyL5$Yca;xRXVWo9p|h?Y9Ce-t|Xk@fgPBdV|s$Q ziAV>tOSFmFRxj+4vFYLc2P>lOAq(CbrLN=D@9FZsqIUQrwEaQvgGx+g!J!O#&3_F- zy?ejxQ33WLXjiWPR|8jBOl^WhhfmqEZQ6M@lM%FSL(el| zkk1;{x0uaQC!7P(P&$oBjv75<%)@QgY=W+&TH}M-b_4>dZb+}1Vy{!}IWwf+Q|a_f z5oVOmW2B6iwaIrso|rEeE5}PZw+Oh9GW0wpGO+%^9)@8kgOH=dD|u=;j@_jEd)LQ3 zEe(15E1Q}MEiR_yL_oVLs=a8l2L$9EP}gl1aVyH@s2(;Uae^_ zTwL`6uBo}qLyKINVCo6oA&qNEXlvr(0A-i#9B=6XSmlq%M`1&zCL|e^M26~X{%}H* zCT_sU%ZBU7Is3iE(M$xC3leRT|M!jZ4}LkJ0i4RuO{hUWao;*W>n-u(?WVgx`F8}X z3rAZRs~&>-2emOorsiLYl>Sx;u-hze*pd4J#Twj(?FZJ0auy+A=Saop2>98$9<*A- zWe*I!u=e0PR_$Be88m4ygbx_UwT)^*v4s zy3V5OWOxscwJ%Cah)O7eE*@hwbUA^hTOF?#UM%sykaXa0J_yA@_($I8yYv}JUFbqs zQTSG&^~KcVQ1>TT1YCyGS>$WpDe-T^>ojH*xKbwrX(%_s6h&n5%Y7#>fL>%NV(Z{^ ziesNTf;vF}9yA=pih5SSMVp+~%`kPL5Nu~0DG`n5}O}J6CzAC9jrprD2vt$0wAbX4<-@ z?I)6Sq=|)5A5O?>j`MN)Wohls7GBHaEir;PYE1>{zAU1ktq`D%@WoIsirJ@wC>MPq zScIgO<{|e0>a`82wZ#V)VpNN23uOQbx zPmpH$_HhvojR;*lu%BTw)KsOV1wqA@_q-R%lhiOQ?tRwsA(`M_hB4lt_b{0P*>e(E zq8wpz34%_?Y;6~agNhr(!rzd(!QQ*xB2wXOw$OCDia#lT49FMC@$%w)^z zL&_^HekNAfrYqt)W*PjYKO?S{q+R{PQgPp>mVP1pzq>=Al`kz!fEN337t;TAjCcX2 zsx@ykMuZ|H*=$K(bFPo>XeZ)`)PsU3bl zWX}8mo|eS#u{HOp;+!)0bmAL{%$C9P$?H8mLT#%GY}7S@pS`f293hh!{q8YJQ&vmm zK1K33PSCvj4bxTQcWcSpTO8DCO!iv1Z>!n|5WSMMW%gc#WQ(T99{>Z61t_Vt&|&*ENqkx({LEWx z%t!_pCisbqO|keJ7VmKZfr$GwgMIsoU*+9mar1cRoH*iM!0F;$B1LCj;zOX0OW%57 zNJz&utX=ypeExZLt$%r80lZe0$echv(cgOawVYfInNP{Bt_~bDTK!Dfkfa_jp)O4+ zXX#5E2e&Z;DMxXb|2Sbh5dHof>XBm-lpGyKxuJA7@_gUdzJyNKd&xh_N|yf?WpIt4 z+dqYkhe|RXENz_U@dv`H%T^(7C1>-kW3LEm3Bx)1 zutmLoHf&qALJthE_UAtMPWoC+*msQ7Ik;sMd03AEcV?bcxJS62}{&Pj)nOmT2Vj8FK54VtpFU;N`M=U3l1UrdtlWnD_ z%A-O7I_J4F{%L5A5mvytcN$(W9u5geA@EE6U^}hFc3y^6(ubR-e>PnHE7Y`Mx4mTe z+pL*JtL=Vl zCCg;pZo-Oq1nWY?g5Xbe3B2^Pj?JSZ-z9rL9BszTs&Y4;mK3r>V)kg-Uf|{^@l#2q~;lCROTc>ZOs6 zgCjr2dBaFO$NX3FcCPh7`iZlISX&SR4^bF4K{_wOtpjEaqG|8FA> zbo`}_3()R<3Yz-g!_HnorUex`sK$s(C@H5U>2v&U7DP1=d@2QGPH6d*)GkC^wZcID zNo;sb{E6IK0@NqTMC6cN`g{>r=gOBeJja+mkw#WzqI`)aJV^>>3lvS0Tm#|CWS?6; z7e(|v-<>dUdZ#mGAz-k};cu2{J zHQkr}PmXF&Km&T3GlyWwy$an_!yJiEvA>+V{SCqGu6;(5 z$-gNqa;+Oy&YYD%DS(Y|d}yUAs`c3@eDT=El{BaZ!u{V?#NwsfkxpV7bBxoK)}9oD zibGu(DDUn=iTD3qtahD!@!$b?Cg2AR{};VoQMt~|PpOFY=&oC#jIEs76lSe|^qU;- zR4D}alYFl)MBTa^@7<%895M>y5VCpdg)lVqcZG(0;5{)trDGy;OH1m&r7(hFkK5Hk z-+!a^NpA|G{l(Gwi!s65Vqpftk7dNiPJ*nGAMG4YKkVvUw#owh&}}Mbp~0TJB3p;; z!G&eSy?%F}o+Wb>j;gn`BO<+c!2>ypH}f7{)K#9LxtBV@dr1>btCX$f8kC+mv~Q7ASyo@g>mhFIOzaRQDhSLjk*TP?t{c9ax_|4S z_1bmG@0dH3oPQ{=!81*0W|?hHxxyD=RbaA_w1lDF=y<4(yA28&qyFz-<$d)9h7SNU z+xjR4`NVqbeYM>iqahcF*G-F+e|43xaP4<`&^{jhGtk7E#uSP;j#vYL%Lo&`dCQ`<1ki`G<;tH=zGe87 z-KLTh96JNIG(9*XL5)QJhD_fz=bhT;j>^+;N@DymuOJx|zHaFU)XRQj&cA*pmbCPI zKX6R@6My?ddVjU6SXp=M8YB(p`MbK^?p3S*k@9mox?d1uOj#coPlk27>)`K5mT-dJ-2+!&9z=PxEhjP%b>EN#u z+Ml7f=GK=Ur#8McYr*KM@usl+gRZKh^y#^uaN!Y5a?6&-0;nQyJo`l~JJLJE98wYC z(uT>Rekd*}8?ERdLNMI>+V z_|cn2XhC>gCdMtD3g7>^0X==W5dz$lDuwX=&n=3-P7`)aVIosEug+H-H}QAH5MP>C z8hEeyHYs)Icqk9d3~^n(XpzO`F+BC{$|Q$lIo<`qnS)~kg=sKqQ1ye^CXtya@YAm? zsqiuaq|zx+&dP}Dr%yrtwO!~|cB1dW&ZSMGCo1Un_U8LK%h6%45`3KKW0F{$XssCu zpCYX4PsF_~<$fjXs7t@cM8X?VDIEijB(^LbVsq69H88A!SzN*;Z7nLbW&Q5cz(G_x z7*$GRFCXn3TCnQi?R>pn@_K$J={(EJoY)_;dDGTM+@3zbjQA6E{&3BUP~XH`qzchG zidYcx^eHD2Dg5h}-_=U*1GCKgNYOeyNWF=m?+Q=(Tw)*b-u}dWPD{Hykec1)!{zy3 zti67@5dqwk%@YA`Xm5RBLPXk@lFM~z=o7GxC=fVpK~yd zRN8EWXUd$9?Kbh8i@l#@V;U*Sn6yVL%$$wHL?6}RaE<1ndmfL$ZLF_LGjK(NmheOy z`;??^cHy*12VtuY$~`143y>H36>uzmQIhuV!VVa`V*E=Nbmcoa(fn+OJ^aYNX_cA= zqni;%2wSj~3XgD=+uAVudrCHD&A5Nmk0xC-uf&%E#7p_sj zcY1#6EF=Y?-^1kpvf=ra3>f{h0$wRPwaS_N7nC)BhtdXv>lm_#NvXt_mT>M{E=oaK82%OHyv0~`itYi7_aU(st{&6fWgKS|eu)1;1z@Qu}9u@dP zqR=vz?I3wP8C;&rF{4z%>=+$KfMLS_ z_QLk_nod0cKTvg+BZ%|l58BeIFbVUkd`H}Is#kmD_I=W)zWb5GN9)C%h{jc$-}BaK8{7AT+?(3780gngy!GFT(+NmN zje#gm!vLaVvZ`SzS)$6MJ$D>G`R5ANRL+t3p*|CaSlmW-imPI%T zSI@dU&f9A%e%-)cEOaxN!0$+YyNLlBdOv_$P-kR>^mK5{)yIxh&O70uYq2PM3SXUSw ztMe=HnX1kY9$R07IEssT7b;a=`f&i&eSvMhuj% zC+)ZVvE%C1sB7GEve|u7jCYl~zTjRj8)Z585qYL{sf~2lE{t?u-QPVK>*@5vKWxbw z-MWLs!*2_2;(^J#Ks8RZpT?rXrXL%LC1?NY3x<>6GinX4JmWv+lh=c~tBR!mj_Le+zS)6X9 zQU}{l>4jq;2i8}(&gyL%^T1cR!|p_d-%xj;=)Yl1`}VEh3pbmiS+uYU={SX$yO^K4 z_&(uM)Dh$@Te$v8ByE(X&aMjo%A;!B>4}%~V<3$QeE+zfuDsXqZ>%;oir)@q? z^j4Tgw)WExXHZwCQ&)62a)yjI>42+*_}}da%oiaUfG`bYL-l`zT|u9*jjFOhrGGUp zm+tQ)1F_62kE@*S>qj^R6H5CL8R>5tok1Fs=*Ex^a=K9>G2OEs>hgqhuG?1dB{CF% z(V{eQ1aT%PbS64tV|YfT0Zl5WrZVzTqb~W$5Kg_y^u%bvny6a+MU`IG zv|C%&d!PlTJlZLAKP;fik#&rI4?i7j%0J_mB6wk3a-ol{72}G@I^=tAe>Neum<_Wegk3{LEcv`RA zkgJn-B9@o@9oSA+kc=-E^=^_S|DDEUrK=_!_-RqV%op3*3-fp}7lHfE{oWp3wzM3$ z?6nMS$rMzn@O?ZCL#VUoeBsQaiT0N6Y}L!$G@iNtEb&CueG=w(NE6xA6xt}^+UrBn7A$S zex5$8Q+t!AKnH9gU?E*(&%TT@`v04oEu5s;8bEmM{V>{!9gF_ZDcYm~JpA=a@SJM_mJ@*c}rrxFkC`B)$4JPuRoXwlk({0fK zwe*?7vEXp&)d!8j`;5TwWm-=2R;TCChw;=#TQo~6^KcdcciWC)kT^=&vvzU@XHDE+O70JX z5#=ZtY{fJa#(}*)mCrOyHxNwu8f~4@7;CjzSm}vIq`RWG;-J*)UFu)Qj=3UFFPDqC zG9oApc0`01uu}wlFNKS=({>+8O`-WW*-Ck?BDMM$>0Mpr-*|gj9cKsbq{pbxNUQ?+%J>`lkw+tDCVug{f#fUul&{-^cZ0=0Hn1_4^etG<(O{ z@?dGYs!g#i^^KcKiYiP&tPsA~*K0;)NB0hDirU@ej)@s877f!6is{HDI2m^zBD0_pSZ3anZqN9Fw`T=A68 z(a3IPx5+e(Hi0m%!<6WwaM57mxTv& ze+4lLHnBj3I@&hp}ezkl&2oe zLF$G)ipvGARc{y47tE%2i+k+pI2HsJ7^g(eFXjL4byq;O4dKFFge44=?S+z4E?AV) ztioEkG&svJQ-p-oM^-5x_$+aQ?M!J1qCzL|U(Vrv2~q}x}pc8`A>c% zg<@;f%uudVy}0hjeqThT>J@os=xYDW%UT}{2cBYxOYRmRQeor_IWOZJmTl46+Kz=n zyUqgVf_nm!BRdezwu+rGa}zs+wD*JdxD+aaD7iS(a*)F9#-?42Jl0OodzQXwEanPf zFM(-Gulfkjr3|OFlKq=lla^1_kiS86QMW*YvW{Q9+cNOVVZ-O{3#K{r2cE0;foduC zPwBR#B7ecKXnDzT{cz@NKj^I~B{!#zbo~n=!})A!4j++*Xi@Kw=Fcg3ZW~<9S2v}X zgleBo{xv*pux}Lo;a*U(@j`lUmY+s&{EZV@t7Tdl2`os^+BO$S^4BfOs472MLyx-& z_G&D^L0mytY5iHP><}bj^dRAeoOad!yA~n*0;B-|@~eIG`(F`#SJdW63RG1=JV_By zp2L>ll5|#d(_P(ad`xuzaUc$nTk&YGpun=~Dy;x|%EZR?IEP^Dkyw16@Yh#Rh8mNo@d!_{}o?b`aT%KlJM zF`bgu@W;I_S!*E`smNDI>G@B?4#nk)pG`#Obk2?9cJnkdCFyfpaN-L=f4QEHN%wE? z8Z6E@8(O|a^8jfeA>`Tcst-7_EQYfVai_*^fk?mbXaRUuP~IW`>v_=?-ElORdm3LVd@6y*^G6Yq^kFjs zH)&^Jq%M~deE$-h?iaXVThclyZr07Ajd_;qdZ=m{7-tVBe~{z&g<&XyB~0RUqjq!U zW(Kp(FH2L`ZP zh^qlQR^-5|Ui;RYxcV?cdd_m<=_U6+T72+m74x3RISOftZ<3GG1rX_uTd=7Au9`Pd zJcqaJoHct9N3NDjJ*ylX4rdaz8uyH2!-4Oh(hzFQa}W|hq0_(*FvTd`d^^JD%aWawW@%xAH&HCKQo&0~H zNAZP52S7^$GNJ+b{PxxbrC3hUJT2>QPcvZ^4*fZiWx?o!Ld^_i21oI7#LxVROD%8- zyt6#t*t)o7kC5fHNIg=7Q4`d&NJ$)` zmd=DwzShI2#Uu$q7uBbe)BgSSBT8@v>|K^a!R7iCiv)( zQaVV~#i><%L&H;#hXW>tcq{!&dFtGowXr`=R!y?*n592sO)j*Ki|4xnvNIZ6e4XNT z`x42GENhT%FWnm=sCy$mjuN>vnQV|Mz4iI`^Vc zxWpkiPny9~yPt4yoD><3>xk^fzF~3;)5s6|-e>`q&#^!eIJiNFaF8fM^aBV_8uWuQ zj^#tlL{X*(8+Rp_Zy|Tl9C_2z_mkoAa}k}Alh(Z8t~KzD-9h*a7Zi%Z1vR6*LZD6u z0WSf3Mf{GMnR;$ml%P{O=|j(OO4z5HhgD)+Vj2=y!DeqV#&6vfYf`Sqc$t~83k6b| zmU80qq2HG?k&A9sz7-BpwIgFkpG4gqHY3?*FGj6>XJGZPs@pLbs*#r^~`(SIE}$e%K$O(o$&^~Xu}Y81RCy#pU{lp$Ar@xrDlYNN#* z)E{;9>{Ql~aWH3w>2P(fZ*dH2+mv1D*K1&A;(y#MA%HlZhaS$o{9-Oyx_?0pZ$nS( ze{**Zhg%C>Mj>!bS~d5m+%D1xLH`%|KrCNMi~uF@)WeP6A*`hlz(Dplm(^!ZvnuD&E01rUMcFT)lax9-xR38G$tD%)?yrh^3=2#tnvHbMR|40U7DanMlDBbX0^F;X?Gx;rhSLIq?#Uv6eaRz7NM?O zOJ=M=oDSGnxpSuqk~b8jKcu3sA9;C)(g|=lo+Z^Z;dVS`S^oibv3~(E0f2ye&Ot#w zk^Xm-6h;@M#Es9CY?vGeE(={?#-kZl$iu!nCU?9_50)$Uaaz4Cw+Sx@DWzX=rU*}o z8qkH~xg`E53W4X?w2~{2B|!zHM)_CBwV4^_{G8MN@xW?@X*=i7Br@^?QRYQ2O0S8& zK#?AP}x*~KF_ldY?3dL^vy-G(wUb|&*r!|VmGl! z;~qzRW~GF6aOd*;7foDWR?Gk^3Vdlm%Lv%Fu43(m;HcL>s5`S}?>R6m3b)MBJ+{XE z(z=DFqZVtuhxO@Olqib2S$AR~^lirZ+(qa^#gvA#A&${-BG|&-tq=he7YQ70$)ldK z3mJTVF@J63zd`EqeBZI{mv}$8d^_ZH6|2*u)3pGPQt-V^+i?Kp{u9XRv014^qn#1w^Z*l2;UVT@L zNEdf7+NOa7$>zSH-yuY5hOySb)x+G#w_*`uuDPwo<#^yR71bYeo6}Xp5Cv&_GSah> zqD!fx{jAgp)Fb6l!yD85HG%wM{YRgzx>BVrcO8{3GY>c8M*NK(#Bhun)52pOxW=fh zW_{_*)LI&HvQH5*V(qsY)%QXY(xMYO>-Qm1kxXI%!s5oA%V%X#98oPim997h$19J->c=D2EtF{)od zr;ymQz}StnY$pV~hI0HDRDxexa9Bhw}p^aTaBGGYHZuK&Bk^bqcI!XHooV)r{~L0Sof?w*It@|ew6!Mx-1%|Kb9;Q-+wDh zhn8dp4Ooi8jPu)e3NO?%-D1zuxuc{UkY6&QTsAtRn#OC&cwIAO%#q_j2zrs7HI5Qq z>p0pv_b8l}K30GlAZTk*cqWL1Ky_0vpscipa{*MagM)R}{X{%DG5rWOZTEEvG)HEr z0Jhw8zJEH|5M+yg#r0KX^|fe8)_ZF)@SePaGiF!PtpR~@k8LBCN61fg?RrMg{lUry<$ph;z2aY5900A{KBl7o z)T8%!t3_SR{6KQ7UzC{lxh43GAADhe5Gfy{IWVQFkW|p6$*GD@iAV&4!Kyu6cIZrf zyzBrw}Gl-mIAP!3yBYQ)G)i~gd((7ni^y=ZV*B04qHF<#ZHtsA7VfzbV;r+xrJmVwPddHunpc%SC}43t*5 z@clZp)w_LMtamLZPQAdUlMp}8P`doKeQW#?mZK9ua}S007hGe;x->5Oi~ z5YzqsfyvW4*m4Cok33d4Tc+&N-jfgXxV~WB1>dtq!wyy`7#$GGZ3-8tIr_@EsQVeD zrMCtI_jqP@l*1aVzOsZ#3{}SQ?2x(_yEdS?P1H=N^EGvUkm(+f6 z3&|Kxao$!P*l7N6S#t^m%{_h!$cg0X@6Cjma?rq5#HaM1@Th!>;PMGLZbO>exKV@) z&-3aE%>@`O=GKEIJ+?Czez(6@@O#>l*pIdiLtfA5s6WedrDBb2E|856@$1@@f0(zp}|IW4_u#t%27B$h@hFS%uF1%aWN zCn9GB{rHmC*{WUShk-qni0z-y2kg)knl zvjOo2M6`RLann4ap_PgmEj{8@e*gn|@gy-_7< zJch7kO!GS4qnRey#Xw*`=^w4b^JVf3dQoKQ#zA3M15#X~xKiW1w_ixnr)+zRVcrB2 zw}Nm?QPrqZ0_g;AMYbz8h1PQ*+EU+v5nW)7O>leJOjnFlE5=5$$E7%H4hbA%~ zl2rT+_wSfM?MsLUAT;3xD-H5#{I;2zlmIrK+c~gI1~I4^`Im{Ta*I=&uYluz2u)3Q z+@MusxzB|&C{()N?73f>kx#3g@K2Xk!(~5s+e2(@?1QXW&YD_MD6n_IF>=I6mSi}G z7_DL3#TF_X*}@a;MsLo{KLA$e}mOxk~8fo-n9r3n2aauGQC1`_3*_t{xw-;*}U5RHip19^z2$H{SHg8;vWjQNU7q z>S&TeU0xI%XGXc;xV+TAZ|C6WnNJ@EnlR=Drf{ae_As}ot_-w z^b>^xr!YlV%dq_G73!z;#m@`im$fi#13rAifW?>yKWR+XrAYsa@p!ukjpGtWn$zrR zF<=@HqKHK~+F$G~NdC5?!9LX%3WNV_gj<~XYnBLltDg$J=1~qMqEtpQk_P@}kWaH& zjFT(Y2RT=PgOh>om$5Q<^mxk^J{(WysOQGH0|vyAeZP0c5mqumIHcg)a{$8wuz4)C z9>t-(G*aIv5i-uA)Sq)+^=d$kIG6E7-b4>@X}so8*s53(-c3J6o$$8>Vz)?n)EUi* z=(>X{$DVIDWb(eP`X^Fg>TTDQ4Q#+iFtEfp?mI~Q?KWJU4XB%nJ~uj!_Rb6zI)(q8 zfkNNE2>1X5#7Fs;AfK&oKH*qQ{(HL9dypyVEb2s6u&O-vF1;nw9uHhENH9#y@lWdI zIRR)5W;?9XO-m+39_Om^KR~-ZKGIN*yl zW27gS{1kZ!2xjTDm2gE~ZGm$m0Iw|xDz+FcfyKj1c!ehnGNHVb)Q}|ve;(EL!Mn6d z4OlkfD_kgpk7irDyGwlXXO*_M{NT6Nv;Y)f zcP~~cnU^Ov`b+@FK0V7*(13+fsVCjK`FE0Nho0 zN20ZNvtWaASiZ2_q=-e=_4?K8pquj&t*nzWy)v$@I;s%=F8*IXN+83pnDPT+Drl+u z2=WR2<{KS}x9{VJQjtD7V=s;(p>Sf4QPo|(A`&m{KoJDLY>XX0HlYH`ysIsdZK2&5 z;n1N<0>1!88yd_bRt`EUhc%Mq?T6YsjWc_1%FRP^M4n41)=ndql}jxdaSU=dJ2Ssc zT(6X0Nm4%t>FOEuhSjyQ*$*sFNH2zf{}y#5{;{ipvcxW2vuo(MbSHugTxhow*`6u! z;k_sbTXDa`f{FEgA#J+SjOu#A_wS>$cCWUdRMhv)apW$AIa1;`Wu4 zF*?T$n~z&L((w%*dqT=9yGCSVMMTd51S;?nnP3~w0KPbg?;|cH`3BoxL3XA5gB3xg z#?*inH5z`2f2r5x3rPTgq_8414t)O>@&mZ(!hnsUp7{RvJH>kW3E0OAVs0O?-a-=M z!-{Q19c|jrU8N(fY8m+GAN&VhzUz*12&p+G)NZD;MYd*(S+eWH3{#A+wqe$oGKsFC z?x4!`vW6rQGpI1Rc`H{_j41YAw~@b&ff(QE>kA>OSljQFl4%PCB?1jT?CIP0kaPoz23Y5&xQ0X6l2hHZANUjTq0egD`n;zq#^pc0aLrmG1iLHxl}{3Ef%xQg%zv8Co`~e zde-613kgkPiYlpSo{2n)+@XSFn+7Afu|aQm zU<+!s-lFno*(cP6f0@qaYkWe0@u`C(UIFjC$#yvm>KBWzM~9t7U4c1ih~daVk7M;1 zL3m2uBJ$?!-lfgG5ZHeYtm!xU^DM?qKQc9SB~+cVP)W6-Jd@^koCsenzD8O9xbGQ(NfGa zv0g6@3`7vs*o>BoLXFLxy?0=eWcIppE{W`EPi-l)PQumwOdAySaA(+fG`>gBj_!d@hDOIC_*r(lLyMPEgzc`6 znTL!q1-kEE*s88B$gkn@idc2|qmDN_#qKa^dt)0pjgA^GnrdzMohzno@ZvR z{p56jXs0M;h8kXGOw4>@89LiWPDQIziHjsNSxE``ePU%tC{NsFJgx ztGfQ%?yLn+Sk?%ST_f0kP^fiUN#if;BBFMekAnaKQtO^K!hH7+8HVMRu!t&pS<wI}u)v5iygr z&7h0+-#;(uVx3gvumxmHW$tcJ?2tB9p&5$k;fiWcD&-gI+|x=hRuXN0K++}>sm+BV z!6iII#`37jcVZgh5e+E{4J5QV(N#yOB*7}*0hO)&``lf>AVdKWyKS6hz_)J~8N_UK z#5HK^y#7Ik*IkJMwKWsSCvU*V%TP)eY{SP7yDFWJQXu++BnWEW;%|JZ-0r_EK)Q8u z5e4i(tDvmwMJ=cMCq&4#2B`dqX%I`OMxegCsKVol{zz9uQrXI(k>PRW9j?|89DP7QAL0ZQ%qMz69V!Z$>1XXG8w!hJ7=8U zlXe*TOdhDZp8T}p>-W#i#?_HiZ}W~gvQytlihmqJo?jed01g^nEI&Yro=fJNhb`VY z5|@GMV)6M5Jfh^=(>HC55j@~>;{=hHM?RTxhhN!tMKzy4Y8DQlh(d%v%pTNebV(Vc zi^w1mO;?m{G%JBYipnl{3N?2256q$1z~HWNK?S*e9p3(~10gh9->5na34pAL>cL(` zwY5qLxq9SgK+O<#V;hbT{%T}LPOBqOO)Yd3a?N;m=)JLMOGW#RVpwkJESQ8B6!Gc^PiNi`QCe@JZK$#AvxOFCub=h>(kHS&NW=Pp^9ezC6SM9_&&aMgR}l z%H`uunAn8furch!K<-Qq8twbj))vCHO;d6HKV^?p0a9KbpuE+GKPL^5K{ubAC}JPx zHi^<>faA95kG2a>3LUt#qguqRdnCpRC@aF;HR!WS4(J=M2U^d8leKs^#mSKAcuXJh zhJtFG*V~qVQQhs6V6c%^cjFn5muc}mX4f)SMQy0PJ%lU%>PpZX`X+iCvf*)uoF^|b zplZ5zFqi&Zn`OnDRu>er!s_XA76ycg(@$2gb+?)HmkUNUv27$-{8K>D@9b7JW`t?5 zUJ6>Q>&F2yk2v^M#{N)dO^>h*qzPDmpEZoi8X3h=R<-oMG!pXVAp!6hoJ9mYU2uN8 zh*QkQIrMZhNz@u{=0wS~pEvNJEF^r5?vRxsaRe51K*8 zTNG_SJus}To1w|N^Ip#Z%M{%4$dfbqyxWJEI8BH-iTF^3(#Zz@#k-y}~pBFJu z%6r|>_u~YNm7j(zpUuyx^!!;YST_o&T#(%lP#1Mc#9{%V=tkmrZXOsjFL|UVCayJWD#Ehkpu*Uuu4rC94A4p|y(w$>HF6 z_859=yN29u7URbt&!1$KkVTX`ONTOI+K(r0xa^gu#brEkB>gKB2o&=b7AZhj%v^yO zKt31VHe2GXE?L&sCS#NXfk@xm=<7Sc-#(PMi#v&7`f4Vl>+cvQ_Hz%utrduIOb<0V z9GZ>aTLQH|dzj)DIIrOtkhve<3d$jDzd{||qEkspE}QS*=di%v25yBp=SCGM+S>1W zx;<9Q;(Qxz;YGg%nH09U=dKVtJhT<2<>e@*l@F3Vf7CjW4ZAR)E=%HH#l9CCejVC~ z3L4^{>|jLSrN^`c1GEYV7KCH&r3K$op~ImB?2r*oyJ|EQr)5>}Hm-v}?0oLRoLAIY z@I1bZJYyq4$;rp_foZiFU6lj(8lMTSfAI7`T-WK7SCh!#44Tb45IM71@dR&`Z!m$# zr>Mn2U-A6;{(S_a($-Tl0+^UDqT)GGC&p+3>+k%V&@3dU8g=V)kt4t2Hr8SL(PM9< z=U~qLB`u6*M^56~Rx1b06@$H>k(V;e79s)5cV`R>d`TBqL=);lnzG4}q#6O=l?jUe zB!}|eB(#}>-D0;1Z+zdy-W#1rt_+sB^t)>4Jg~&da@bUecLY&d#{LJ0hHA7Ec^zt6 zL{ha=5RA0kJg&dBa$zrG#J%{PX;TcXtrO-{6UQ4m5Yw+#|ke zNhQ+$D-_ck{}pg)K){_u9>4#mog{-CExDOMtERtx6mv7!&&oe5r&L(zl?6Mc!i_g} ztWo6S5@F4>8x1W?)V_I_iC{qZitfvTw^+oRft?UC@TgQj=()QsCI@h2DhQ z-?+VnOgau}cWRy*^x3=GbJ11EX-a7x;x$S1j?tw!@;K}BQcYZp(cA&$RD}*IW3xZ)GF&r{ec}8 z9pQ@XUGO)vq?FPBwU|l%qLcwpt`l&a1M0zD_$(BAMRm$6^`WL@lyABf9dKUYUeg#4XFPinBYeR?C-j)n1^ zLY!ZW1N{R&Bl>M=CJ6aQ<4QS>==@?HpA&aAvJXbmlL05^qi%N?wmdUe6g68gbnX~! z8Fb*inJzMD)3KVYv}{44ZU&s3CYJQ6um}8*r5mV`_%*WCj>8L(3dC4!MrE7Eshzce z@4C)XkZWP0i;ze(8j~}R{U^(1QwCEsX?D~SaIe&zkr)~c{;7r43;#I3L^tbJP@YQr zr|z;-zDK1fO1HpyqjC0N-d~#k<@)q57+CQrqF@e!Ww!FN=9sPWkaeVs-w*6VKYy5@;svr^{K`%f6{AJ;;EMTnJ7%86 z&3=5m-vu@86wBg6l(dJ{kN-fstsn76P^srJBoQ8=`V3^W3#b&td_rBM>cXBqVmYoL z-08!1h=aj81KjGS>eCVbVzi~W=JDL3$8E2>xUfkxV3mS?%)g^=SJeZi=#io^3CZlBkJX=quiOTF&Re zpV;bvbpza)z+lz<3O9vnbbN&mq9l$@=N^R#f&b9Dqo$qq1QJ78+#tjm-@*R3Wzg4YVPjH~|JcwL>+xnd;xH>dpHClLvt1z0%o(d?LJEOpuJW9h0iaR{Q z;M7M^KO+h74TKDoW5QNZj1Q_%j2Aw6j{Iv|uXFZuufH(AZCMBRtP2^(2(-*^-1`#T ztj_DOyR%+(B@Z~ZACB_;rWc!CpKg3(99xVO@mvFLjbO|Fm`#TnsBbjJIo{W!iu%t3 zY(e_KX^o4~-;bMzgdns`-|sLh6iT{qF+5NC{&KeLiPf?!fkK#b7T#=8;Ul8VuXW!F zjhZ=u(Z=ejsY8J=M>aRfK@gzzx72$~->)&k0=gv)NtNXdNB#W?8@H=5H5IGuqO^g% z-pMJ?c|d^j_Dq)eZuK7A^4zN#r*s(fbZ=1dKB^THjfy1naDb}Q`sGIcN%<9hNhtuN z%1mLs|I;k+qPxDWNw6QM8OAKY??E;VMrIj;6N)UUgrIb?_F$*WauvrKBroTe@ofHb zg&E3ikV!atM&5b?e$LJ>r?vVB+hA@{3j=1^c9vyLII;E;6+zE*@n3Wh72Y6e)&sTM28sm|j&* z`TF{8Uk07i?@5V{`qk8&UFJXdv|N^;z=y1Z#9Fe~Q)x??EoLPZA zR)27EzVNACETtwI9nzQR2~EqnNO?B5+c}NF0B|m`Z}$T zwIi2RML%YPTaiwEjw5yOH;}$N1YY&l{CKO(H9Mwgh%3bU#xRaB^I_6Fsa;Myl2E{y@>}}!%C?eAxpYKTJ_-|fvex@ zOgbomNHD#F><9z6#pwIc{MCM!rPYmX}D#$6;qqVK(OE5dCkjRYQ6@+vVrS2xVNjKWVsE@ViQ zVJg*VvPA7r9_>PK{r$IkZ}=in0g%*k>EMEVy1(5^jgY-#Vdlz2KPmNv^KR8+kuJFp zymr`Q2fvIOg0L`Xxsr<4Dsv#q9kFAvo!TI?+RT14l?Z`tbri*RpR9Sr*2 z=Et3*d+7N2Cm4l|gpi7qEQFdKv&PjzmL$+TYN-);%jrWVBTTV{uvJ))gXBL?`yvg! zUD8zM!Y{+gVOd;%oCEUdA5uxBOp;zTJN4Y+H8?wz9hea^`~C-XJh`p>!49{EzsH}A$7+uij8B@xWu?&@O{zzm!fTh9iL z%bsq~HzUvJ=)2^Y=rFm#R^G&k2^9=Bv3W8f`zu7)ft!W`31MXktF8ZnBGmE~6jeY_ zyieom0ZmU%<}V*NBh$ZQk>a z&Z0x-le#89c{eKud^7Ox(tljrBzQe>QZ00lI<9%hAq>O3%uS$N86DJbeg+lO)k&eu ze1pYHre0-MfpuDVF*|s;t}|(vJER3vrizz97&bs=-3|eKt;XoC5# z?|PDpHh9rJKxy|ytMH1++4oLWIi3>ZtHgiCJlOm0>bYhG-EA&CJPVbl@RD@TIh;ew z8I6ih8WcU)hzk@z8#SM`#2RutiEMX&WEd^h67Eu%SjDphoa&w=ny?3GSS2W=^~vYaMVkE7A$vUHuIknY=6!RZQOd^m97N`Zp}VN! zx_xS_9JQ)V(vAv+o>WIwL^H4_pHciXSRGW6Ja}w)LlA0~p3pUFO$~&lp!F7QzRI4# zv4!%l!enK}#mZ-R+0m?vjLf0fvM)AN>tN{7)_)axyT26F0Ser+(10tdsBag^!8YQ+ znaDzKt~OW}d&vwbh(8NKed6*`Q^V!dv865jGiIGd^MD_)15Evvt~>nqEND$H7Mu3S zyIj(GC{%!qeTu@7iTj3a zfO#BY9fX5aD~(#;=>D-&8tNko=@!;yd=e7)xUE53y##U~tU}XCwXE%DcG?s0L|YI_ z(WuI!x-=sXk-PSuKnJnvR$}?zr19(j;?e+c)g7PZ{@=d`Z3;tj9Uu6c>y1!}CfcoMYK)G>^<%Wg0ny@^ zD|k7p7N5d(9b$toG&U8RWi&m!rIWsxD|Z7}lUMpkhzzhu)STWp3g8CY_TS>&9ak(O zvWH>eL~TxbR{r%#ZhZo93A%3mZTr#Qb26-YPD3F11?2k)3gzZfD3(68qeV)Qa<=6b zYo_o`biAiVlOj`I3IkU+dL!LdBp{q$o~-B41gP^WO6I%_h4gK>t_<4B6t^H)SAD&+ zc{bH>x%kkK$S3X+;pXFD%fl(ZLNlE&u>L={vmrbfOxm@)dc=t^4y zqjyz4`o_hzC!LG^clzeAL&%wMF`!e`Aq0Itw zS`nebqL6Kv1Rf;uyT zh72=UAE*RjoCF=3OLJp7KDYY+9Y--06Mcj!FkA0;DkDXViN-=SH-P&jPR^%3NFsb% z{+C)NzfiORD4s(dq`(J&%@qXVzoW#IiMRnc!muCKqzSRP&g5vcf^B=7Id1dVJb1>` zR8HaTbWmZurZkO=1<_k-oi13P97oK`yV3VfW#*s74-i+<9Dg4N&Gsr>-P%cmm}bk| zyv6KK7X8BTacgw2Uq(%DkjQmWYYscvLXZ+vTO90TSyyB8q>NOl83z9-0XvGT4YXP& z08R0XuX?eaO|h)$oOt2K{Anvhaw^CzVKrNwLA0vcIFo)Cpd(&QXI%P>ss7}g-w}}O zKsPPj{a8V{m|-(0tngCbk1syl`KAg?O))BkV4$G|(iQuob?3hVeluSJIsgF%y99f{ zEwGCzN{GWoM3n1rZkwgds@ttqc@-ucAy|U-k#*eyOB1t=LgjMc99|>)qvjtpBoh}o z;$Cu&RlbUEmaw?hAR>w@LT7ii?5IXDrhskGst})xrhcb*OMYCS)Q|_JT(`DFUqlB$$i$96Ke*1*O+4{Zg5*A`Fv~)T#4@B| z2?hyLJX)E|k^ccyK0Fwl77W>W)9Mc+86o&SP&1It-7gA@i$YXjF zufV(t7HBrMd2UNYL}e4P^d%?#$bfeFAA!)q7lAH-pu>EQ<3EYQFFy;N%azVp?`{X? z4i+g=LoHjdUDkrDy((0hmf~}wS$1ZiCNs1|anL-x^l>JP(K4dQPZDE-Wa-z+1|*re z%!7?~swo$_PVwCDEwV@c2~vB;uy1LHm{&%m%`!<826sgk!26=g-AFwlPaq2e-sq(k z1lx(gh$nHgST>%qGHQ!(M6jdMg|cipLf2`@mpL%(`hGimOAYg)b0@ z9=(K%kA6YtgB&%cHtAfy)?*eRcl^I|t<2q%`mi9fnq_(}Ffk{qwKjm`AL#OsaD`i* z#z`k*3g{}Sq_990N}$J4775xo(nFOoVx`TR3hIOP)rncroHkL#YXMtQXGdTU2e zpoz@vXG~nUC~AeX!&GJUNsGHre-HKxnJ=|cZEU8jVct>LPk4CyI)WXKL?1AVz zORnT8*1Y^Q@5+dUCYUF1eDR8Gigk^xUeM7m6rH<@)I)Z0^eH^ffj}uO2FrDjDdODz zf+dGf(ob=)WP0H|1|8m6omxms}qep-~9Qf(&8Ue_Gg~tlQg%kQWRf$9V z-`r)pzD4lEvf=&;1R1y^+ubU<)+>{b8MEk_s_7j67u{yGmsP(hP;pyS>Fana%fN@c2xeqwSBW+wjT5@i$%`xW zdO7i7ZTV!WS4P)3E3jVINYl6;#y!$8q6@Nsr!(YHM$d0tNYYTA7etuMG3Qs;1Zf`k z%US-dmE*mC-1WQbha3Og)Ru<0A(h*|x7+5h64WW7>3W5nLUv$L5uvK9$q4H!i>L)a zc(V`BruJLM&i;*+g|Dq7N|f|H!oYPsdQefsUlsx48vLicmb# z11^bhIGnK|rgpKf^!hZ!W=h{<&1?-=LaD*T0o&QiFq6~DCJFWAjO zC>q}HeA?K8P8e-Kk#YGI99Q#3>_p~PgJ2Hx66Sb|WMzJE$}@JB1SY|5_b3Yn@DUo0 ztDlnG6l+}OE_%KrsXok%wXeyu6~a*OtIrhe=mkNfaVe7`{TWT;Fi)&kP;A>KXr&Y) zhDBJcXz`X&r>8~bxc;p@?=SZ`Tih3M+i3p#n&dn49AWlohNrKi+)^juTJti_0<4?| z4n_T=@v@1LB&F^SAN}01$w&0iF3)XYA*pIBc6bP$HU3M ze!9U6h&jibx1op>`c_?a`y`8ORI+PPw0`L1`P4@KbI_GE|6#v5F@xAV^|#7%dS6JE zUfn1@NtTar4_6(zv2TlyEBswXh*dzUFWSUJ`WIq?)UX{uv(b*9QyX0`Vu4+c%gDQG zWIYX4(HZ@m+6~-1Pk@&Ye=DV1sWghRG?N^kCe_g~?5mm8b$(mml-CtI@P0${TjMfv z7n~_PN|V54#TLq$>rkZK=xZvSJNG2h~)0?*6Xvwbna=HsswNmjqdGeyrY8V zeYsrrc{u`#+eNpGG*k(Qnuet09EY>>9;_cD$v$Rcm=fkZs@J_NbeKV(>maI*e+#)z ze4bI_>HjMVbo3=^2oROQ`%U&AQIFfOT@znQ&*RDwS>*8^p6S{+Twt4@>e`=lEGq@YSSIfI61? z`*pat-m}*F`)Zc);jdhl3r5e=O~=a%9>PB%6b?qQ+ujEW$*i<)_mtp*r3{nxp*6SNzUM_FtN4Gjc0%Q|DjPzG9|^7YX; z84xP1D3!P)smiHsp~ol4Oa5KR=so=sH3Epv6tBnuAH6Y*1lLFTzQY@S_7-NF8 z7I2^#5T|JAnll0H=bWU>78t$2se4#rk+cd1-na9tJ$0Rvvvu5g^16KpMA)+>-H)qL z$lse+H2+)wU49uD0}MI?E?0o3-(>H9R6Nn3FcGD6(h>?jID8&3O;=QRgy69;MM4I^ z^g#)Ur#4S3J{R$SFvQ#2<5h{+Wv0CLt$3N1ra`la0B2jqeUq(yCvli)&A0IxVuF99 zMAhM9=@)!E4$P|$=);6cQ3{d+!-CE)1{a%TAu(Z&*pxIlTmJQ+kbAidJh5Zn3qrV@vrBqKqvPcKf)|$3r}C#6C`&3o_dt-cSSrl&ga6V zfqJf(FCpnK^ZfQ`8EWS_8#5om!KD0}6yCNl7?pNk9A0d*Qu4`wA}~>bFqwNk-bB zUI?aBox|r0!&va~FOfWcIhX<*U@?)S0e`Sk&k^|Y73uvsL5Ab|BkEoj%O%*cvs)@L z5*~0xzAyG`p-~2@qCcLSj(69iJ>ax4x!$prFwKGmB8Sbx@!Ol4Ddg=!C>$XQ5;o;R zy=jCPMHZpR-GoCfv!*-e&QppEN(8PEaB1Jfh8PiU3e^zj+o=9GXTdsDMmh^v)+o!B<~*_svuYY@2My?vFV$KC7H-e3Ys&` zu)U{q4hOv5l)ER?1QBbU!cL~>5F{z;4w}y7M`Z@Ve~Td4T_1)JhvuN`JehaT_Pv%J zMT7sg7EqsNGYxA9o>s-1XhXU2JtqU{rffZQw5kbDNbAW9((%Zn$+*j5$A{VASe*W) za<_Fz%9t_eabuVn1txPG1yT_nWXty^pY%vG13UsqeQA?=z8kXt(y%Bo-%9esU$i~X~0ET;(2$VuuWa5?RDHwmr#}W0|MKti_ zfBV}XUkv5|hMe6XUBH(A3X`j_!zwm34`xuBXOJ_NMDPSgZWab;uE89%Q8Oj(EV~`m zM|z{)=moZ#gapwi?Hz3zagW~|GZacNE~rp4o@(GTE>+zOEDZ2x&?|IafJM;z8ZgxT z#!n||*V4^rv(>CahuAebwsIR{SMq7skcbi)UE!wD1MAf|FenH8Qe!BgSh&kWK_tPg zd2;7;{3Vu0nu8px2_bbSCN{1SZ_EOqa7gtM5>B$p9AtO%gsw#j@w{RPj7eG53|&!`TA`z&$hAh0p@}VjuNcX;IwVzZZM*E`N930!54RNz)139k z`on(-1BCMb7f7@Kkn~Vh0sp6m+*%t|D3UWIi!^1hbW?(AloXi+P;tL1G%`L^5^`Eo zsCn9p;Mhn2!G(X`T5lenZ5GOW44Q&NgEyJh8wj9&#U>k0XsuFMDCnDTX^Ho%HWn(` zXc8lsgk}ra@iSKkh6Wy$Ip~3q%sy8rGumc5B1!);i#WNNr7x+|{bZE2qY}m1_hGq= z3?h!$TbJK1mIU@{EflD-zf;qDMreIp=v~hSm&lmh1O|PPf->IBe$Xr7wKOZkXs0PJ3+c9lxIDjM8B`2GD zbB(vVNMeo1`?=`6GyOs+crt$QFOfie`B(ycoZajJ1#w{CR;bLbjiIz#%g%t{j2Ydl zb$JwjQI;UsdyH!mCe4dAZb=g_t2JOv`qL7Npt<=t%Dx!jlL}$D-{8F=kL>DCVmss> zF?doD{882dJOm-tC2Y@~7p<13AfEbo+XN0537iNj0eb?m^N+}!7~(a8bcM@Tp6BVW zy1#(R1(%S=AGxS@nv8dtOs;6eHD&mzrPsB4B<*?-)*oY-=|R929f=r+Z@!i$r@mAI zqXiim7f?a~4ztmNY$`{P&2_xSv=Nn0yqW77P+k4NMPyF&84zJ2!+OGa?t0Tmd- zMSdmDUTSxDLx!W+o6l?9RdY);ktxZ&YUmuih)RBUMQU~9p_3Aige;w7gfi7JIKO06 zp!2`%44)ry$v4iLCR67z4Wz0En12g+=7*LxC^7{nE7&K~IkQW;vcfxUC}j?2M-c>+ znuqhKal=wh3LLAM*1+A&0#$vCTUmRE^Len<5eKZ>w$sbFX)ud#TE$(Y5zHkE9vq^9aM@r+7S8hjAbIj>I53ucweo^gb7|klUiJ$4waTIr3 z`2B|6mx1UvMs#+iK<2l#V*g7&wW3%Lq8bvK*1n)F?aYH6&m^Cishq z`x4Pz^$TTFC=5IG{iefg{JX=G3=wS}CIZPQ zWdK+h`n+^_9f?JZqfzpn8I-5a{P+A8<*XQ7e7QsH#0AE&LWLy|pWmpw*FJpOC6oa* zoi(XP$Jiq8u3)Vq+6p%dN|~sCQc&!IXBVOhX0$!mRr7xQFYW4qvkWl!7IBsUPDzhY zFmB9Y)Ni&VkJvt$@P!kezaDsy@v4{W*UQ3Q(K+3UJEm;HH1ID3G%Y}6KkM^26O2y% z;~+VIdC&Q>vz&AqYtIb)g91YTg8Bh~x(ilq1NmHi+e}vKL%E3S7|*i#F|PqplR|E> z{g{|pV*2KiXD1G7>t(w?sbvOD4w5!PmXILBwOaq!MlXy!BhjRq-G;|fzn|ogx8rG0 z-xtyG-l;86h%J_i;bX$!XowKlHQue6%V+YvRI;Dy)J#BK{@XX9cx_E81pfGOff)wr z*OS2^Q&jo;wGC1mkl8X&EnlI>3#=u3Wz{^rmj$K|2fupfjo?ih?&Q4=#XRZp%a}&0XZ%^0e`D^2)8?Pq1IFhX{t@4-jl{=X$7gjyXd2@(7E`G+RHKua&$P z8m@FUJ-Ap&_?w_wt&dO$6A3^bxavRE5{J{|j9nc2Dz0E~MMK+2%NAj)bN3&(3=R#>d)h@C=%Y z<*s07*V)I^6)Xh50v2oV8~edJwv|7D<95i0QK3GmUX@`r`a~1%^?vT3QWhcMlw0+~ zU&E^sfbEPzwAi)=X&7lpxt@b9iE&Re7 zn$*a;@F>V#YlJQB{GKbA=9&X)I+yEg%ZOy7u~iLL!_hcq{9n|2vA&{i3y3;Q?jOMG zPRh3xcH>`C@Ps$e?Gnz-#i@fJH&&+yiwOgcvg_c566JCqt!^nu1{g-ry`M^8j_+sBNKxpPZ2}m z9^8B)>u`(bNA%pIZ_BNI5VT$}5fz=8&=~-u zzExB3@L}qo2?ZQA)fyX@u_2gWI3Tv^p01?@*$lhTN+)mLxHI!0o)q(wr}+Iaq22 zHVuY1V!A($NzXV~Dbg3u*p8)KM~d(huo51ia`?9Q?{EpF#w?Ei=3RnM6rX*5&mn+h4(g!bC*EQkf*t3&5mVS#eR zudUH5QNnLwWv5Hd-aF%7O*s)Hg`)m-90x^m7}2HGVrDA<9Sc!@KbWSvEk@H&1G4S$ z-3zF0x7T4a(N5jcQ@yR36z!N2P7Y1Ayq=#YmzZ9PKf93tij-?v@eVHdTTKEQOC-sS ze08W1^7;ybAMT#cA<=foOS1y1zw6Oxg|87}BfRorEx}MmJmEia1vQu$PUI)auv|o<$wRdfk-}G z>;NuXX&t8jz4EcrN*yq`7RP)@!EQHzT4HRra9BO0AR3J_aWJcj;Hz~T_GOQU-$oVAOUJf;uGxi5jv7;Zr03W~F(2m*+b&y#`gf(IB! z-&og*3ep^cTM>EeJgQoox|3WFGX(YNuGu$56ZG1Sr>{=Q_M3|p<;;aU=w_P?81y2j zEEf@^Lj%+-)L{F5C7=N+FzKt=nH5U-<>twWLt1oAQPFv9+)pq{`Yns3<4=6W&E41B zw-V@laN~vkPMCYhKY8o{JP6N{T!2&6Wk!Px-nw+OuDp(Qs65{NlJTspK2hT>LwXLl)i4qy5YRb?qH)b%CKedR6A2x9uq?&ET9UDuscLzLHD( zf_hqy)T@+7K~e3g#@6i0uW88Twf#(&;Gnz9-g*K*lCy{i#-N=bLP(Zwt~P0djX7k@ zrqc;`SLmYneFuDUqy-H0K(3dtg?}VIi#W$r;LVOAGiCHzo1Ek&2?cw#Z1*WBN)3Fn z?Pg#iH@1ru`7Z}Q{U?VbfMb~x5gKr%X?Z1m$GnO6{99ojtxOnw3FG!Dq;zZj^h1*jT6ejfd^h^Bs($y2i3G#1}H=gx19tKt1 zF!mR`0^@BkJ-3vhqY?O`ig#9$KV>4+ZZHeBE}Pv>g;FxdEZe;-GM_Q)iy4*dn2I@2 zmTCqUb~&YknI_{H*FTQeQ`4n~5slolUkYSag!+;pz3z-d^i^Ereg(^=h4CwfMXn2= zh7LsrNYgf2ZuJ&wOVACu*bJNqEYRF!w&ZHNeBBJN=z zA1`{FUbLuAL3sIS=DXu}=~Gd1R55z-BD)ymmi0B?)IJ~w#;^j+mYg^2dDdNl*LN$< z5CS-ebqNohfE$H*QyyP)r=8pj(R{7V23A?^A-kmCjLl<6pmWo&RCc=zOChL{7ze+o zxSRBznJb+jr~3HjUIv=KNSJ^f$;pTHmgMR5u)h|npB>7WU_Pj_w2o+2vE|@oCB|#r zazM_CZo)R%>l9DaFK$iKoy--|i2)w{7!hR9Fq|u)-55a|a8T1WY0D*+ICJMr(nF9$@79Zq2 z?_~K#hMPNYY<~9`QjafyYPgx6u58gN+dp<^_km&93IF<<*8#JRTwy$3&ru$AfwnC7 z#5o)ScPL1q`Yk7DmeKA-(5ORT_7^n^606C0v_xozA+f~}p8WSey6!(bsSO#%?-4R5 zf9Zg8ppP?{u|-*|=i2m1f*1I0TI5ElK3zYZ6hx^&>`a%-if5u%`cH$YNEq-8TXlsx zH@1}X0ib5kl zE-A+AKbpE>Rg__9Uzq26zo3?PKL*};6MVQ74z3#P+4b_oS*shcGUuHN?LG)=a(8A1{A6lPfaZd_yr!3Ckbeq8$ zDu$Y`jIgqgw;0I*?tb&94K=Sr9z5PO#?vboE8;Q3*m^UGV*B+BlUvG;^5J$)U-hLh zJ1PX-Gla>z+Tl>MjIaoc5&nZzo1qhd9>ieBZ~Bxxj5wQIol;Xf;R&Wn-e0b1HGV~5 zmwjso?qwn5;E-^?a1{;ux%m5&1Xt3PhZ5A4u`k>o>^z9lVogkj*dK*~;*0mkVMKI= zonM}~Gp9}}d%0qNmH%6W@_vqiD_{(&A~f?sJ``Rz;ya?xsBDGigPg1DPj#5Sm*X>K zZ`fN;U$NT!GACj>?8zK&(omAEo{j9!{bMs7mp%C7Qw zuQyF`zt^I*_6aNBp!4EK4*hesLH3SJ96!=7Ip?+G(n$ZH-Dv~2KR|8YyWs7b9u80F z3d=9x4^sAB>Yf8vJd=e)1)Gg`2z|lvI_25;4OQb;l5-y8Cg5UroSx*A10D`%pNMIv zz;kYHVko3_tyMYxVI>WV{7%ZR!m=&zq`J|gMjY=yUZ7qR^p#FsgT`_G0a$s<=bN2! zy}gGJGv?3Vm5^}mBbqq)go>kQU%)y2d8W&g^Xx#*m3r#^hj85eX_imx{MZ&>IXH9}uRfWl`{Ny+a;uBl@KyqPIL6rwYP1ATJY^jVvT1Ipei=Sd0A zWr+V}M8QvNHvsl+D>@~}M;Blr%R~p(`K`Mm+jp%5v^2)et&Ym1Eq}fGNTnlU^_bEy z!PGg^;5spH&X!*#=Er9FPPrZ5dX%hcDo{A+Q4yV7GV>w?xQ2w;IaInOdMn3)`SbQY zQPPWkyQp7&oJX;xDTbi?RE<8xCSfa)zQ{Tho6UFO z8L8iq%7&VSd-~S_H}8}t#P^m~ue_!{D{jnhDye1ibD`22HW@@EI~(O9 zI6&C%qVyEzeMddUO+N7#y*t2LkB_DU~xycr!<6TiTvU_T1w^waS+59)Fm9Ac(G*ykJf1!cUKUbj%% z{5AW17m)k2_n8N+f^^FlkD!)sVqgBu622>Uqkk0q-e|RDXdaoOjUaMy+~x*DWp0SN z+s#VSNqU)C5MnbGcZ5k)BT;tG#3;{G>AIe`j3bs8K~;o+=3;M29!1H8u+HjHcCGcw z7}1O&owgv-fe;()I2h#aStHIzRf-sBJO4#(CBX9_AG2w-RP#Mn-XRq<>xq?!D{pr3 zLUw8Yt83jl=iddT#AlcufG|(1JgffeRWg9=YpEZAvF?bw_>Nc~9l`gvZ2~MoATB>R7%l&^ z-jFUIe)aIgvFK}2RpC~FuUp>d=;=1RPCD=bSG|q5Xt&QQ#&mlSzliN_ImF5aT$M+@ zE0t(!ROJPq*n>d@3F9L5^=?iSYGALgpgq`4>~3o;g$RBnP0|n(rcD`*XNb~p?#jPJ zV300Y=kB@Ylpp0#s)X1m@hRa2(@Ghtc)GUc&U@7~JG6Iw=-#9k5YmdMZcx8qu4KID z;2B3LouT@v6AVH$G>3*|_wl7(-{WPvYAe*CgCe4#RNJ9!kg8mHg?GflwTlPq?}}6A zlg$&rHcY6l^8i#0Aol3@G}h|XlLQPjm+Ox}ue7Lgvov)Zb+7SvsS*6^d8q>vOhOGs_CE6W z*WK3&%^ZFuC=XpT-jeByfn$AWNNAomUlX4fzvd5+RHaaALaY`=3hg}Y>Q_6=;|_M> zgW^wKW+GBIq>-Jj^GE$f0#f)y@&X{)&f|UkuRp|~LDkQRuTz7|oL5D#`3xN+*33>n zWoDm-a~f6s*s{1}{VkSVTW>`SmLR9g$R0W>9ns(6&Zl&S_z z2bR3`WuE{~uvB?)TgSj_Z_vo5ZK?;3Y*g_F65n^_WcCcIyKiJ2$00n9IV%RHK9cSt zOPHPjVKYQR*d1x-sOr5=IOP4dPuaSpI;y-oMCraT<6pSVT{h(VX5U*nd|+lPlRF_f z8b$2lT44!qjH5B4e-zfTtaAb4Twy3mN=CVRVU>O%dO7rZU9;9{WfVc{NilPWqS+eT zu(%h2dvC+W09aq-5mZ99N}|cmAtobci+VbBC?XeqKGJ?=vrh9ybriDtlUEh|Vg6X% zlJx;SQicrul&TXGkiQ zWhAT$qcBQbs2~te%A;?@{g=VCKfQbbUI_4I>i-oN58!hv*>a6>{5&I-?n+>Q-ygyV zPxt~pi8u)Lv$4Cb4+dQ`BI+kna(4!i5t-)z5dMclGBeCv0%UbZOc^cc<|gjd!sPN) zZVoDQ;b2a>%8bTO_N`kt5a!=zV>CyXM~m^iZI*&UbhOnBL6eMnf(ueom(`Jv4Ya|U z_*w;`Kfrx~;`BwGUuJh~+%+^yI(ZZh)(*pE;|G&xyqBsOQd0RVCHyNk3 zeJpFWcv!w6O-(|iS(9cG;jnz+FJ|&ooP}{hZxMREB_j+tEtC`&lh570buM52m(BD( z!TbPV0onQM|KFa9Fn_Mz_71I>0(6`fS6~@b2V*%3qt~m2V{_k6Ez3fF$QETeL9z?C z>LMMkGnWhaySb;ui7cV&Xxd0^$4Goc8PS|;<4H+4J&*c#AY~ofq>SLRiwhMq`J_}S zzTvk5J_nAogb`la;(-sb1J}$z5EDL_Ljxl5#)NQdnCW17F6wSN8OexSovKl+2D(u((PuGtgzVX+WoFLgQ@E5d|fd05-}_1Vk*F4 zE5FPMhvT8qDWgD>DZ>yiv5Aekr~Wz^BOO)t1~EWO8N7xUL|V6C$28Uo?AuZ3!3xRp zS8VLz?IGnoiw$>FFks(+p?`dW>qENZ8DNs?{A<=@@M-1`Fq`P#tN-r^anPXOx(xeQ z|5d6Et~Yf7T`an5Q$P^bb3b2db9}ZJ^4&HNyRY4sZdQpn(e*rw>%(N+ZVfl5b7XpS z7p0yJDaP2JK!SXQVPbJIc5kXdwH*~tK`>xfH#y(*rX^zHB?Ww9H zOBRyYey-jubo#Xn#G-!7IOHCnFjB|D!n@_=GzvT>7=!&0;d}(%9+-f){-ZMyIQm-V znaj`Qgo?@gPqo|63a@1OP%BnP|4<5+yOGl@Z(1ZjOCR5nUp%tK1E(;2e=zmZ1H5UO zpITimK)*}Ko zc${NaTzU~<*X*yDugRxa06>hM7*`kMW9rqXx>?o48;Sf%ydj6zB7SDO0V~3vyxAmo zX>c%>nfLd~-FzVmGDlJv5fk z7B6{(6&D!zVFapY4HG|WUT|d$3;DIZlR^qh8%$9~Co-$Q*?N){C+ty9lie@7 z#123hMuUqO8Jda8?_7-#oALDyb6>1)Q+ODKQN zYY(3vf~o1HUSj89&K_hcw4AYe-g}=N@?k%mj}y&$=0k}E-t62TsiS2|vEoK8QBJhX zZZC;TO)4jNRAFQZ4ZNJYsZ-;G@vXX`V<{jn+PzikWK-wfR2nW`ZbfMo^FG{j>G~}+ zR(Ie<9abk~5FAHXtt{|BAZ5`+zTVCX3t#X?!#4fJAd2lPLa63Vvkb?Pr5jg{}nV8(U3TX@kooDYq^AaI^vEe}+b|O#Z zB|zR4a-7e6^@#YvkND`fLcx(+V$S|fA}v10IuJ0{1ub)Sz?ZLdaT$ikV8lnT@UlQn zkpc(pi6*=eZyzyWJj2|*$%P~FWZzL2eNw}2TStnW}MPAiIYRnLUK~-D`*hAvTp0 zOUuBsg-Pjo?v6d-T2iwib2w5p9$oV^8al;6J6BDpA;B#2@cHp4E4m5qW5D|j0_3In zGLzUW_Szk=w#^~1M2(J<`yF0@?Pr|LurLS{CaG%v0}eBbOQD%qJ^8w^gyWcXag4;| zMXARrLyALK3_e8h;kIC=nQr8N7C<(i7C`_DQS9F*!0WFvNb!uAq^%iDeA$vJBrMHY zne)W>cU_!b**c_}i|RYUI8_=RmaG^zi*$@2OX|v6qA1r|y^&vMzQLA0Z8c8sX+B?! z9-&aavpOCzfrwZAnDra<|05(l(i-K8NP!Fr<0%3CLC1jzjcD*4W%(CMbZ^H^dbbFY zrM^|2Jo=ywXv3t80B6oU#bgp3tN~i$NM6kTQSE6N7KkS6<{1Js#}&p>L-k&370GlV z(pP>%WgmApj;44mIDfJleKUk(MqLo0Zq%PeYpxpYI;fez2{L1UaP;V2kFE3QcK=TD z>^~z921LG`b*>7$@hV*__e2oqm~Rd%DdoD?R$8>;8QvG|EBmklMj;~)Mw-ao=YNN= zSd3($`(>R>wo+8>jW>rDb$!@}k!2Q4k@g!RJo3WyZPCpo^pOe-@&^IBH5bbMsuU4y zP-tkObXwI+p z?(fSr9)=TT3mC{0iDJ46d z;-)jYo3lE+J-aHr>GvNl6 zR7LgH-&0(E_s_UP0dd3SbJ+r~y{>q5*&C?m7N%6}0d3@}X+vCVolqDQBrjqaDa7+9T*l�<9m{uG&~;az796pJ7eWaZI2<@pOi@F^js8kO|#2 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%0D^g&=)F^%ytus0^_mq(E_MzZ9nDT` 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%0Da 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

qE1Z<-P^ zgk!&eX(1mSLZ6a%OFXQP!eVh2P{z;PZs$^L7E>p~Qo3V3~D+mJZT#TrV2xduOs-U~F^eIYeJSh?Ee=C1pa;lD{1P?-drZ z|B6uwAV#<(oai8*%Wrpw>kM|x`8iygF3K(nU=`iXuSvJx*Cn5p&VLAvyVofeFpQKi zi;~U3zNNUb_uq!jyDaFcPvgl6%i^HX=v1KifI{jW9X8Wxz-x?k)-ow=iDno`-O?Qu zh&ud*gi(j)MVm-^guBbhI{NiZkVdp=acSt~P9l^#YxHmMPu;0*E`rO1(t{Mp+Ei&r zZ(X5ii2!~6a~iW|9&?a`PT~tXq*ubpC01D^Sv6)D+KEPwQN{& z%F~s0i!U+xc8a6J9%H2ef$H%Y{JhPEc2Eabb-bsTts80Up{xAaawPI;mL%)-g@1sd zJx#-23TaKx30r*7e0T9C@)$1d@JHtZ_S_su^IC)h5e~{(3ae$cahdvXC^LzKkebiK zpD2HhzB^6b$yN!3h2Gk{X>0wUgZ_qSIPIMl7)SI|q-M?w=N+6V#4_%U`dp+N)FTsF zJ{PLq!ZfHN5_o(|ae*D(#v#FIzYz%B+go?}EnSXnR~X^!65OX5)Y>^<+Knl$X#E?3 zx+ZSB%OK4+G5&cQYT4d_ovuDn@s4VpGl}(hiYa3I24FFKwjoQEDG&B*IUp|qX&kDI zTf}F(n9u-&@H|IlyRj7V`9kCW7jz)!ub`I#f_{7tFZy3uxAWb1E z)ZD%$;acomP3rEOIt`aq+%_O*f=Q2eSU*=)*k$g;Y_q8CZ6@T9Z|vO*ogKK#g;@w+ z;(|h>Ut-G9K|)QbZ~M_C8cWYaC~w-%L6rd(s!PIcDwt*+9oE}4=JvSO&Gp=weFwYu zLBQ=>I9kk}(%P(05Ny!Ey`WKMSqo-+N~QYXn+J9ka`Eh(DHZ8|q;z>WhOEzQTjg0m zzbUtp6993Td_a3Qj!0_=BCOa&8p;%+{B0VCQ5`JoUru-XvMB@DlqYTg)=B=pc{cJV zq~VnDX#lqg&r1Sd^~xqRstD~WQAjW*eE;^F4RRz|eB`dke3sW7LvpwcbAXR5n>K!R zzN&}Gl6Z$NOd>H_Kym_?vpr;jw}UVmw(}nC=B&TY`uZ~1Ht}HR6c^2017ePSJVNu;V)J=aV#+iz>9!B+ z?=zV?kXkf@z-C3a2KFCXzJ5(iOK<~`_@eKDh=QLh9DcEV^V=Y+w&5l=Pv0l6#$I9x@{=-O~nCt?XB$90^fVO#k$eD~SS!rE0ExI^Db_M##+<28Hs7B}~pm z4`!a3y((bwKz7rY90dwIy&%)M1lWb}|qh{Zho9 z{2_}M*G4XDxnJlYA@yFLQ3`g0T$Py{d_^Ihv?xP2zTEs|avUvLReKWi|6=Xy^%d)K zK&%5=R3HJ}{HlW9>MiDy#)FZLC%XNM-MLUCXlcIZza8Y%z}Dut$Z^5aw2s`~^m=im z+MW|b*)x@a_}MglFS!+m%D>}N-{Ow3$aZ2eG6cG;Ld5St$sosqy_ZPBIt)TTxDE(m z3zHN!7;tszviC$2$7C$5q!@?oNwBA01RG~&j4HPeB_7x{x6UMRfV`wxg)1xohwChe zSuVQG4TVJ!4S?Ie3+T4oKonXebG`VxEYTDOGq#Q;sP24rYo5_Yri2=6ZQO$K7_$I! z7T*TOL@LT|OkK~1v>;t>X!tMV`+Yrh1>m9E2tXr1J`3OO;WlUYxF!9#^e9d{zMG;uEHS8-%Py;jXQIM)3patfZSZzv@lC+z%0I;L3ClQu)oqV0wO+TB9y zTC-`!hw&JQM?y>^Ko^+Q!;0W|Zt*J6;Ndg7lw|wh)WtHuq+L$Inl5`dWt7fYy?(^Y z>kwZotL`?EZHs~IQ<-m*3#||KJz5pgZLRDNbR#pRKvgTdl>)E&K^*~~$GWp6hi$sD zW5$gB&5O&)$gp5iy7e6p6rUo4vh-)i(g=2M<)mdKMs|EDW!TK^}EF~_Km-v9WQyo0{tR|$xpaS+ig zpf4dqXgu`2UxDs6aKsA}Ne!UgIky_`^!797OJH~iSQHr9q4^NXPSILZ=ot{k81L{L zGePn}92ks{C3A3FD(yGJt$!FM7@#JxtiI*wRczzdM zT0rLL{v}M4H!pzk0w&@>l?joZ>%ybBC+z17Hfh6M(!X3C`bAL%px6#8VguDg_%W)b@mAV@MR?wqP*6i?LYQ*t) z6F8OjcZf}Hfac^ys9g*COaV&OW#c5_FfE$>nrakcFgn?qO7D2YEh;wKPlq<-%^QH9+ENH~j*5_f3`nztdjfKxUd$ z*KLd2rP@s3EV%^zV@Wd*=;Ag@u1FjnIxEx@{5I2fKz90dPOfJ-2sHMGKUFAWYpUOKkFz?bDZ`R2c z4gp0Ewu)YeeY5guo`4bpyOO=@Vg7OV26={Ic1dmj9JN928;kSRZ( zqg6BeMpBO1XW`#m>W1+tpIs}E}TnW21Xj99qsH6^0k7+GT4atUmTcOdP*Gq*g zlTTq4Q4@*6Ekc9segq#)QgLyc$<6FO`jjGpJ#&)CH(fB!`-ck1G-+S2&(S{OtZ(h9 z%HE03iS?_W>*)SjvmR=G$;oSgG|jW0mjA$gS0P0GgT&pK1bprXX|bM^cGhi%GxIMo zBz_^(0uU%B!6N`gt<%i8tW;fE5N?pH+*(iEYleS-C)YBsq^?sqWsVu zU~`qRHoil}R#F=u+?=ZKpCH~a~kdXb?Mfw|X+N9?qej$2^uIW>yznbRfs{h*CMo1bhd$YT}nOm4muJQEBT3igcm>Dn7z@ZhA>z1*=p% zhe#(-o#MqY3AkyioqfUMAV8UvX!Xl~je)eJl@Djda7#&buf=SGp&1fmEurD)22L}X z;+g`(Y4AfrDex11fkzk6>#tLG!UYt~g6-(neBjRT&^>nv>h2~+%Ur9vp@%cB9{VfvH{cw2zqY7sAfk0&TRuuNz@l10B2hiHz~^it%D-QE{) zB3+?UP&(sb(I+$fATg+8T@O$F<~hdSw-T(KhkpLC@v_z~ivIhXNIewUKFg$hW1S$B ztyg%>kSMC$bI!7MLVGWe?TdvpW&54{j*3V-e?7Xnp=B9-@k_`1#S^EYcp8?iLRKe8 zx#U~NnmOIxM9D$#U|fZZed2nc@S&E=8!hi7$i%mGwVpVxY5%qJ!Z1!&v`BJe+g{q1oTAcF} zx`S!snt}w0BaR*;Q$qU;WPqpDhyuB|C=pVO|RIi@~=5aA%79T%8W)D7J`bYy3*MLEDR3-)% z&i1F68pE3NERnIjJbZ?PmREGFDVOH&az)S#N!|$Da44t{jc1r{d31ghkE4lhIA4xr zFLW4SiA|{R+V5Nb0$8yz98}F#?yHr!KnP%TX8&w@ioa}{05m~&qZ!n#Zb}S>*e$4hnSGr_`ekg!m(xt7{%jO ziUf*EoPgAsNNY4Cda|YY(RLAVHbow{6U4v)N6?GE<11MG5IrY1zAfk9CSSl%zqfBH z^q`9y{jRVM5nVX`%TBXrQVB{OR2!7gapfIzNLxi~!fSig2~UJTPI>Ysga>O<27J28 zrk5~4xQRE&OpA4OBqLe?Qae|uJGD-2A~y%Jtxw0tKOQ8vE+}vbEY$SVnW@bqF~3&z zO(%B66-D{i3nC3Mx5(%28py!G+F(C8EFzhOw(7e_OIgFNQy9pqCIu;M3b7br+G zn;#=kdKB>)ODc~uTuXp`aS$)A-u)z)l|~jeRN)wNyrnl(O#2s}Rcj?Ezjtj>h+&ff>1a21bR1VSmRjE|`(n7Krz65UY_o{6FMYe?!W@LKZrCrxT zS~Q0n`#Z_b@=jT>K0ZmM3=g-_|OiCQv^ z&t-J_y{_{3%uBey#FiY)$xL%jYMRWNK>X$wQ10+u#t>VS)lE|N4ovno9E7)LeH;SY zAPr*hatG080lp^jcy6aLq^NkiP86g4M0F&|^UP-a3CQ^M!ypo+Ku?d3&;Vb(VgkOH z`ir1!W=U7Ye}kQ>FXR>gGVW)E3GfG?3Dc4@#)ZmNqI>^{*2~_tEyP}c;O8W^pRD|T z-3eQm6_?8+h750O3QL}`kZ7@Nn}m}4z<%ZC+VI+CK?#k2E1nzl8|!Wl=-|6Ju9I>j4y>1BlUjL1N>b_)J0WvDdbVvVHfUKHmFRZ&il9@47cC|Od@(ugt zX6%YX>u1S*vfP!J5$2K4+8WdpU)6RrNGQ#2QknjVis#G~F>yg_ipNZ)s(&u4Kc!V(d%dQVH z`ht0RW$n}i>X8rRgQ(@g321h`JcDY=OsAudD{$K^|H-g45`+s!4tzUHQ0J=tZ z1-}2=c270V?O_y&z6)snM1d7{kK#FwN~q;Q+4H+w>s-O~y4N4WhUpHeNU#byp#wR) z?-bl>wzQslf?(Pem$mKHA1bPv$x?x*TozC$JP7`cA8AJ&1$JfO<&GWxz^RN3$x#6v zAd|n8q!cOiQD;D<#OVfenQVP@b+75QAo)sk$Ma`kh~+Vzx<=Y`{;O_V6-sb+Eg4w+5ZVH9 zS)P7+DdZFCQQ>JG*N0eNH`(cS?|2eXXQA~i;T-=A7mFCvpHP(=W%T0+7oyOv&qn6Z zysz5vZ`tXOdOI6Pan8LQVeq{%k&Kr{3=X?(`MKDPgbw4kRYg?OAZ%qMS5J-ET{+vh zv=}1=3XVI3o!k5*nU@|td7zghmsOHAzZ!mv34(_ z@+wyyIhUmPf+qjVZM|P`odCF|dK04mJ3=%WKBe*gG!dW8Uu+Z&_Zd=Z@7Q5uN|-|M zi?z_O3V+LiTBo(3a2AZt2}Bi2yFNzh%*4Gr%1^Qc%=O$Y_^9u%aeO<6Qu|tFijbR) z>zHwA*ov{6^hOtYZ4-^FhY+KZ#ZH|S7K+Q5+bdDjf>yUS%M^YEX2ef1k3-613>NxiJpauA|9pXU0l?0@ zj*0(Y2fjOVTG`I!pNI#Ub8HK&XZgoXV?c4TxRbt#um;q_1PeU;`SDPcvIo~8AEZR^ z0UH&ofIH-rBpsx8W%lr9gWey7Xk6%M3 z&~e!IwD|&Qq7bP^X_jJx3+L4*b6|FmuaaQ=jCTpH$+y`kR`eL#>pb+GJjHj@3`h{l z+>n(Y^bdy}<3TXhrxL5Cmly@qy68IZTO3+8^H8D>Ob^fIqq77d6Y(_R}d~)gvkrRjPL2Kc0=R`TUwBi zhtN@7t^FRLt4qlIH=m%(Y=Qmiu`C}dM-%%EJak*XaV=slnKf#75~m7#(jH4tG4zJ- zhzM;Y${Fdz4u0!}IzRpzV6$z6keY}CyEV?wuea|{Cj{djR4+hkD&3~aPKJe4M@|9L zk;C-clm1<)!#Ut0QQm9%nZS0TM0r}KI<061%LmuGYDEe`V zw!^L5HNty>8MVj5REWX|D}i6quC&;ubM7pXg=B!RgMNrbpr-llUif9Jn1m?Mu2}~} z-dKZZxJ`;TNFsH=-TI!|wvRwD0(6|{pO4V^mroDCCt&v0?Z2`NGF^-Qy72VHPBd+c z5gRWYv8o6+N$s&$+B0veFFB`RR4a{xA)AOG8&gIWYy(nD?Z$UR6C>gz6ZfL|PEW4ns#STv9e$F=ndlhlv0>(Ichf^j5l zTHFI`-zJR(?YMrBJ7UTPRT}P&AM>a56;=9Yu1{N=kFnZPqAImE((Ydn_b)C!%^`9` z@QmL!gZZo_5tDnhZHRPl-Z<{t_u-(~!C307IagQ?q)%Jxflm~g6N^1o*O8+`r! zg6ai8`ChBG|6fg8(c3J~yU|h5l5C$XfyImI)C=Bay2rK9M2Z{O74PP&A<(#n_NAZ6 zo7#HD@IPG*i&R9DTtDisgFsmtk6#35NT~OzvC-DHq44jq>a#DEB$0m!?|IF$E7JsJ zeS%q^=h}M7Sc*Z|8DT6eJqNrA7hbD?FAz}&NY#}Q$gxOk4C%32_&(AzKWeV%Ra?el z`1MP#%3XIo^{f5>E8;%Z($Gy}r)+?f6WL#WK+0qT-|1KX2nMI41ijw)jz^k3f2I3L z=6t0{H^4G6pSD;zu8&}pe))9q_q+Yc+_3q>swNBHb;7yd>jJRf&olNif$vbpG`%)1 z4>E?bC{}jzqWA@=(k2x8X|MS8|Em=<`$g3Upz7v(+X4B+eOo}jb@Y>t;H`K@%+zZ0 zUVe^YY*LW>GhXKa&YdC0T$!720y#JMrDLR4i|IX)tx29oG{t|2ck-4T z-Sp_4uaO3b!P`A1DxOn}3?;Uk<4W%AnTq*z(R<-Z5>{fC2|-h>dd-8gBoCE@9? zA3&1{pr1PPe#p=J7Y{%s>8YT}pc!{B%k=FOrMe|=CbRq^fiD{9;3&vpBiDrO*r40A z9b4HWedSqa!Di7dvJXNt1D>8jBW_<)1iFQcKQ(c;bt+Z1po2eZR5$SaVzSu@YxhwcyKnz z#Vf%k7J#QTp8s+0x^gUcn~$;vUAmZ_w79c<$XpoQ_G|}PCS4vI(d`9NfuwPXw|G0* z;VKXVHPKaDrsCoU@~-TOz3=b2C>hUhNXeo?8Y<<=XoIk~+T!8DZfA-#BJpt|O~KyC$ZR7mv_j|2_e$Cr)uD#0_EE}ttz3$4 z-uXDHXZ^a|xomOLy6b9{qI8MrKRee9IDS4n*b+(|hPbCY=+i=&CI{jiF%(#!YT6_x z{lLY+;;6on%s9@3{$HRKmaz!ccq@}Ob#N06>$$Pr3#NPX`8(Ort zw>^|+8GU~b@+0&J`^5Vf%XF?!dCR_na1?|C;0hGjGDi646`>NJz4wX(a8Q0D$-m*^ z&X><1z-LoNZvoH&a=8bjz%i3sn(!~F2hK~G>C@O*e&zxtYI!X3H#ankA-zy{u2&3O z)+~b6#d0rIJEvZB{=742Y=|DKJFdS|mr0|QD6qd90+neg`!#gS2kQ>OcUs78i*jo^ z)hCzC0!x2O@`lSCyTutUW09g2RkY~B2q6l+Jj zv9q6nu*U{c^6Kt+Udk3M-0!z4s5H88QH`e)3nZc~5&SJHYlYvGdhE z79Y$13i=;>u?zuNo(5^Z19l3QhnoN1_VMABb&=NYR##gjRGJb97HbIA6lX9@qkl`U z+j$4OinAqd1=>rzlxNL6;hQIe@3Fd=yb6GotgT^-Qb2M(NI4)7E%+{x_w9StmI6y+ z`iHLu$uGX_DP0TOHbX`o{$1+u@-?iSQ9Xl(n=w0jGjUWeAyXl>!0%ebzm^q_7%?mQV@4d-48Z+F^UCpSrPW2th4$!rFV{YFP(E6izmFkCddU z11A+GKG)(a5aw=c1h9UKuqq|XqwtJda73%0uM9Pq3!PMh2-SI43F$leT5(TuvYv|k zcRc?~VJBZ6!vGIy9eCmYKBHxO1VZV$gL0mF5+Ns|q1oFa(X7T4a&>GLoWr#S(}m|5 zs=a~RpOGRG_`^5`UbvNweUVCHGvKO6v0-GrzDG&jaC)=gh9MA|@0?URhY*jNab5SL z7hJE6c-XiPtX=Pz%p<#9nvM6NUaK8kOVQULopSS_D#{1Oda!W*tRQNchsK%5?IOGz z3v(fhJFxgnTLd0y*RMPpG_2ogf66sc=mqgIpN-Wkq&-D-#|Q)m5antJXL_4QKs8Ir zZhRgk5fYWU+k*xs9MjZm4L3-|%SjAI`@+lJrHSqWIt+tbZ5yo!*W$~_FpO*>Rq%|N z2r*BCn9kvjjfIqgb4#3ZE%dYPVCJH_nl>3OP=BujP)^JXy zf5=!BmVCjLt;ZN5rd9RafXB-%4af;ryk3w-B3JOv4WUT=o+Y9)x$Zb;_VFD%HtZYD zwdnh)?ifm;0o4ee33XSpg(&d+L88F$zm<}bOv6)rWTMzS#BChOd0ygYkx_7v(s1aFBfA#Uyn& zqgJ(wUHT5C=e79iKQL{^UlNuC;ta`duVM5HW7Z^0!^;IFA^2zBoac#!q532#XE-)L z0v&Yh0$0LB$(aS>2Uhe?(1yabe=kkX^%u%00EH|7XCL_LZP^*cZG$IRj#Pim+Vp$n z;v(K7{cOTT|1ijFSbcM{&&Hjl^-ANcfDPkJnwuXQw&{38kNb+L{gOJNBG1u7$h<|i z4!-Q~mcn#}mNLtcvDb!23S&-z^0h#x?T*kTH6e(;rX5)_;o%e>wgU=Kx$lp}4DjEC za-Bm4c;M(bZRAht-y;m=76ULn?q7T$IVebbU8$Yu5~PWL=chTok>d1Fhx)eLXmqS$ zM300L(|uc(>e_G!N`t4{lR>^in7cHjePSz)85+!HMh^!;P2BB1hz|MtOH?G;MH@&y zS26V%P0thGua~pHhMYO$UyB0Y`!9amfv#SAfsG}A92vjs$e=x~HIMe6r z#t4HzaCAF%4b|sW0+)zc4QU+RgMTw__*$vgT5+o%S-|^8Nx>k zoX9`_>WclXTi%Vlnw7lXb<^E(F4@oXY?@f^WJ1|EQn5Ai2^}`-B}rzUOOxZ9A#8Wv zrQ^%&gM<5|wmC!=gteP)Sr#LYsZ#$}rxW>M*7bwULd0~5Q&CS?Bz}GWRcDB4mp%XUr(4~suqGRk zLzEGk&u%VhaXj3ReWf z(eqA*tq8M7Hr-EB#y9o>uv8n=u8I3(a9~|iN2VCM@`#uJU=SqU?Ok|tUsv+*p3!=} zLJXY>SEQb|*F;Pz>jwc>R4~96h=0jdSi$9VosR_jiR){%`|le9djFD{0LT=&vB?2H zy)A#cAy1P{Gz8;TE~Icdaejmf+aN6Iti&I~=DN6^axwrKe%@358oh9Ao+`T{9)5~YGLu7=Nspv>iwC_t zVLyFdH|Gf##FDyk{jNz~L$vIYho#d)qr@h-HCHtwnd_}b|5?`ml5GzV)Bj&{bP{0F zL%<2RE6xUZU9x>OTHcGTY=?A|%?jN4>H_;+ib+ltw2=Q|5=80`2A>CXhwCj47=I|_ zF+C*qvWk1cF`J^(dxj}Td44BDyHji6;YD_d>!Oz{gtuY zwDOG-f{<26P;oMlA7_urb39ybB}**vM>CnZi1EnqZZ!cfyOIidykZzlCgILsoOsPZ z=4U#bmbm=5zESqI!VVdN-Tr|@l~M`gwoA%$umq%qt2;O|qdMNGGuWJByphf~&@Mz& zyRUe8mK%4*=3=_oEWgUaj-xF3U-UW@p$95fUee#xKg>2CjF&aq@O>JZMjuYT> zvi}Wsz`i`E0G`f!o`8$Lb8nkhEMqOhD>vj7*+YX%D?3JoC zv&y>@o+8J~<^LpVy+_8o1OBlyhQdv7s7?cltc6hsUK70IyHvo?A?y1HenmZ$YBna* z`C|ZY5C(dN2F6P%YmqZ%W5b!6w)pdNdyu&vVuxw>=L9Q?((RU8?yyB<}VX%!uUp4?~W)Qo|r}7BDTR^vv`@QOVE2lcl)2}-hUo7 z^sN|o@~T1%m3EYWyV~nRhl@AmviD}p1aE@ z;w`t7`v@m6TQd!qHDI~9yw2F zUxDInEgurhvwEA>r@Qq1u>_AQv+)#lkRmL8+Bd(v$={b%pW* zTv>RdhQYr^u;e*k4QCUJF^B+dP6UEueVz!OLp0zC&uBYoyQGN~LdeQ~%=~sJA$LV9 zT4Ie<^@=EIsFLB^rh9O@%S1xRa%wAR-qeG<2snTri{7+zHsAs_@&=r_z8BUF8Aj9a zZxRXj1vU);BdJ?P0p5I*IXPe9PA8Xtit+=y*yDnr%N<#rW?)bGCZP2zHeTPF%)`a@ z*6b7Gdy6-Sj^!@aMrmOnCHM=60se24u8a)A!s@@l z#*>Ofh=McB)fQK}V%B7ytg~B3o7g z2ZPIw?Pbh7$9^ohmZJ+t)UU--+2eI=re#v&p;To2OPUyX0pkf z@^R#8IGzlzBdtS=OQ2f%8|8;?`Mdpeq`D`K-NY&l!F!3V$mPDw&t9o8u@rhT5Q5J{ z-tto86QgGDbV38;i6@+WlzqtgR48$Ct3C72u46oAgMp~KZ6r##G28SdB@Uii7HVnr zJbwU;(nRqPKT@Qk`f;vY5zTs!Kh7yss7Z3M!o7iZ%n#q=9DXH?6nqZePHv}w z;SmnrZ5H}2ZG}H zD5@Z{9nT3bzMPNC5(Az$#{`G*Vl7-L=#L|U`Oablk5hGa^Ql`q!!XQfAb%Q7s(05U z;#xRd`YHL}i;)u7e*eu&9c=}-fJ42|7nPEb2SeaOGz~OEv-lR*!oG$czJ{TEi$J$8 z5fq!vCcBiKI*bi@(%KAZ<@sz%MWV7L2qR^}bJ@oTxE9!`*}$?2&L&s2I4Ux(U_|@} z;t}S=eb_28n>Q#E;lsu$$YK{==TEt!3jtFiPbMfIJp<5du!71=PK26Q1u^~0OAh?$ z@?Q1fm?hq!i6pNlGlhZ;q7jD{&i^{OVSZW60W9D)c4+{0Y?kVf@{sC)G*ki<+XPxA z#Yr=_|E7y(Se1MEJZ79}AT+t#dBM*wze-m;#xnz{Q=4YrVN@tLw}L<(J#;zqteR&0 z9-I724>bdt8uyy%^EYoq-$p1|Qalc^Z0WciyN3Z{lWXfbEo18h0mxrQNOpo9^y573B{MXU0hu}+Q;lE5mLoNjJ z3HP@Aqf8fCOXl==RHTo*+SPVNWEt`j%Pv6{t2^NaPFFm4b3j+uek-MOTz zt4mTZZVw7;glS~4CR#=X4|^{U0wM=VE#8iL^nwX_d9`86tLZ^iu&_9+D7qr$*s=ADeqs%0qdO}hS=D6j>uNn61Dn65m9zn}DrJ3=A z98bUY_G-TiLdL6^aQ_GR-Iq5jqK4qpN<)F^3pJ&QB!gEiT^%=Uwa&&CI#@>t67gs7AC?KNdB#P(o4~<_A>H76eXZSxmRHK?7ze@+~f%pN0WL zg3CUwyqk7tgIB7^6o7MK_G2!C*O%{3)|Swr7w9?gP=p)@r_|I*2Y0C|FmAfDRAN=l z*JX{DK#030ts8)f6=n6z0TY_F#2nuN_v@IS^;dQ)yYhj}3ks_Pa%|A~pkXnCH|+dS z{7?ni)qs{mj(?F-=n#OWk{_L%VhQ(_VhDJ!{Cl5-D88JQ08Wv{5Ip~FbTj8|=IxNk z)#!I!2;49XU~jRcA}xZdUX=a8p6sje8mz_cXJ8eWxnJxX7-@=cln=LtF^qJ|~+d7`Xqtc(+wNIL{c! z+n;TQLibmoVd}NYPBTx;GX)P%GlM8n}FlJ^7B#&Zk_6yy<=#iV)b39q=1Djc}+|P zRY0#!5U%&Ft1=#$YYoSu#()=+?O8Vyry4Dc)xx%vitZXj-2q zy8Dkp_yvTl$JqnHD#eH&Oy>z_jK z8#i=$y>N?g$HmPc9JQP-0nL2jfCQ>V*~a|~wedsYCZUUwlLWu)ooC?} zotpqtdRbZ=6vP_)kW1{K(y&;eOv3;26a5#|3IOVo8!X}fzB?wnAqbNyZ;i@<(;}KV zY35#Gm%&gtbFYpWa z6P%D_=8!sAmRl8ppgGCNH~bY8X5yq;+ccQSVWS9W?>K*hjs?4YLgvVgEq!7jpNwU^ z4tTaJ`P(!oEvQ=5sxAaKDxLAkw$HMt4oyRgrB46UPOAW? zB)@bckWb;aWm*+#dcXDpQxi*)(5yl|QdV#)UoKd2!pRJYU5olt-D-LDDxHZ#V4v)s z=b~=;a%>jPS7^0&BTuxWG#=`XuHNP?Fdnc8E!<&)WL7OHnGG|>Xfa`NuWVA*p5hZ5 zWK?kZ9{I%1QGb{~{HjT6VdIoU)3h1lYWin8iI3|sM8Rr9L!c9umyM;^tx_?iMo#x* z=viw@iq$GT%IsVLYe-?)lsmc87GYuD2krAtCi|_GY0;e#3wi(2wW~Fs)|pgdhC|9z z=${V39?ax!M2L8w!wuE(vd0s#yZQV`$yDtMWy!`iL&xqU3#Ro=tP;lY!l!o7VD)PD zqfC^((gz=uL#woQ*M~2=d3l|RboD>F4SuKT{9ISojL`EgfC;lgSU>(qfNI%gxJ6qy zO5DmN6=1mW?4eyFw9LQyH#KAb!d(O4{_@+u0{N7A+iWC^d{+;)5t2Pm@Um%3`}UfGZ>@5n~I@Rf2gup@#E#_=dH?9@FX``Ra+ zkTkC!>X^OlR!2CT<$Y8X&9qY5tJWk$MvI@LHCf2!$D}O`NGWe)OS=r@Fh4}jgl$xg zM#`%&&)FlAFDalRF&p2rFe*7Q8YS}%>VG$x>`HZ zBV%BsUudTsZ!S(0ib1aX0qqJ2>hv>p35cGJd}ASKoff7xd6Eu1!cEy=^U|sTTL-W5PTo%&o?D+jVsx+qvtxB{doXBQ`PYW8nV(BkCU5^J=<=0mrs&Ta9hoZEQDYqsF#v+qRv? zw(T_duIG4rfB6aPTzk)+Su=Z$jF>*&EU4vJ!?CGkNoqe-LjB7$x3aEOpz}fWqVj(O zAn%vSCcvax>G~1)0q~({6X>X;1-f3%)iMkwlV8~JRI?QpiCS zopiqD!nKBc8G1UbPD1&V{&fh7iljf{3kNjDAKbte&GAP=yMU&-Hnw67n7iIQCaRh_ zg%Pa^^Vs%1(PC;)5O1ms9l1>AO&79<&2HNZMQCDB$`Ax~x>Xf+H`+_0|AHU8{BvLN z@0{jHT63!f_WZ?7FZ$u8pLT!L2-$+PO(Tv6)O`0AxV)#*uuqoXBMVG4h z;1|di0OY$+8sNMN-P=|(6eu*_MwS0@=_RidUk^9IrYZ~6^!CikK&i^1S20ZX4Mp|Z z$}*SG=xjV`ABTNLYwmX+AX!b~PCuqt>1)JNN7=Kwd$;eg>EzDiQgOnT6z$^c{)~TA zZB!&|7C+{IhV#9|EK#sa@UlpDb2){Q2^SfbDDGFhmjqDRWrrP1phB##5pNDdTSlSM zwhrmCrH_lOP}BR;iQKQ4l8fJy_?yzDLyp>5$q&LQD`yRYzEvnZcd@a(6Hg|9rAS{~ zc!KF8?FKP190RSucDQ6W(xW;bQznQ0?u?Bj3)OB`wCDs4@3NKt?6>$P67NN@XLPiW zm|BxW)Sx6o`YY`EY;1$o=%`U-ru+=JKo?VnR`dBR!gWHU?faXJ8-dsC5t$C~Ag!Z_X6hRk!?D>oQJQCw;V=q*5p%skV6__HP2sm52J zLldy=AZP<^S5@!d?qkt}eowXS;2qnyrCH63gZiJrGGI*}-kYGoR5rQDq5*SW^t}uN z&2y&McQ5+Qo2zG-b&sX=_u?8xSLJ*9_^{_|XCgm3Xj?v_P}??MI~u3ua$L&7v>pwj z74J%U2BWo}A7RAnu|l2;4*mvv7Aso*#a9Ib6LWfe(nXY z#f1{&Nh+nJ{K+XY&q_k@;))|JAVcbUGdkRUp0vh8D3)ttvm;GQctBT4vy&6*2@{>T z>rVaLo9tNfp@%fgz6Nc;oQvV~K(W<|TMb=XKdxBCJmrsiRy^bRf{%Ib8-1j`U@{76 zyQYva^+(=9{Kz8yF^L6)v-ByO_P?=3=F4CgVBqw|8xJTWEqLqY6-od25Zh~X3<8Vu zH+*1T%hzf9oPOwM%gi`}>Uez^Y+?m2j?D!Zi96Bcv7zKLgK3579w@1<+~`0rFhG;+ ztPDVczQ-+U;{+8he0^mD_tISOCTc#TkVmi`xGB3 zsg!l_q&t_Y>%;(Pc{2vfuWLMg`~z-_>!KgQ#_DZnN;XE#z=St~%S*}O+6q<&OD;_= zuSIaola^i7lJSnmQuY6hDhgi|djN`p3jIky=>)hgBD;}y1NjIZx1d~EgsF9etvnjh zn$Iyve#`{M>|%m61Yae%t2=8Aqv3HoWz+tk0UCV%AMPm4%AYTqKh}Q7fc}a&Oh;IEw^S!F7M}|HnL2G!DPu2#GonRdBfHn5Lk6t z8iqwQungIw_v}c;;XGa;T1zcxaO~P0taf_fh$R*$2P~l(+*XmauO(1nL{MW(Zp@_WtQ+Ag9yhv)Iwv0drV@J9m_kDtA4412by!BT9hQ z=t`ZB!1<(lK{u4#Fk4I;cBZ~l{5oFOfmv!xpnqNnAwN9~hh(+9cHY&WIS`$Q-Tf`e zcdO?!CKvv530u_lPXyt?O%Dt|=0L7Y{c`w8HRKhz)ySB_9=YZN%$qa@=)VP>#uw88 zfXSg}rV`|H_HAnjo1*9O7v^(^!bEnTLM0<78p~h4NYH?+Rtc zD)W5x?R9ViT`Y$;=X7HVpX>6GwW!5;?vxHayDeTKhjk(HYOc(!z0_^fKGKB8>=gu) zlCOLh>o0IW^C+x)>AcWzejU0UNA0z0Kg5X-rfCP5W$%`W4yHRT-`;?phq^fx&L7GE zvslv1@Tqt`HSE%Gru{L1?{*#4^PfJ>ZBLFxr$PuO5(WxJo2Un-K7$b8U`L0s+mqgK zav`r0^sV?_;VwGF`+(jd*j`t{&Y5XLhFf2oQOXelxjwgu-)@uDdk?8|MW|;Ka#M!> zy$aYXQ4A+jqOr-|s}k%PvK5RTZDJ{BT2G|)BT*$O2onfYjT#r86vrJxI*RE0W1=M$ z63j+@YJfJ>L}7(z1-Ix>m3}HiS0^Zkle?JHT{NPr^cv`RfqWC45mp`z>Ki#aUb~URARP8s2TjMNXP7@ zAPiUR=+cIyfh*TB8RcUU_wd;5O1^cS9w{L(zs&!y=e)1(SI|d*pgW-&n}OHfn30pH zZ#2K%+;bT8lOIxSc7E`(6FhziL@+Z5}#2O4% zIDFu$$9hI*PZIv2RpP|)LwTQGXon@Q|S9SC@p(@YD^@{mnOTm1*aN*rT zYBYr*uL9Clo4XcRg&MmS-#o9X-7l{V$~Do|&}z=!#Fia+Gt`yR@{JL8FHsTM?ycte=L2UfrQGw0vO)1=+QB^2TT*;IJ_`Q((=*7_~Ml)5gmMclX5vR|IdP4 zNGH~0EW)D~uyXPNDP#Q}=t4umPFRC0OmM0Y#%StKLT8(DJn{yMuH&}gKB0eOo9P$9 z34j0wzpoki>`ivj!N7LU-4{RYQNq=}U}F$__m!EWnGI}%o$;q8JRW!cdf-#C>|nPT zun0^N6sffFnqNkx&G4{l@5->~AOvmzYA##uy4Sxv>xjk}wF|T)7P{c&7z0CZeyC0uoHg`9)t%06z zOUv~nNJIe)-c>&3AcfznCe0MLZb`_hSL3ym6mg0|xP2Uf|Go7VUks-JhFRd?20(+C z6&FKqtNa(XD48;qeg3;%Livu3?s{_{o=#eMI@r=;;UlaY?MtgA;oX9ekd;!X&uqhy z58oR!+{rSQ7iTb%HO438o#Lm6XMHl@Zb1e+Jyo~r>xh$1Hh({fGAjfBQXI*+`d%%j zTE%I$lTMl?!4C7vCP-6`wZ_!(AU)fcw%(y75lS)#V3y%Js`BxgTTlXOn6u|Fy+6eyh* z7i^=VM6Kp{UrXiabI{Gl{pd;3SfB&w%&~gn{@Zi2`I0yTNR$#CodYIIYd+~OZo9g? zPuo%g3}LF&4k@OnxZ$|1u=m`F<9V|@mYK0VJm4b6r!0I}%nKX9XP80zgeW9{TIwR048?L`ssh? zf>?ZoQ@%Hua7{){s^XoMG;2`JqC`|w==~ajIT35il2DP$QgXgsiT@B`GCqcQwy1TL zm*b5?k+EiQc`+{_vHm9ONX!!RvqN&2M_D#g$?KDH8VNQr-SD*=_vI%WYcK*MwY%o!5B&E(Nl6Vz-}E9mN%kq~R3 zp%V&f&Qr$-1H!Q%J^XRhVB^t1 z!WNg}8P9+lj+1d^BKZ#6@b%ZFQD1FW=G*x3k>gN^V%m3!vaI`U;-b54`*c0pwh;4e ziaCXr7Y3uALyHE)t1j2&il96k`?Y|+F?tC|qGohbx7q#{bR@oU7~S7;l3p`xLR&)7 zdhgSQpU8KUFwhma(Yc5kx}TrXfe)UQWC$JE2|hA7aX^Hie&R^x5kqR`r9WtncL_s! z9R5UQ5LCs~zpjity+$}vbI%sZvg6&{Oixj`0F!^!Ies3m5BEMwwJlj3NPhZQ%|=d@ zz4ze>`?nPDar+{=1Q0RW%Blaa$NvhG`in9*hz=N~n3TNqu^vZYdAwi%=pUiu>bxWu zl(IW}M?L`!SILsrBw@!egsrjQ?PRcgM{7Rr30t|wh_}a;XRBrb%gHAx`ZA-`oNDo8=uHA5MibP(ftGSa?0?Ag+`CttdZ}iCwDd!gfMOK zUY;YCT@-o!lZuGeqfn!>~C@`fZlw2y(jyPOOeaB(7ivulXEmQ-mEA8 zj6WYszzXiJ0L9_e)AFLN5Z;Y3ZtWyH;o|tyGRYkM)GeP!$D))+@}OyN^*OqH9=7E# zp8wNHt@0oxVIkj+Jh=vIx}I8K1NXn}H?J?DD}YcdTww^vCla8%t!SDyNSgHu(|Mt? z*FNbC)Jl^0Mn6xLf_SQxaT%N~+0ZC-S7%vd9c9koTcp0tzRb`}*y$1LJrU7?wH3es zPiG|#y#GjX8Gow(lq7=hIpHZ0HSr^1E0+vh(4v-$3>)vX&4+7H8d!K1smUgfeKpa= z>)`jb-pu=-Bg2-i8X~$Ww2$I;V5YaRIpece9ok*7^xbO;LEAaiYh8>52w$svLleoW zF!QWbOuF@)?J2GOpehi+)sQXQ0*-Ot4}^|@?k&vCEB+wiwDD08E_RS{3;4Zm zRxH)O8(3j*njSkQHA9%eKt~$P2&i1MR$-A{sZHUwMYeYI2W;<@H5%_O*qU~p|6Vvj z-!H3cfECvp^AgCXIG|+c=T`??enHUzHlg<|9Ewtx<418neTd3lcX%OeY%H(j^)2e@ zoUmcKP%1mSVoJn5LAEFr49D*i<4^U1lg%>-An!)~s(C)(D_#lKF0^P-Y7770L^P`6 z=%ia8ui{eH$L|T!-r(qTN@bG8XmWr{SvZxQ(amm%FE)NZH!8l`fF6#Q3OQX(fWsT3 zo*K-4W+I}uFM5@4^nN$@q7V}H?UoV8sN)y_4Q7{UR5nZ$$2Qw&!Jabd<#@QaN2N7> zY!I>#y<$!;C3^~h5Z5-C0t<&kMP_kSda>E-{N|_0L{!gh;M`20@nN)Lw^U%=9+clJ z?W2|bHv0E%Q;3uif`0VuZdxO={Db>oHO9O8V#XUW4EzG;FGnuJV_MQ%3ynyd@@;+l zjg!z!uJ+Huy`i~_lDWup)9Pi*qThH9w*QsRfC9dRZveu)@E+elKGy(?J-0lpoy$Fc z*mP#4-QCkaRkiR9iM1wO{B#WqK+M~zjt4>Dm;9O)_meuY?~2!=tCgk4XFwiTAL}y# zjxNXcT|6uv9F3=&1lpDYbKsxV$%|}QK&}@D3)!JeGE^!!NV9M|W&8{2Z2qt`C!OduL=He`=yQ|`abzKc#hCk+2Aa|6I`aQS7gk$+9;&$4^;k@z zh$|+1NFP_jt;pm#G2J66O(hGfyh9aqy9|gD#$YA#9WH5K z(BmzDRn>-)p*n2NKsZP&khM@vJxBK1ecGTUTu;#Je4d#*^Xkw)*r6z=I49$#{79gv ztk3Ww0A`({t%x*uzr!CZk#iT5LEkqpZWp~_;KrU=g`l?m!QFO(?9>TLD+y%m;NJu= z_$%$VfV3a*aslr60ehoLaw+&75W}vHiS>SAgg5_}VTNm@*%Q1(O01USdcxrGU^QfDin1L)@i2zS#DjUEcgoutEY4U5A}_ZMwGLVrS@$hze^K2gpJOh zl47)w4cQhb#YpLEb^qt%8~){U2k@zt-LeJVev|EB!nx>Csp1;ay*;ENWBqXjPL{Kx z0xIZKd?cpr2us?{YSk&T3@`DZTC2WdgQ>}9nl(M+9(c~2`l~D+9&q<{I7uZiK8aYS zu90UQpFPKGKX$k4`^j#x^JnMKuhR-!%yuE2+%`v{;v*B%*(Dj=%X|u}A44v9V`h?i zH{U|NE~vDweypI_Y}eney}L`i#hP28jWGsp94k-)8f!X(V^=jIPCY3hWlRILR|PBP zVg1B-@0;-R?&t%rIA)J`#BADT=@b|SX4X+SooXlGT^>VQN&R2!g3({G z-veTY@W2WC-!Zf{6v>Hi)($U33w;HD+fbCsPO7%ZWAwsl^;56cj`lnpiBh!%bXvKX z+!u<*X#bqb_7vvQlf9j0Ir;aWhl~GM?W0QE1}At>zjlZFXnP_|fSC2Ffm~|Ayz0@2=64Oj53nITGJLL{hRxU+_2 zAZ8`!vP$aO&Pp*E)Hij$P4V9|Q- zKnr{eXn;78ztom1Xe#8PephSQw+9hw&A073bV#%UfpM<@D9kN;-M`ab$fUCs3y3jK zy6W@dwuEDZyKC~`@{Rq~y!`Zaz;8$otz7NRSCo1TR*Dxu@#$UkNcBS#= z(_9B-`Lt9aRn`RkRC57Obxjq>G9aGN`q_v3ZV)iO11yg+8|eQ=jifJ)M}S8D$DtbV z+uPbuY4EkxQ-LTNM2{Y`!=8v{U0>x$sWNb1cKD=B%)3gIZC{kC1okC^jkot~mI>GU z3AJ&b(%=oQfPhDnCY!kEwYFTia|LK8uQ!!(A=|UCoQuu&y6;}Ot}_*WvX}Dqk*|f_ zyd6%+Tsx4i1pXhi{S>k~HZ^y3|2UojYQIgQELiao0Ydovp0%|xl})r@@pcqLfyv-v zD<_-PE;+al*SD=xo;dG4pDxh?<6v)4sfwyAwpa5s9tVzyGFHw$Y^m<(&2~HF&h7E* z);y1u`vlyD=|Os)Bxvo_1Ppm!9Y!N&9hGp77PRqxtIq=80sv;57<}-LUs!F2#$~_#I zpau4#WoL#3#RHuP8V%6sLk1fw@sE3@6ylokjZ$?;5;4}I&ZpVt)2h7KF!AA-I-O74%UG8pw{R9vIr*2ujjr(Tx%IhaPSUbWU7_Y06=wKkg>Y`}b#}hYB2r;vaa*vam z_qLhKmBeIuv{5hPRd|bP4_{)MyBN+cNY-=L(wu3ATUvcNkfdbcN5p__ST#?|Im-Cb zW@jW$jVa1x8P`;!{BTc#Z01d;w`z=HliS4n=4YPK2lsPbcn2x&nl-`GSf%*^7)drl zy#3WMpi=YGHS>E}13T|#F>l<7l0+BjD{z)UrQ;+E_F4-H&wm3*-WS9R0K)q2d+Pr_ zv~uL{%?d$@ERQME>P^2IH5Sx&am*0|&Y?0$orXF`ylq8CFpM8XrI6bCdw4ZvESyA$ z%DnOHMUX;mU5*M*cds3GyPsm&v0!g0o+Y6qmwvq%2!;KqyCR0=D{4koK|AZdZXP}n z^j#N`P)*hD@$pfKL@sKhtJb3l65n5bWv7{G8%nKE=+<%!1BIzo;WBB51_c*y92T}i zfp0r78xyPTheZv_4VIG#vLK`x$1lwVo~gODBto$6Thu^Pe(^kLbLG}zF9UiC3gI6; zdgRCutd*|Dc`pWEqHxq$pJ5*drG{>~L{!d#eQMp^&TLB3>PBne2V-|fjCP>#t4R4* zitQ=<@_7aLY}P{&{O?edZ0k}^K&zuJm@Y$qCr77%F!u|sN+1n{gq`EH}WI?ObSB`O?-AlcY?G+o1k zce=G#bz9IIwazzRr&w4fUd5^`&m?klJEFrt z+>PK6Xg6km=eWA%PbPC#tzuQLxk+VQy58~UP!#=?`P(YriSHlaYBo|FWo8wd1x*&Y zB3GlT&wAdE6uGw_Cba(NLWHbt$Ft|(grxM#=`X-(jY|s9rU>Q@1fI8>HMFhe?`o^A zFYjBcNZA$OPJ%Q1T~GmD2uULIjpYI*E>}_DRvSVL4TYrFqbs&d;x@Z6J_EJ6;wzpxp4r zlU6A5C-X=k_Sr3+n$pn?9`IbnH^oL2QBvI1fQ#8^6b7mbNL`BMq_s3>zUO)W2>-o& zn-Rrb8aSgtXZe6yRi&IQQu|SBP#k?*CEP@Yu9XqmIFkO9XD)vJanv-BPIx}@mI>uy z!jg^)NoO~=TOHu$iA(vT7z~FW7%xL%m}rO5edcP4a-$C~DIuK=;`@4+a?(LU>&>s_6W_N;!KqI-Bln?ZE(1jRsV3!|{DJVi)9RngXUA~q2bUTf~ z?=HDJf`!m2;OX@8z(`eOeNKCnqndJNfrtJ2+m%?rS(c>V6Q-8D_awr)->w){$NL*P zSucOre$b6QO)gQE>USRE6ebxgFf-B4`tx>!V3&wl?1RF5e0T$^b{7bpPVYGC zfz+r9Zlk=Aw{-VU#Fs)@JzFaY=%Sr~94QOn6eq^M@Dop9uM4OF)_vks=DkhCgWH_%l{I1q5B-4y>?-xq0SyGX?=ukYwc+7ohDspb? zOXV>lbdfW%ofm2NCQ;|gd9{$Dc7;4sO_9b^*x3x{tx@n7Mkgcz!69_8B3Avm!X8-Y zbna5)2y`0{qlxzT))WpVB#hn#{LqRhGvw@*_(fJ$fJaz@E-xa)c$XHm&P@87Bb!dH zRa;6Hk5Yn;SAP9K7L^Z?EDb%?+(exz77gzdaJArb>UV5D3&d9t9~`>8mX(jit2{Q* zrqu=73-;Ro%QjH$SGL~)+0FzXY6AIWd}B}=DK@)wUbqoPwA9o=_z~E3*+rn(HJxgRrn4v4#$6(OKAjHpYR}okK%``jDPDKDs*oRdT(6azl z8l5bY_ZNVE99bd0e`2ai`q?F?Hq}Zv&(|wes+5Qx>3;U9bjSgYuO?{=l<|RnNu&}U z{WLO-anMlRD2Yn&myeIElxNOQ)qhi*EA}0IR4lV1Hix-lQYtfES4Jlh9_K} z8~6q!V#*W^er8;&ss`v`ZE|(W`QwDYM8h6_@|4D6De{4RTp$0^$kNi-ZjX2|C8d6#;0Z&rlOv=9xdO;Oy*_7&C^Z#_rVD3v})hDULfBczWRCgO8wM zm3p@;W_U*|4d5)w0EwjLjd(I9M_=by zPw{ZIyl!jUMP$rz#V775?~DKt33RGq!a2d|Sp%Sy*kh?dD9L(0SUvLRH2l^XxSp!y zgb~81@Jf(Iuc{^+a}4rPE?sF$OqWiJZ-$S!i|R*c8*TYG{Kmm!kQCJL6W8kn0UI#T zt2-=fba`2KP`VtFxu=Hg9$=#hI0PEvhv7C)cah57LygY9-OMjCdFi-#IBN{(3Sc%g z7&|(yku9Bc_V8Tm;Wx49Jwzjd?;R)dH{c%HJ~hwOiEdr32?~lO*+ZJHIf%ci&LM`5<8zESKZ%$YVGOcXe)`%Q%y9IN)uS#&q{G)yQC_yC& z;+&Iy7Axu=Kdx-`+A{^#&jm92_HR*CM7F$Z+jfwLQ&7)DEmE~F7jG3eR*N z_iHw}@^^nAUEmogz|nc!L$#gmDotYQn@L*u-=$0N?dl02UO@Nt|79%IZ-;e3LLGm- zny>MtOr1^eAM0_t+`o9bMx-R_FRJ1U^ly=AUDIx#trHzBzg6fW>YNg6*9z4yQvo6= z%Buw~bG5EIn6R}GpyY~#)hVu~RY+Tnxy&B%4125jJTabOIS+)S-htecpzo){y1hE1 zMWTWmbcFt?ZHncZt2(8+9R_nk10USo9KT= z2{OmRcOq7K%O-IQ`cj0Lns?4>`2;|dx&~i<8NLseI>$)2%5OB(aa9W&{vcGz`&pe= z6%o~sXGY;kNnR)q^`!K8T;tI2ZE1oE7A(1stJaL#AUp-_&(w;Z#Fmq~oB$pd=#9W@ znCJN`*__9WR{VR<57i6n4Xw{d^RrFPlwQ5}-8&oyu3w(~$GW^jQYESH{~jpc;V&vs z02MZd%oE5b_}f5`M0UggWfIE<-X(*0w=9M$ zD=Yc)B*F?S*#Dj-!SOF3FaQu2-vU3#C-mD8B@#Hh%HRH=A9}LY=ufRPgiOT*9=;wq zlP?W27Dt5+{3KD3RX~Xf%%rs%<_#7I24L~Lx;ummSN+o&xR}+rmz)#85@XYfO73?s zqofvl1UHrOlxud=1A5%DW^MuJ44TVr#1wK+yOTasb)3M40r(?)=D+Gwm9Y|qd0H|a z&E%(;b_-Ytixs8e|4^wKfl+jF9r`b6Oq?)EQ;SzX+x0H|T_!8J@IGmWL(9Y}xd%rd zDFv$29E_P+2hIUnfruipvxYk>ZKN)!+FE%)Fd z!y=B3ItWhbmL8?p>tzHPoyyq8ooPwBfrHV7bnqV%(DWA)H~^{XVaWM^`<_F_TL?Os zAINZ~%+N8QR>I%SutW0~lMXrNqVz675Dcdb$F3SFrrxs}fyPH38=svwBy5YJYg!$(L z^$VZF=_6L z2m(N~H~w=G4R3!yBvG9P1_yh-#)X{Q#J*ZvbhZ+(Xi9L!@5l&F{XYf z05L5Bi$gffpN=_Y;9Rhv@BBb9>~jAcUL0z~v{URSJTUTD&pQ2)$G5z-C6-5su^oU( z`Tj23MC&SI;Ni{|HC^vf_6prua*;PVe>^<40CjvE&3JsiXR(#z6?;P7TyIE!6g=TKtF(>O%eYz=%aPR(deW~i~14YR+A{qo25KwuLS zoUi*sQ<#vOJBx47=~p?@AF3osCYv%tV5oTsW-3SMg9z9c!|jJd@l^S~U6VttKkUQ4 z_nJ$2Zt+yz+Wuk`CM$)n2ij7iKoNPxM`5~?P0AHhHILl4*{Wb^ zq#|z(^zb>GFD}pJBqzYTkQ74WVXxmAwpop6K>vP zVhd@t0=|)^_{{~LI&~o>AIFKUwEmFzE!W0vpjlr-{I|iZVA1-v6()~Y zUCWr)pS{L`B+Q$f4aX-?(9$hsX(T@cR{A=ZEBN4u2+c8y$9T3$7`617`ry}9Qv;#3 z&wo?C&99_E1Ckaug4hJuX$r|#n-4uFT$9gVQt|589=-56xioWGm1mO#G0%kKK5OCtqu3t#6cFs+&HH|8Xm<{DEy zS;Otg*H@std`P2$9WH2bPpB?)XWckEb;OIq0@p4W>W4fL&+NgAoqlZDt!Xm}jGcl4 zgP3S5w^(?+zs4?f=*frs>p_k~8D`$p+~05SmCez>{_EGe^A$S`Aoe_yt5o1WZ{_rG zwLM^2^D$>Nr(wS5&*~I+>fl0c2-N-%;vB_7q7S6l_P4>x_urjwYZ9;PyGJ)_%nN&! zE2Myr>eWTpBnHnrlL*NU~}rNzKV9}Wb@5G zKCbq%DmJijshc|7NhPG!c^UV{$dRu$5h}H3*0Ndo1ocn}gV+4#cW=UNc)bBU+5td- zs+gGy;K1dz`fAM%IZz zK~p%45Pgd%3q?QxyV)hHzz>;}JhWqi%`hnpy^B_qR)T1m_zk1~SW4;}2bt4g8j8(8AUK*yi? zMj~Kn;*R4duF`x-;d26yhYNJpX0}B6UQa2fJXv95QE8OfloFe`z&G@du3<1;m8TG7 z`4Oubd@na{MHDPxc4?*#lHr({D({vA1Ndzs=CL5Y?%9{A~SnWj<{E~op*2WhVC4sIu@>~)d6 zVr;7KB!T@Ct4y~fKWCa#vcAQeC!d@@t;w`4n3rnE_4u z58oQJuJfBc&?fgqStPM6q0PwKu*sCaTOYHE*TATqrfi3W8@%)0+Z+(zJY{?*Qjpi1 zD(f>RJ&-8eZ+*IxIne0D&;`~3igFX_JuETM83Yfz!Nn@%cVk$DPJ1U4Bc>>G!{o=Y z!9S`ovHXJ;8qU*vv}O>xH%`|TH{)BR;QlBlkPrVCJ{8SaI=IgXng$`N!8?`ipp&h{9E+MZ<$`xV*bpP7s;! zr!XGXD0(cg*O`%83_8`%4xq=%y`2iVj7G+1iQjZw@GOqlFwMs`?~FReMb@(JDCjIf zy5I6M&bv-Z;lJm7OWCCP%#B!?SR3OjXr10JHT@vgo|+?^-L8_YibzYP0Uzq&P|QfT zWe^AB@m&^4k=&2^(F(-D0;}=g4Z#;*PXIjN32;$>U<3JVf6IpE2@sNVI>tO)i|i%- ztLw?A3uvq-t+FyY2{bszP@Gv8Yi6`_I$QQro4M?X8eA0r&Fd5 z_=A)#cyu5Z8e=0Qcd+)~cnuos4ny~RXD_EqNqx$lW!pwdvDoOH@Xm-7_tlY^R!0P9 zO{jnpwhzribR!NfH~OBy>4s1tCwrqVV(6f|`uaOOH(<TbzbU|Ch!=nYY`DqW6Ft%emeK31qmS{iQO;g6GTb65> z%pAIx8#VfzTb5mUx-N;QG9>+?`o)N6QktHmVWtL6gitk)wTG8Uhv=z2&XMzxpKfT8 zxs#M7tPyu&G}7WwR3H>RZ_67PT@+V)rT8f*s9qsfI$;^F0((4E{yHpmpAh+kPp(lf zaka2uCFEXkKx_&UNC@uA4AbzJf}dG6w}*UMT=79sjzD)5D*T$y-K9C@Pz}I&gCa9~ zq(>nsOcUTezL<>rzZbjb`YWvnfV85{$pE^S+P*dDBnHdZ!Gh8KNHl@3u+`B;;m&1& zgoeDIAD>1xLY`eOtB9Z+GN8J-^}AtlvG@TLf-@h$abh}%JC}WL&>az}uXl(hw`yxf zWDwe=vy9SDjw>vgDCgxG`PaTATPXJyR6T8|&x!VXt54TB=0vUGHiX^2aa1`f-o?WP zHKlwNJKao@^s7pFz_nk`#>rxbQTj4#Z6{O7J8~gUFv2qb+0}^0>E|}0&we+IRAN|I zs*8?`BA1K+dwRq$dMi_f&}pe7MUd5}J7l3Bi@5IQS{9W+dpm`lVCmaY=kEjNL%a^K zXL>F?rWNC)P<=fIvAqKz12lP#<~9wIQ*>&d0<6#C!l)LYS7{)lGySK#WGUf>#1yi} z%2$kpR^F0)m5e-ZGFdF2iDzkg&wkD}#5xKD91{=W6}OQ2)*d|y{kT^f9$cO%Gt(>7 zO27UI5Y=O-^h1vI$%a`C?%a$BmmLKLpxy;t+=#t@pV<4aOdtX>aSwZD|G$5{YRg}O z0Xw^scf&WDyeyD&S!c2)Vw)dmrP~+3)I|odpBJ#(-SK4Y!-3QY79Y!A^(breErDO~ z@*N|8L#hxkX$lc9EvB)_ai(Ps1DVEE7&t7V0*>U{xh^(nvDU#!h*bu{iU|_=QvPjk zE%T;k&F2iX5IFO#Vy7CAwxKmchN^81gJRaT&4E?kcUDOoDNGP|tnWlk+tyYn9v@zc zs}LD5Ws(*U{8nR^atbb;-Mf-#c08LJZi@!*J10cif3dg(pK`woC1=TK>?^VQIrc)d zG_s*{`ML{7rqE(+6cv}xM*V6h^YGZ_0Gl=aY=q(sk(nKSz(-Kux z=(a%+z{;S%k%1QXGrz7u$vy7D-!TI>zSnheCI} z(C}>t^gK7;3Cl0{zXsl)UnD315@STQK)_KBp`6m9`(^ThirD&E%IAJzg2y*r(4RuK zK^Y2>We%yB7giaXGZl2yN0IYd<0b8@^K+Wf;YXly12-sqID%d##F+^eoo|K$4Z&>k{P_`l(qo^*#pwli*0>F6&s7;=SK@Nz0hZy3){GgWiqQxOKs7nYypyg?ccGCaxSs^ z=!v-k`!}?JGXMXspr`-{6%4%B|8)gbNekBGs7&ElT?_uXeqo;ZYPAtxxvj^!9)uK$ z81(6TvQgh^rVK~>EgZ0Hl}MBOga}q^-bVpj{tPJMAdGKC2l1b?6-WGC79+;;qOZ6q z`&&8gr%OMYa|Y=sgakz-iWgIhd;tC+nLt?(nt3QQVFQG7zuXmR#BYaoonOBr5D#p0 z2ZGU}MTmhs7iMtXkC$@n2LI5UGPV!iS|B^07uZSo!SvANyj?&7H6Y6+o;P(hR2C&7 zrR&90lZ~;2y7)4OZ+{~dWx%FyzhGETbhN8}U3M%F?|n*Rnxn^9hYlL;{@jVScCBL3 zsZ8|Sw)lHL0qMm?eBlwiSHl`Dlm`_n<8wTdo@D|Jq=uF%09EmtY@{s#VKtz3uM50} z*J{9vz7pPq>utjs-vaaNyQAU%c?v>)d7=S4g(PK`|JOK3mFSM2Dw-j9|8Z(U`LZS? zxPA=Ty(^7s94->h)gr4?`(1(cD0+kZZr?8OjPNlg7VP-vGTu718^&Ub+{H$B7D+2q z6K)*h)|=WPdYfR>+P>-_d;GwZ8Av)gJx5>;Nf6mSNRh;=w0hcQu6#J@s zL`!ZM*wfjvK-uPh0-CD|xJF1w`*UWR>GAvybh&7zFxrAEn(k+VkeA?Q-JYa~DBH{5 z_U3ngIXP~%2EoOAU3iYR+b&DgaLVsF6M0gPj23@ytuS-ECR;OOwz5)C&!P4*eH)sY zpZ_0G=fGZPw6x*aY8pFfY}>YNG`4NqHXGZv?FNm_#?H6TcY3buf3WU(_RPGq*32}} z%{t8Wg&Z~iS@>|)XN;o3Dw*AL_;q26!AirG8@}@|ckV^_!hi8wOlmXN}o9Z^4VjvSTF$kCii!dny{+8F)XgV-RJ(AtEwJM2j z+tbe?j(i(i(#`VFs2`}SNPrg`i>zdh8vUV(QUtGQQ|E8hvh!44&ztOs zH&fc4vQ9@C9lY@h3!gbdaE2uLZn?0h`;KKNIgSlP+FF#V>iFZITSETAf)2n!%)m(n z@Xt4~(Hw=Vhu)2DAiyUq;JT&MMJkDdV@cxE>omgIkA-12nkf0LoP#7NU5k3nkLfaM z&d%-bbtBGk!ma3q-C0AR70$RQ-y?Z{4TuOG<|l1kk@3Z4HJi`dMUVJ)wZW>K@L9DJ zYLK7PiH1#TqFbAURi+{`e0&h=*$8?PYl7L#JeKs*{G1q!!xksre~eaM@$#!#@{Zf& zcZ-Uvm=B#e6OL+M>m7>Wz{~O2Iz$NuHHZ`hXS`9ZR5}pJlAJuCeTnAN>8#pP+q#zyN?~vlj3MjF-{UG|dU8 z%|VnBfIj92cX|9xIFX-REUdSFPXOHdE6Jq^C0@GL^TS!HH@W}~v=i3ktPd}>fGLQS zNdfoQXs?9*o-&!5MWtVk)Zy;wp9UO53g$}~)`4p}n-Lzpz0Xz`c{2Ai{`nwTq>lv3 z>QsFQE~+3EOc}X{w_W3Um=0$%ZtZ-2pNJ%#c3|lG$e=`oTJ@SRNR`-#AEcJo^VUnv zkIwI*L~M*FG^XD3aI&acNhSBnP!?nV=({9X8n4T{_JausXfH8rGt;n7jA|GU|y zS`A0cu8S+fYTLF{bp~uEs!m!jC~6DvhepoFeklinJxKs^r6N!fbg_RSe;>4~JEpbG zdsxLGQ3dXxn)!0hxCi+>!988R6r=#b#xjr1AKd&IAg?gLBLBT6w!0$(3yQ&X=_(zB zlCtO*eWt7y*uFlWtaON<>p}1!&Pac2OZ25?+2{oxZ=DRqq>c9*2!y_uC!;Pd@R^bS zLFJ8p-d2Ud97yr_jI6-4E+#9~9&w=O3rhQV3PvAssVW@3l^iOATBofruH(sy?uZu# zv!l+jDzrP;3-y%)4S?_6fUZ#300y68u~zSUb-~`oUBS$1oo(SYJTtbGTi~xf3aLSa zW`pS8y0ie@{^J3%FJS2h8K${dX849%bpLtL~QKR_o14LxNRxflpwi&hL}?z(pTU!=FnSVmsrr z^mwDnHVJYLde3mN2u%O31bT75B!C5wfNm6p2*~HvTZEFjGs>^1I9cLks2s)IZ7tfX z@q$g;7C}cSb*g}XEI0Ajre$%6qF7(%qu{jpL5h4zffnAd~SGCq2^yutF`LUdo5UGj?qU6$2-9#k2Y(- zYm9e6Ep#EAvaXoKw5>IzOL`IGFcc+ot(B@|qW`p(*b`EUtv>W@u(q!;q?_PD>$day zV+YKgeYULY70H2oUsIIx8{??mF}vi-177w)m-ZWp6|H+HfbK{JR><0avn9 z5O<756m3ujn&6=j2<0K zom_xnd0wv@E{FsvzGsgtVv1Gec93X@RU`ew?`=zGYj*6Z=7p#RpyfHR3)?^76##`& zEa--zf!39l8|2n(Drn_SkTi92^zWJM51%%lWnf@J8vr(4@+$Zi6|NS`eJ*L=S+XEH zYx|f!5DWi{6NFzE0Bu*YgT>K53(QGIfhSHYT8yFKd2Xn%j=;6k${F8kGxm01hvcKL$` z&mQp|ag2cZQcNHnlp0HAt}yd_6pb{?vXJvAMF7I7+<5HC%(bgeAHu{-KlVFd!`RmE zYe5g6?FXTmZ%2x`U{B_(svcS{ zaKjQ^jPW<{W!HOiwy|>HnB^Pj~1|OSDOR1dkDq~nSp*kPDVmWs6k>hf%CM* zhDbt%?3SW`gg+LelDB-eZ)%M!vWb+`>E613_ER()nmM+|vZOOXOuVp!V&keih(Q4N z=$JNbbJi-*80<%QxN4%ZlKx~8o074^$gNYG>WvDhz4(y@pLUoxZU!?xMT~x+whSmB zUZ-AEd|Y74H%Vmmi6mR1f-zzJ3u`1_cEbVKjd1`Z7v$6VEw|L|@B|&hIV$O0n=|k% z*2U`9G_H|Mp?r+KgRvhxlPmQ#-;oHjM!&*mQJU=y(w(@uT&`o2EG6u63QZu?XrM+k zd?LcKhAjC-=c?WXz=SlL{b>5-a#;*8f5AUjwGUgS|t?E1XU4f z$WAM=9^8atigtc>Ok zpK57-N4jWe^zk3n_K1G6VO=YX?HpJO6<5jQLzj}d{_yCIh~RurJKfCBsjHH$-kA;vRa^zREq-6+3aFw+Dqt7eWAUt75zhnhOck*f+g(o1$;i%M3Vd-Y8BOg zZ5>|nG>mG$F&k5h6ZGg?c$$H0gIdP!?(DMuVy9$oXh1elNj8@OS9c7fL+r@7izMc7 zr&SqDkeO!mCf71I$pq$v>!%6ukQLT9m{RTc3hO&!=AQwCbJ;g&?W}97DFUWt7T@j( z)zp(9`7C(kn*YPwTGL`dE zX;y&#m?wG_FqCrD)i5DztbnG3gT|ONHaeM=5$s|!&MT}pVk^*1Zfa8vcRdHK#zw6^qB8Th z+V@nCX_Mho`+=eo7zXPS!{|+=O{PF$9q^jDH#q9KM3rg~*6jy>b|?}Bvp}f#MXJN- z%cJ#*sLT!_NBcHMi*R!<^g^$;mfyi(;GyEKR_SjMYF+Oox>#8s<cWP2VpehVayP*Ga}DL*hRL+L-=`_f%6*&1<4* zG2zL0(XK^bC#c9s6%bFDY;CT3CHS^k(;+IGJ5m!b-_$)b|IU!PDV+M}X6e6J-~(7B zM>NBNe1g6y^&R=_4LJ=FOJu1WG)akF=-2A0 zR(Mug(01O>zug{M?>ty%&9vTp^TAcwAuV^%epLxT#ULB?;Wqi{y#kwvUk%j%;z7gJ z9|e>NR>xuPCJ;9W1%b0{Mt5YWy5N}R z$M_R1F_MHpibGK-s^{3d?|3>pfX1em&&J zD>U1%5F1TxrIdvru6Db>3i+oj^1nD^dyQPXL$>(x!pL>VfL>AMcSN>w9O>F%za10+ zF>0OdKMbD^=X_!^H9~I2d0mTl^6)Y#GP%rza%@AuE72mcC%=nx;^sy_Ss0DX91#X5 zUL)55@rQERscE;pa_@awf9@lMs6JT_a>O+VgjcC4HF-URd>n6X$Y=g@IqYB52m#c% z<-AcqKDFMmZVe=hGAGuLE$dy3dhMs;j(WwNsg^c%<|oK*|0?})rXAN}nr~@`8-(AV z>k#sUN!Sm6PsIpuHJ$DlQ$=TNPuO3n+J%N6(-)j4QcP~)V7;V-psr^aiYmRc6D^{% z8WywN-&iZ-;mSQUv`(P}&UEntXBWxb^4wE$k+c878xwp`#?htz`oe}Vncx8~a4usx z$Jmog%*XMtJqV%X{2h~(g9o~XT`8M#g5$5KX%w7KiQ1hZxH(bc5LqRlrvXlLe!5WJwmS+t8{gxJYUzsw?447V*@*2SQ$!L)Pim`BU^Tn_$St z8oWsh{{an&KpOPU(`XrmzA{O}tRaksL3iUG)|zRs^_yX1^OW#s(m%h(^@W`XfSo+J zbP(hd|1Fz~Y_^IsdLt**Dw(+eKh2`NrE;(JD2~6dPYPBolt|iRHPr}s1(UQ}?tx8( zvAyn4QmULunO+7LL?u@@;lx9NkcwuB@MY6DdcM#Pvrp}e@pF*XT%c`3hpx^t>*v-P z(cj+4R_7~$80>Sv#))b49|A)=_`}0(asExV24N81FPZi94mP5_#U1B39$YEn=9KoT zP{g9HFQI$Pyh;W#M_eic8`gp$t}*`Gu{{Gg&+CuBG{-v`3mxbnv^a-i7d#64cdOA%{Q7zz-;}DS~Kn|$NDUB zvnZimAaI`3dMoEg6i1Uo4N%5iV}(%Z+|1@{Jr@3_SUeI zoa=_L=yUTc($hcSAqsy7mebWY9Qz9Jdy?d-DcW?sAbX`;aeEU&*u9K^R9ie0$WK;% zgJLM7kYOHiLZIQAK-7@V9kg}}{o?Y?j< z#YXg3qE*mb%a&B4Zdxunmu7n#OqL(m1{kQrD>$IYSFH^*F}w~JDE|5{7gInVDHnk= z{S+ZRw)TvmI7KGl7eMVQl>${@ zcn9ZHXjxAMRhAB!;0_U@BZ0neZ|`x+g2QNkip+)fK9ySEfX>&Y#Ryn_Cim$1#Y+kQ zM@NxknTf^jpg?i8l!i{u91cy7KJ?w?Z5Z)vvwV9aE1aaxVv**aqAQPiHYzkI8iv@P z*!qpkM3lLt+nG=osCsmwaV@AKC&f(Yc@=PWi<6MlR!qXSEU!2(TTIe=)O@UYKJr!U z0lJkOoGs5l$HAdER-M70ato5Re45r(>SJ7C(C$$l;hTTY7K37*KK2q0|2)NQfY;74x zdsm?6wy2mpAJ-uiI$@q4BcZ=fnCan`5l4z*g~pgLyhw+l#C&oM*HV=n2b3QhAzC24 zgZkqKTunTvRl#eQ`iX&QVXG<(s$7uTx@hY3Dvno4XeyVETf1%J$Ocv7?$+Mjkf8Sx zXYKO}Ca{{_?Xx=f$n{>hpeHgh?9XycsEsZ$`mz^-h^j@V9$y4d!)74JtEaHoV(nZ= zaE!MrlfCxoXIup5#a}GKMV{T1(~Bn&i2j_|>Hj>9uILc=_ z&`KUA;u|~IpXaM!EL%``?rpW$;(?cfg!gwC5m1CZ3D%C&F;zvF{Mj=7MOS|%#-$2? zlye5iE47@L7R=u~8&Zyvf!7lIs__?ZeFE>k&H1h&U#IG>a_lr4;;_J->}a-bdSZ27 zXd^KbS{ptO4fHZ#GZf<*$e<1IK6Oe>R%=#3`CTTMoMTZ;ALG3%sc#$c7 z+mIUh@3H-4zm81?I5s}|#q)n2#Dpg#VDK(GqH+(ht4rWfPEJ1?WVy>D#Fm2=@TOj7+*!Y_rLp_oVHGm$~f6B7Yl zV-#3W-jRb%$eA+rXHw>T>z{MQ>t0|dG3qP+(7kN9VVCjM0=_!YktKn_qqMC(Q| z$miMr7pT53Q<{t4E^dc_6J0Ay+f%hzRE1lx20lhG+rDLulUoc+xRZK+cf7Pf&yYv* z=f`FV`mG5kVV}T=2NC?eVvsDw+DHVb&)a2p| zdQ2H>*wooqONqOzfs~_0pYKwhqu$x!kh(;#!BC_AcM_TpUUkRU?7$CLY3c!`TeJ!~ zZF?_g(;t#(H>_DIWarbP%kK?&)V5a59P~5(ioJvL574_Hh$=INqX{+P$IF;g7uJJs z*Pq|)ROZPA3)MsI;9c>FLRrSdf62GXYbN|T{}n7B8T{jx0HsUj`~ z7W0ZqZ#?2qe!j}zbWIZTIi3)&Es3Ilg#Q5dgXOLE4KGj(-a)Qq-W8%vtB$UEF(v3L zW51$!9N6*`AF>%Nt|6~B9`XvJjfDCr9hGaoLQ{Fsp!v8W#-D?U9 z=ZZ0E*M{;AxltwXNvHg_6f4UWD)Oy56I*kfS(26&v$+0kV+eB%B+uzPIsv4aB8WfwDO))ey@4Ty51CZqk5$QlD zqvi#})f-E}=>+6u#CItvHj_vxu%i3gDCiMH-~1H`5Y(F00o((ln)d^k67L*CBk-SM z?qnQxr|+`hWQyX&(iim)!Fto(;CaEx50nAZL66mOpXt05OZ*RNYnT)?F0=CPVb*eiMnMWp(jl4C zPux$PlH_R}D<0aSuwdA3pb967k=&aC{%(A9x#n9Q!{T_%NQTYkY5h8}$2F0%Q7*IY z01>d?_f_CQUC-e96CJ3#VvDm$pw-t0BOIZ1912GNFJftYS&R~3G4U7mZjetv;JA+T zrPm0jA~hO#H$FI2wK4@k4ArJ?IG#i18UFJU?_l8!mw>4yemG26I|MpaM)5SxO!t=& zfj>LH1}_6xZ`32!#BtYk9LA^W` zWmkDR(g+g0#EJ zZHYsSFvZR9%^|cF@vhF>)?@0s&=KI)iw)h8e{a@D1<^Sll-#<7NXlbe_|0D;B3gl$ zLZPp5G14fX%Xl0nlU!T{Z->o1$Q6z?@%^BeTPb~h%(_)tFoT`7#0( zzz88IDg3}EZ(4f#uSB>$)K~F!2GtdF6f7l-mWlRy#D6#^5%~xWL{5Bc~{uAlRes6FKlF!lRVl3^}Qu>u!vs62{ zpw{X>zXm3&^XPJq;e@q#J_N2V+|l*Cj%ofT%2TOmhXsn$or}w0=t{LPNbwwK^1`}r zEk1{v6aZ3st7{7flh5j!DeuFOv)Bp%|FhZ&zpTKXS zSv_Y2w)Z=B5}nU=d;*+1Zejf>>259Z&6<(o2lnfh-MZiK)E=DRhD+-_gAlsD9?U?5uq@m#GFO& zo{0I|(y6!l2v?KJCO3&$kh~OLJ*e62Z4kf*JcTqqFC-jF>@qc4^Cb=!8dB*zL_3f? zSTA|a%9L}|PS(J2sXR;Ry_eh61wjx{WaJrHi1QVw?n3P};bj%LW4Havo?fyEE-&!x zq44`;DKPb`;&3C3t=Zv+|8(Q}p{0SZQv8dz6g!`Sr0Dmua^H?YO8w0C26W?p;Q$$b z!Jz@bDaCr<1O06y?}2r56okWfVB5OLDUYQeAd7;V2;I(Z`ioOg2h+IBChf-x z|F#v89bYC>gp14F?k#}7q42;yi8Z>HW}mM>Ax*bHTGgwdYs#iLhS>C~M2A}#BsS>C z58jpu?A~7QxDtS=oZk&{=s_$#QQG(2V;(f4|rJjrw4iK7Vg$rrr-L!`J*~2BiK42Y(9xgB{_}v z&p7h!Ce+3f-6y}}rpS?4QWr2^uJFO2Wd&sS+GvLqR)2B%Z`nJn8!L5T_^rtdhKK@3 zsWm>#|NNKP7Z_Rqm~nPhR^Yq0S|Y8=dO0$Rhh;-+f&ALjyha0_wX#Spc65nT_;imb zg3D*12qcHIR?uQ?Qa^lkOJ2m~dy@o;6m?WOROH2*a`kD!vh3yR9 zM#MAHz!UE$r=R|j@COqSZx&2x2I@KnOPZ1kcapzCY zQ`qOFemXNk7WLm0C0Xl;g4q0{LT*M?NUvJB3mOcq?uL)2ks@WPx2%qH7JgF|+0lhA zxFrAk7W1?E;z0-C(S!|S4e}}bmTUj;xY(8*If)$Zn+ZuXnLuFSGQ)yg|xpd5aMe zJ@9Ji19YJ=l0r<5umF7TVZXOj)TU8_XxcGfO=DZ;Rt>Z);RnG18-7AT;BKMnhCsec zN<$L~9ZyyOQZY#l2lqB<^NqrO=Hj4Jhl)&gJZcx6B0)itpklUL4wIs;v3X-Sm>V!T zY5fUn7zPaxVHW$!P@N&HEU`z}o-wq_spi`PP(M>+;AYCP{U~w!dc1SZe5t$=^_^6# z%#esdgiuhMfEL|7D3_gt)KVWuIDI2OYkDdqNvHUyPrtX>ctexNzsJJ-c3-6F0i=OL z;i>-96CZk5tJAYVEjL|(O72n&?T{?5P&+!3BdIO|WX(E!SSFyKr!<#8%4b}Il7NEa zo8b0Gqzt!0+oni~x+jhvzt=Dxwn0MDq1W%2#*)sAZoqY7MGOhl0z9t9yDLw!hCss~ z+ns?aUl;qE7j3x9{x7pWcOL9}eSP}I^}Qq6ez%{BJn<8|7<5MtjutTXaRUrjJhiMn z_8=x=F-JEn)+b?I=ERr{xYL95InNSIuGqim|M+Kijxa8ZAW1Yy{eS?~u>r%9*-=SR z9l1PpLqa7M5!}?`I&Cg&nF$hgp|py(0)Ai0mbK8az9qLB3-^|Qp3lLb?v#T%B#c9| zi%R2;ce5jQ>o`#!2f^iUybniF*LrofxpOzf=m4V|D*F(`5;4w6lSCEHglX~x0?-A_anY=N*~w*W07 z(S0ogh@oKgbfD;S15IMrac#&52_*;AY=6A4hd1L1rhYA-MCEtD?jV7)WL6qFH8mlV zH%OK1+OST=w2d|kT%F(>ldS8)q|~5T{QfzgK0;aZ#J1)zkxGkqh1E=Kak@UYs~+rj z*7?CELF%&a{~kZNeo60plw8Kj%>C^O1@gVJUZJce3; z^#G~F3ZV<3efx>_w!K<7242PG6jj8*Y`4z2>B(nH0=`-VFFVtE_x)}e#ujb~C+1M>P~ccBSFlqTju<1;TyhP<6zg)cOQ$E_uMf(vD$I4`Wo;DKR$LgYR4z zfudLDAxVT-+;O<1naw`_#u8=L$sF3Z&xKOfHy`|-xR`0lNpYUA7vp^>9gTys6;k#w zf4S~7^FYmoHSz;}qe;b*O-+c{FK){$-oHPWqx%Zj(OIVonpYFsMx5guZbY)E*Cf8_JPLxzxp+euAvLSy~y$+KG_Bq9>-)hI|f%+q#QA$m7%W zO%5(I9$FWwC8D?;^?)nEYRL~NvhHbe`>ep1`08`L>!m_IHcc$u3pDqm6{%8WNcb;1>lJ z00sVQi9V3egExmmbp@a*b5UlaUCsC(#CV~@fSZqC^QVt^5>QK$bTW^GR`^VKwA z&X<7LqUyAAI#u}k%rRzjqQInnyjlw(5Fc5QJ@BIZN#U?GuON` zolJ-3OYmh!HX8^D&FRksKmK?igdb4_)#X(gX?l|!SliN0rt4zXCX#kHVb1J(zyWot zd&Ky?ZPMFW;wrvGOPZ+#`t42C3sH_=N4V&OB!BApDx%R>ke?;7ZQi*^<5?Hl3DS(S z-;q;c1jM4Tz;Rs0ZkU)!Xk!p@xoFMJ)C!JA$-T$2Z=FeHjZx|Z2ny*n58RLQ=M(kX zKwNJ_GT=hG=$8mmD2?}z+Y%qrAZyciJLS;5$HghSwc&XhxK8%EwNE&*alL{e?#hWF z;!;E0WOzT*4NxdiE{}H)U)_Q4>^8G8x%IX>?UR;ichTaRPV1UUwTMwC>oOyFGQ)>J zWCrJcs)l{!f2S#Gr#XgOI3+4n46@l^$?y3w!m=j`!g=sW9}TUK)8;I0##T6P1&)*rD`rgxCBdJSMSvUnaeI_v|H zP_g4YG0=FJhRnZv1Wdxgl2ecuK21cVVq%UyPG8e~nZ5QPc{ceG=2BnV&c3zr?jK(X zS~i1A!bQa~=@``SXgAVO{laxL=M})stcD-0Da(*jOG6$WJn2(d&d>1`u%MFZWKb>A zB2OXut^Py)`RVryA7vd)xZ+BDQtc{P`iU+*dchB5sLn}Q%D}n>|B}up zbiWk_M+~n0(_<|k-k-em(2_m9tyA5s6je&4$!3gBp1>krls%g(Nwpbfzb?4c@Z^+7 z|b8Qd`)Ay7{BSmT&|&BmoWA|&JOPeuK(<94FZRfsD})hFq?~|Q6%jY z+q8q2Pn3PP*Y)@`u^j;t%rr~^AKffrEaZ(;$!U_q={WR}yo{M1RiE**Mf+*twLQQR z?nKSlB|-A-|8s-+*aXO~o;XM1@^d)2RrVvo@ zl(j;RO_;;p;`No1jAfu0$=Rw4;}z#fNGnY1YTyOgE@f<(ZM)zIWoO=LMs`EVuxDST zv3lv{xqWB~-5_;5_rFZKO_(beLM9z~SJQP%pi6CURw!-0$E_gfG^LY98!GFIltF!* z?+Q~Lq`^%8aJ1E+s|hFDpT7#`4?1&-Q)4@gW3RiF9ndOMEgsDBnjEQiyrB*J9TNui#Ifw z=%GK=Q-c1ayR8Kewq8Ke7h^I&l2f*gli!iJvPV!afs-MpFnN-Hz9%5hH({>l6R%bd z@8=hnU1e&9`a2ya$IFh;Z4KhUv&10E*B|AZyPAag6$2RzTC zAVncG?w7Pr=Po^P1d2uDA%Zn-3bbR9r{3U4&a7pe2;yhG5KrOOF_QLWob9S!{^o}& zb$5ivAch#}YUxE@cRS1?F0S`>siJbf& z;;fTxn1ZdUwYEz$@%fzWn2Oozmrvy{M?v!vI>8+p9x4TF)th4nM3^J z9-QW%W)su5)X(YXEJgbqXd+ip`oHi8*mwV*AN3*i90veBZrVBGe<)M{-ZIz zZ^{83i=4&b8cy1yxrk?t*ah7mBhoz%h^4%$OrDw7hdA86a&zWBa_(%(fO=~yVG-nac4`zpbC^fq$Hg4K8a z`=|9w{{q7W0E1ZMj{vBI)Qbn9>_nlmnX)L(sh=yvP z9!iHL@8mT;6BxNHMd5_KrZCG*8~(`c904o(o=zRMJ|e#DhAi<=2tEzz zYV-8TNSrQSf5FoB$u#vaFg$`g%11=q*HX}nX87(d^S6{UQ0vi|rsOOi=}r^nEya^V z1Vb@S*}k^TzNIX$#Fjs$b_uSIKJ*~8HqY<4+J%At z^(AG0@!$sV_`?(K3FssEn_m&c+8B%UE*6=3^tYxAVd{B03#C};Sk#DHTImk;bq-h| zbyYF5Gv`tT*cOYfW~INATiSu36_L?FM-`TiZKE!j@7D;^4{zx6xY#PU`F=bUv&On5 z@Pxmg=ZOR`B%YX-lqBn^Uzj<1;-2C$zH ziN*(fehoT5ie08#;Bx%dF1sO{;o=!r%<+_z=(HmtyR*1+cXVrN*q~ERu07& z<8#!mmf!`$-0l5BTVi0^Nw%N*#rd!V8j~Ttb2`p6)I#AQ$?x~uu*k#z9QkvZmkfX9 zUtu^u|9m_q@-JUbd%n3|Ad!$Tc`ib`^e&BD)7@1?($m0#0fgXk}FIofZ zeu|i;RDnFh@HQc5XFp3_I>Ea)w*o5IR<8n^BzJCyOrJ5L-F3=LTnRDg#wRN3*ptNH zecW^rWKBZRMHRa}cgV#%c;OQUqoQji#cNGr%wf0m^_sLJtorNhmu1Nf*6$sKMB$No471Wl+|*Joye*xu%cF`CO zclTv}Y5k~GoN|+Kc3Cv?=m$z394f852D-xf7N|o? zt9sz3%7KUsWtZbL)C9I=Q^&p5fU)-T5Loa(5d?8qe{P+ z@Bx?IIx%DgDj?Q9Aeu@glt%eAubJ?CYnou zH$Mg%INsJFdCv2@Hx1LcRL`f!Wf;yT$GdF#Jg?^>%vAh5d$&!23~pw^>uQTi^Sn+v zLtDlqmHJlJC?z+6mmmB6Io8>9;HfmcgBJ0mZ0l}}$pQ`PmDi?PG$ONPCva?Q`?BF%J$}Q|7E%Sm0u|M0VtI3 zWAgzm%Gq3>%e!C@_csrywE2%XEBi_RAm-|8xcLX%1R3CZ^6T)_lHOL z>0xgztxUe_v3_O(_ z+zh$)+|e4Iv=F4z^d@)bz|FlkSw;ph-*$8##k7yXT&PMVAWKih)N#Wn${v1N(VPL_ z(Cc%TconUw2f{3MV!8gtmVR*@H6OA~;|~;C<~>Aea9uid)5~Oju{A>Gy=pK4sbzW9ATjmRQW5P{}@&m(KUkO%;f zoW9>d1FpfcWS`OEf2mQ;i@uB7r_fmL^ZZn6qTk@ylKhK5{LP1Q@G;Td)hsz!r2*U{ zW!bvlMJ`pd1Vy5i>JGkV#AhXw`gE{3(-|9OI7?JHhMS4f`sV~VMSB#lLthQ2fC{(^ zwShKynmvuPi)OJJn*Kh61dWTbGr{&~a4l&3nS*xGF;h7ltlwz$%X|%lr6;EG#JWR* zI+9JwpxInEiXoqa{<17oe_0}e3YY*Yx<;hUpComl;9MBgMau;%2WTArHKYZ7!`QOYeXsB4sD~(*>?qRg}Y{^=`v) z_S*Ju8#5me0t^N-ZQow$iHOGhdKYD5W9LyeYS8yIq(PBLZn5WpNphzHh@UL{^^qbsmm83!y^V6+SKw$0cp&9P6Xw^6+P_tz> z#|5?XE!R~9OIE+hc_VIdDXT%N9sE4B-%8;^W}xuzMgpfT@tuMrhrb4GESVGa+@GM8 zpyF+l37w|31e$zQ_mz$fOau9)f%xzcq4dfd1NkWaQhNoB%@# zkg%m4Z(Un+!FLLjBG@-am`*YEAxvidyw}*0M>EzXK^mEskEJL5Bvp9Wg&;{yufSH> zY)4~zHxv#g)(8g{PR1dwAFCKHKJ+~~i+dfJc%nI$H(kuu+ENToxO_^ru5u-X;h2tE z;IiSX@+RwAEYce;bbs~HWNBd=r|?oaa_hrVX89`-tJ_a_;yZ>wM&K!80k0j2|`V82U?9q8Hk zT~d?T8wNEs6tBNM>ahVdBei?5Rj7Kho{l@9#0e5*L23j)iy~16%lKZ#z~Qgt&-^1$v*h)KM*l$_ z?4r(%C{U@4j=a8C;?LuRg}Qg3^;ti#Azh);giS`A2ix`?Oo!LJ?mZb>#Z=eJN=RN6 z75orVJ02hRuY~bF!n3C|Pmv>bp_)_sqw&q_Fv>~G%jwknVq8xv8rQ-{>$4^@ z(jjU37cPK$zBC{L&;Y%DUmM6L$XhnIBRhIM1#Mj~8>hV`%JjV;3H)vpY-@~wAYOl|7OOLhX#cA5-K_L zWx32ON+wI~AKs^)0Dp|8RvYQbEt~vm=mP@s`G(I{vb@)d(5wyXpf4J_QiVH(ysVjC zI;*}>HH@Eoec;E*RgNS6O8WhiJ6%E~oMdN59MBvyt zFZ>5;Y$##j7&FZ#jSD|*%2+p-JYmP^fK>>!1SDWwEsM%-oOZKd4&81MHv+d0#d3Dez^!ogsKi1JEjU?&c(xk}_o7`4sf{&M10YGFak2mM zz2PsrhymR`!BX(sWYOuKB zxS#Ndfdufohnl>dUss93@)&Sj4a6oM@6wTW&-MCzQm$I?W{Y{^M45+j72d|Ib8JnS z^scn~^oTDD6_*^oQQgE!(`5}@(7?sy>UN?sd^#>P+%>|6V57+R+BP}~1f!0|6S;s82XBweKcPeXi#=l<-k zTv_3wX{nTIx>w`7E5h*1;fU&VD)_Hkk<|g*1Gv9Ak}0J=2q{+l(BNHZ z^@pQ%HRy#HWgGydT|)Ri-YO4?#{AqvnelPEo{4fwTRNz>lh@S%wf+G%yIJYx*3l;s zB9O!#3FmVtAU8AX08+!vIc=Ec4EDInw}rImq9S8D~gKg^_FGw?F z_A@=!LRVa>5;@Emy|*L3k9G+dvk}G^$t>WHHs_#W{Z)HvZd6DdiUwPNUdH6j0@!x4 zCU#pki{3dru96G%H1xf;w%Y4<9Xk97Kj&&7Ew8KtKlYQ24l;#=6X^%axNis3Y1Cu>#8vE zU!(NSeHld(U=*IxK8F7z>KqvJ0G75L+qUgAX>2=^&f#Xn+k(G+e^`KIrbmq5qd-&aG|$%zT?QZ@EIp9(suO*!htlnXP*#mC6$(h13PNiDS?Xp@gPj32={=SKhM7v<3aD2N z!GSPE%mjo7yY>TTxP8hd+o{bv@;mmSEycX!W^h=6kcme8z zB5ACgLEG92wLe zk^D5Q{tG+GQ8>2Kr!Wq1&V+t1L&K+QL#z<_dct7(hkj1#FG^Q*$GBN8?&aspQ$%RK zZ=LLBiFqnQS*>O^Dw^HDxV_Q+z(P~NE>VbA2HB(hE@6=zpehdq3~yUv$dKSNJ0FO@pFyoeOfI@cydj|1aPzf6)k zoIv+(rHOb6AB{dHYN)nj*5Tv?| z14E>r{wpLXAqVN4MFQLP7+4Gns!YWM_IxmNU?g(BT?sj!4AS z^_VC9#s222>0av{<CeY0!nB@yci5KS#7@f>*RXF6F^ur3 zW3_keydnNo&y#pQRZDcATM$_R!I4~wm~(yVXweTjf%qgtWyW5crKo*6tf1^)jPkd}@+bYK3NX$qNG54L=&w ze5&&5^HtNn>pN#UqJ4)G(R5JX(VXWyGrY9Fe#~-CmnL@g?K$M9VBFEy{V-B2?*Y^B z@l--c>m?ZKy-Hgb5SQG~z29vrD;Lo#`;8i~pnUnUB6sk9jh^hl+9#&m@}TZOSWRRL znK^T7_P=nk_XR*60N~jm>c2UBF-kStcnA9D)wzdYn(w~2*B1`&MKLjoZmXjnm5n0h z3QKifVp73v4A+im?1ooe@oq)AS9n1N>G;u(VTj8O;G}))&$s2|at{aJ&05`qSbIDK zh8(_Go^q#QfU}p_Gli^2N=M=1{w@GE;PXVj$~6+fx-$Jc6&koOdr;Rjh?J^AF+=vY z2O=f8+jv?Lm4MstzNe{lCsL?`SU3XXeB$l|LDGGZIL*4?6rM~OyI3vsnazIglq@Q}S6U;sY+PcmfDio6jlz4k{fWt3iCsnNy1C6|OpOvypYzVxmJ2fOC zdI@m_X|=UDjAnRcW3^+L%50`)@w?){E`v~%JeRd;F_i~{wS5kQ>@f$%P$&H{9e!v)@AtD`&Brq#LC?)6Zk^g)6oqZut03bM* z+yiKm9=!b~(9sroB3%w1E}SV1!?z=gutOEpmw1F6N8t?8AI@&{qA#c5vv`7n{8`L# zI_N6!^GW3666I}K>YPaO+J}?$x2D@ZLQxx1l+MO{&;EM6c`;wGz=mryS?a(t(52VAUD)Cs*=aL}k zC{wO|zo1V{%(zIvO`W;hi^lwH(P)gvcVx&VRfdOe?=(cr`Rmh*T}ofVz(CLmyVDp9 z^w`lz&N!`NKXB2)F{Ba0V7Fdiaefrr$h#JQU^CRLS-#dZYJ3wO^5^sb+W-Oc4^YQc z{>cggtQ(Rtl_WMq^0B*cp1UO{nvb{yW*+)ca12t59D2Qtaaa}NFS|oj-E6x`oSg5e zTct#2M<&zBj_PaFvB6_;#z~N&rUx<3xPHFh0yHpf)*VQ!#tF6yWNmU^>!gHJR%XBT zPt3d;6{lDEc!;gpF!RIyCKDY;7h>t1CF8Y&SY$G#FgyhId#Ois@~Hizo$G`cz`L%U zg>LjxJd#FXwsFUFlV`gL4aX!byT*mFpk250hu1;{`_=WWFgrtj1%`Ps zp6!T5YVe3sD3UxQ zT+7`r;S>y%v0E57k}HgdW%ejocDgFE%fQ<<{%0@5F2U_UEyN@?YSVSNuo9cQ;v@+= zu{;;aR+U75F8Plx!P_93YM>_${02-rHCNm+j-nBY`l8pAuwGGdxNV12VerUoh)vc& z1|o*GxsgMu_;CG?clRh<`<-Zjl<;#{C@CwU#mPDNJwPSt2WpBLhrnE;kYboa_q>wM zmEjgatTD>11^zmpaX`U&m&y{n{iy@RV$kNte7H1wz|!w{Cyk00U)dY#8-!9;>?oBW zw&^;!^~!^rf>z|Vwy!^P!I7{D%~ocz94p+ z_jfDyUj*>I{j$Fj!2a{{tpI_=!nf=$&bHCKH~r5WU_8^w{WTk)*g}|iZq}w-J)BH12#4}-D!-m1{GtOr{W$P zj;1ht)RCfV$6EhohEzOS83S`WTab?6p}ZdQunY%Icl8(0L~~tae4i9Ir;K;?n28 z-^GjOCA-9o(kYpK`Cgi#?9KsQO-}UG&;0y%iamZAP#Ivr14u(a{?_TuO_y(|OYXhT zbLFoeOdBucZLaxwz5*K4Qy$D~!(m`ajm=}%g$t$yk&mcFLLa2407$42{GXqrn7BXD zGex*C)JS9%W(5BD>=B8#tNhFp!lf$bN%L_bUHsXxC3c*g#3dfPs-Z*VMA5GbvH8Oj zZ{!a;y~Jt;m;}03pYaG<6S+{q6XbvkKeK*FflNHq2ZXgwBR&B$60=GT*-s{ z*&32$FYT<|YTy}a^J*TqN9t{0>sDQ>xTyC6O}plaJ&+I*e3>Q$(Y>c0vmPv>zcEM0 zc1NgIcgsqn^xp-v`{Rp^Du9i;E9D-b2$QMn=yjIqhWacWKzov}^crJNy*Aujkz0SX}NBr1Lb5^cUq~n0PNW< ze!nU)zBfXHYTJu}VoVaT)eXyzQ#)V5qJhyw34KZC2_JymtqvdZs=Dq?e>qF(@8c!7 zqz%UN046WLC}is+;4309^Gc)VYOa2*qx9;0pe2u8c9vp{K*Q5u@t$K`Me4`C<2Z)i z23j*S?&5CLEyPwfC{1!3;v^A9K%>4!_02MkA98>JnsSK%WJe4QI`q;X!dxIgX|>0JCvB_?4x_PR5Y z`87ygzQ?*-jh+9wEP@Ni=O--m1=ouV&s^s-nFs6SM_qf7+(LU$Ja%R%6+Bn(Pjc*d zQD0T`JI3PfVRG zjM7Tm)tsCqnB*~kH})XlyP01?JDS-wqLCyd`)$TZV8QWiowh`&l*cS=0b=r?LX@Bd zN&U^co_KJ}rWYQRp}UdIzu4ySV`^6icF;XJ%fydJbd|^&?~^>MkSK#>Da(Dq%GVly z?@=#lBQ^c)r^0tfjQ^FbVZKOc07&%osfqn3o@8A6{a}%aaeu&RyiH2~rR9`Gxq6be zcsQr{K<>HUH@O)`)C*E`!6zuzWboNoPGl_>olWzrW99NB+<`WL;ler3?sO8~L)8|& zhn8H=-e9l^MIE@vc1+W>x|*L1K}u}xiGq^XglEtkEscZUcRKpEYdiH`BO>Md(GdbJ z>CZW^lHrgyLlEe7;0glEWbMR}C6n1a6MqdC3pkTJS|ov`62~MJs~I~;Nk3e}U5i#w zuA?#QT(g)K1{kC@w1)2)Lo^!@=@cqG!a!w^vz3zO&hpS=c;DiROf;O_N$yUGy`E$d zJ4xV~Q)Eu+QDW`G`#8{SCikA#+=0}%|J_~Tzu0I3*hqPX)c!Z)!(qN2ypIPn=Z^oo z54aaN${%k*NLg+A-i}fsi5%1%3qJR;0?r@j6Afs|LWQ5UWezxse5A`0*b_JA{m42K z-+S!TQ`V2B-t9E!0{JD~D0AsSRjiBDI{4F@a9m-npfU#)1w|DXZH-BDA4mLF7wn|f zZ<-3jkf>XYhVMg+XvvZmG$%LvaY0?K3IX_j>xbmITxyQERSQ)O&WSN!x8`0N>==$l zhW{j)a9hfEpI%I}m5#@)I+#>OaZ1ktR({6?FeNPU?pi%Tb~C2l_grK_+aFkf@9B!TCj;U9MX4~#GJ7Z@!77^Vo5?BbVv5r$}Rhry4gMl7NNg!IXyWWSQFrYI^`lf zCASj1H&sK%*r|o|Lg{IpeS33ZUR}+;OJ_98;DK3lRd3l$0k=A?axGQWS-+4Yp{;=F}zrhu27jngG}x(YxfF|tB&BmDoArDjJLg} zLt_?1q`aO?_FVjK^d8;fM*m9&qJI(41`zN`V|@d@2E6Tgl^jjAX9EcW2N|niRP*Lc z;CY#kO_D_GJ7|Kuco0{9Y{vT~$9f+#J7;SUU2@k83!?IyN}?%tBTetH zE5yr^=(v=_+Gy7|$mziJ<_$=uaaskASQN{uwS7LZ4ThQSnhgyBK~q#j$nnA0E4usK&hq?^4E=u7xIvEE&@StK!%Ew2q#2A&cdNJv;V%J5h~=5 zy86!ONadmEsihrgT!`tQqShz8r?@`NoENh{yXBcw9|g~sGKD01`?1;tOuFC4E+Zug zG`|nSnD>`#@cnkV>wm9&>@OHP02tW3QnrhY$@h;7v1jpPHG6-p<9&~+oHR`n=n{DTv%wq@+Eg><>f zp1kop-377`0W^4x$$KjvSDBcqBjZJ-IJpWR!d0GYdb5Nq*pM)y{&ac8jzt@AB#(cQ z0`H4}E`UG|Fzywg?*$+Wo=a)=Cp||K2r8CV_ImCW$c4kmEBI*J#5j9jp0nPi)FB9% zV8A9CJJmMyB`YSjmK`!0-uSP4K4dLplWy7j?+fekW#H_h3MRJ%e#uvKo3=VCo^ zx7oO>sm_YD4|4Rh8nIb}K60)Bgj7e#YTOzsU3nO^U_Lh5_`Pm~#QfT|<#XN%q(7d< zUOefrB3Z5NP-ulvj&8eI!_N`9ZIz}{ay4pMsw-qPk|r>S8b}d?997oR$JO}F0gX9J zxLUSv{PHX2bVUDtUJx3>*Gx%TT+B zY0M7rT*=Zr@V776u5z`0Q|XuTq$)q~w`3I2ak)gmfO2mIsbIaZ zd8-uLa8Cw31$S%?Ek(W3`Ovac<}z$-hT7zor=Ix+Rx4vPlZ+W-iQDZ&Y!v>suX^Oi z$z#S9CKJgaXUC>uQ@xHWCz_qEby*}xRqHi~TG^^?ls1CfoHpW6V!pue$a9#!s|JHtKnOYD!t-8d2Th+Lgf zk|kVFEN#!V=kes7x)bvPDK2YIhi5z@6zo`uswoVxaHP z%(X`D7>ALgbwyz)2m0LSLGuv1XY@VKmHgGLaB-7unq~Y2F^dgM?HFp^$9Cu|X6Ott z@2~ikS)9~_CWh6OaVPz%dPBr2M@4EcBI)RAwqM|3NkGZeLOh3Bvi=?c{14Y#iy@x( zR!A$=uDa}nNu-^K$`i`O|1gG6gAmJ(tEk*rnRlrP~Mf*(S%y9+0;9bY>J8+kD|l-o)2-k`f{*rGP?XogVu zDNRzQvr+GHSspN`u-?Vm@DU9qPuAht#0u1b=P#f=rU~uDq`fyEi5D&qSC*MCp$`}C zptX)^F8$jOKvZ8YU;uCdrlP9K|BR{4)HPdv&sV@nFySV$BA>+gnbS2uop;E-i`^qd zgWp9If$GGtay;^^ly?jG7bv|jfHpBwpWCpZfZ*nHIoWoL6&m_BqQ1Beo1m|>>A0KI z&8nBQ>+&wddKWs5h4GKT>5b~*l zbji(sC9q4Wm`axLG0U97H`zEA&CA5g{;%JK_6w0A08vLW6DP=L;G0J>^Y+iI;7-E} z+`p*>uf%}5r|w~a&e3>}){Ae5&(NG`n5 zzZ*4|Wnc|vvWgI&I(I{3{Mf(G#y*$9>Lxsvgpmcd(%Ssdiz;Tf+@%6r$h=nVUBP^+ z6NZ}BpRsP$*nrx7!C2 z4&%%UwOQk*qVFS#UR%1;_y|Lc7WGfrK`85*`*A*}eBD0H4@uoGY*9ETV%8Xqq!Wzmh=ZIB+O6dX&J*3DeGu#u?uX-%~mu0Fib7h%toDIx0o~VT4gPPG|v#uM9 zmAHAlHcN);TPaNT?;Um1h~=Tqym$@+5Zn1n!5u;K%;Wj~C>sPABceQT z2~q;mK9+2U#flcoxS&3^cf6Qv-_g`S84G*uU6?!ofXQEzmXmsI(!zK(X*D=KsE?gYnW&f_mwA9BL%)M(PR4 zZZzf~1Kon6CFYjOS)f4!%Qx2cid6--tv6u$*!pUdA<7bS1$~C@M`U8|=k9Jy`%rI= zQB$T-4)=Ke*F#%fJ-BBEhW_hf3As(B$+v?&p6fAfL!Cm72gkHG5 z;F$hrOQ95G1E0O6jZ-aO$PYB@CbXD5(IRv#qLNuuuI%I}%`uR7Q(Ngd0V6Gu6IB#dVWDX8BxE;we!%A{j#w!te&=9O(x-(~M5E^91 zBZWJRimJ;Fmz0sV3n|0F+jvB3d68Zq1H?=vCL>c*dfwcfauL5k_&z5vfI_ z^Q5U$P77DYLXchxh+dS@WnlnXkzc_a-8Rw@rRD_6&ELIj^U-V!ge|w?k+`jVCf#JW z%u=%@F<0qaO_R1C;Hfk+u)5P7VqnZ^0A5-jKs?CSSMM|W1P+#$(3peOhxma5vrzM! z24g*MSohR5eAbjGu~Nkoe1%9m@T%`ACX5`+3A5dpu*UscUZ`@4PiO6;O*=L3!~jy7 zD*kMXmqbu`L5J9jrpb`gWLL2&bhh)A689Ro(Bq+~{f`fc_gYNBf~hbzGvNt|0%uPW zS|Sl~dv*3L&_wiR6786@+X@)v6uVM=x#)*ZI$FfZ*GKB zI3k^2ElN`D)9CUOuL&>+!gMz;+zS72)Md1UL~**;*g67k&M`8Ks!?`9kg%Qhl`u&! zwawk~)5h~%Yr;!~e#>YjC?7D3-{Wqq@fz!^P+%y&oh@+Zm3Iep`AL7`k*4I?L5Lck zA0!&e5u6W0_Cg27hEGLKq9HveIm+=H~ zB=a*L@7P#tP%0zWEqyAP0fpTNhLje-H$c|?cCEUJnqK*ceHg&ej%^~Aehxa zh5gU}rdSN*LA<5xhqSfJM8J@$JJ)b*bLC1Cr=_qU-fUsNxJII1gl04oY^u@d3EBBj zFA4`Z2Z7E_aU-$x?C?n#@;~viEkWT>^)!xJdM2i!h5!5pLb)wygrhXgpqw)>dJku@ zYZ9RMVsEYM3v?qhZ(tB`Fn3_Y+;#x%PHHu3Aq7GSQR$uBFr$a~nBmx9vx-G@wjU@= z+aS~bP&pbJz-B-b+}nweLew1U=5K52KTN2WDhS$l#sYxl2`;4Axu!U}+5p6Qn?;8~k- zjRaPq1dc8&cavXdq0ZGC9?d4$aO57`6MR$6;AR62J3|hOx{EoMD!%1cuKNu~4c6|K zf72pue>S#OdM<Ox=%^!1dj_knWtF#53* zhqi*gF)Whi3DuxKq-Ou-h}(u zyWsg^U8@g0vRYVIr)<8X#rf1-P{!Sv8=EI2^c`4j6M`%JDUHITjmz0~8|v^^;1YD; z)mo@kTn%-{P=dn7v}cJMw4Y@V{YGS)LE`V#w0~8aDohSD{Lln0nRUQdzQ1G_^rqq< z@bm96?*Ny$)(vlw+Aa^N8`2bH$hdqvZXv}kQ)Ah?CGG>pgWN~rn01npJBXWg+#g>d zMe`p6!hyWn#FZ`bs;ov69f%uJg2v22ymK+?@ARc_LnbVSV@l7bUIr8!LLM4F5h`N0M>t5^FI1th2W}md{n&ErM=&rC$k}5wL|Sf z=M*k#PHe$z^O3B4yW!WFABUkN~qJxvDF!*Cr8p90q?85<)hF_f^(&37cj*;FG#yiqZ*e2dfAi@}ofm?N~UJ^%vL zUo^xIXEk)HZs=#qiiJp-2Dx{=o44P#H_THz7;OLv*}JJs=$pOATTudMlq8XzIXT4dxuaBV}39P8?r1rMv)n4!dR4RoWem?+JCFd7s<60tkk5<%Dg^77?oF8*S0(~?mV%OdxrzH#k{K0p-b~of$YSk0?l3-Y zW0B*y#_4(~1W`Zq*++o$L3NMC6kyR$6D#5C7s#(9Yh%Rxl=c~jnLMjZ1?h;nsiQKp zGKfDBlaj~bC__A4yh}EV!{YT1vdtx4>EvyWdQ@S;w-yv@|9n>{R!R_Mxk8Tql$f;AzIk3;Jp3hRYKw?I~qvx{q&FH3fPOZ=rZ%-m|XT)`eE}6 ze+2?2OUed;uOUeAP|YF)ESJgaHluE@|K;8*-9~rN!JX&2J(05;Hj&{G-)fdfx4#h+ zBZ6=+zsm=c&z`0p@Aw{q_LH}q+06hIp2)c;shf^%Px`VnwQ_*#D%nz9MmdGG{f_)C zVt>cpU9K%$($bY+C1$70Y}y@=_F;nR8Zm~Q`aUfv5Z{`dul#(teN0u% z)1HF0^i#g-K%w&tMOt+nK3i$b_hm`3KJ+^LHT?Av+z@1e)> zD!%VtF?HSgVU6s;fcFelq&12p?@PcuOg1v-DoNkmSlTSk9761|49}q z0{8CeoF!UkFYvGjl+hL9Y-*ND2S?+JaWk1IlJbL-v2^3D#!B?*D z1gwlq;F}ItFGKJNrYJVgi#! zhcTJgK+75(`&-G=8n+Mbm}fT=CI`#d6-mHU%gN>pHxfy(#kzyM$X(_)O4H-Gi5Lm& zcXQ04%36IPPGYvbHahkqqqr=Ase4)))l0~Q`*VFk!(a@|w>1j;d#oke1Q{Y({lFAw zyVNGVWMHS?x(gY=ByqE6>qOI(JC2RbA~()Z)b1<{4N2epT;`YgyA7{2zIEEQI(rmH zU2HPwMP)5xE4(dhH*E_-*G`i|{ZPEEvU~J(nnqcU7JZ#>t{|?g^m)}Y`7?05ZLE1% z&SeYg%p|%}i?1u4|{Q*!fu$6?R^K^TO?u7JtRT)>5T%Bu>7 zf>InWG63`(W~fL4w3rinMg^@P@U)bs-+v0*3b!v|p<@;)9hA~Q@4KvfNsJZT5x25rBg1Weg4QabBr3XqxcNau7W59(dfo$WR(?ffS=?+DMUr7nSM;Bek%` zgG+Qp`%op_4>cEvjIEti_U#F@RF#egmJ+;Huvn*M9a*5;ZlP%F(@7^mVVBzE)_jC5 zh{#&C6gD%^J6Z89!km12x_n=Fe{dr?DTdB+J{)?zqUr3w)NM;9>eF0rNq1Znb7cBY;w?y9;_#`>rplz`oTk)6xJ+5ME<5*{V zwE?a<68d+MS4Ke)B_c)r_m>5vu!4*uBeq)flRsM-_>qtEOdX1K6On<>8*Y4>Ky-X+ zzr6zc-DmB?!>@QLrlom89`6y?Q;H80jL#~i&`b?ntOKONi=XA7&+tkxT;Prfqc zgh2ZHNfrRTm=Il{VU{Zpr(H@kJSCS%^GI_#&+D98Lx=bYCK4{2Pb(?19*M!Y17q@d z8<5A^(pU0qolnUctsO7#)k1RMU-sY%P!unu>%ITAyWgYxWfOKBE2g^R1=qXpT~Uie zF{%7vJO3EZYwLDFF!wKp*?p1u2_UmuzT*MB`&Lc6$Q3>3az|7BZmV($!Ch9AsP1b2 zI?+zb0H*IxOSYqr5@{8IkV03bh~+{;8u~-M$z4e03iSmwuwj>j1qZbUr8X)UM0}m| znC)+#OxNBkzeC7VyQtj~VehJDGtQ@;){d=aSX14EP5?iUp>FUr&;`P!`2E3EB~)|F z>ymNVQ2;LiZ2c_Kd+m=o_5tQ3Cj8ByVpDT*z9@YmIjPJHYcm!OrL>Fl{H-VOw@mhj zXD;ySBt=UdDk{RL7vQ~5QTNktzP0#?-)P=!UR8>jp2WoZ>)?-;gQ?rnK_x0C|8My~ zPG8Ia1z5ghPB1{--Q+E6giD_(>NHxq6Xm4I?+4-PEu+op;m6(*NoEsJ)Od5(T-ahK zN##^I$1~(9gd?BuACI3>z6S&P5HCu`aruiSy0VFjk_IA! zq-%oN4O~Y(+Y0uz%psu+b2btKiID61^28HXo(wpI>pOgm7|8Y|yRAYpetjrtxG9`8 zvyR+7`-B`awhacX6hZaBy+ZnJFBpY9T}HAgF#8J)t?0|E7*^cp0HM+DO|aGL!ZnvOC1rmj z3!Gr2+rFRwf|c`^0h|E_$XHJ*2l<44({~L%3NChAd+f;{6`xUA0t~vU%8ZmrQ-glR z@o(D-I8w~9OHP?}Pj@6)gAbhL>34N$1gcemxL5SFGDZ?e^b4MFiEGm4cH4n`J82a4 z7Y6T9Rzx|wkZ6#PWJ zqN91R;$@q2%ZbZt?gpD>n{Lem;>Wms`%oPA`%0D6(%Eu91~d30Yd-`|kDHwVPdb)Q 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

#`Weg` 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 Lm1Yrr1S39K$GZuaw$E5QgQ8i9M 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

Ca&`{0@jROtQXc^m)(QVNcRY-|jY0EEBAmdHf+f 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�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 z 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ùie z$SFP( zUUnL57Cop9v1re!`f^Wg5kW|GtcK^fYHxoCvm6;2Q?yy=spr4t=hyzy&i#LvKcfkF z=Z(r}<%vf0;|dXFg9um(;>E^6xm31CDd+a}oerFY*lN%xTw1%T z+FZFq<-vYL&u6GEValx>=($S9WfG;IJ$d`~Mt{hHdy(Kru0f76L}B+{SYlqzGxUix zm&s@zQoV*qVT(5?O%N^cvk4I6Tl-G@ZC6Lqw|&O3q~9e0q(_odix(MD%Xv9_>8NBB z&{c54NM@E9SA>|b&){v~W{EE9%|I&M@iz-7{x5woNPdy|6$5|xekHZ6Q$L|7Gjsy7 zma+YgrSbjp4u*CRkhIE<@(x)3RsXLq^*jLea&#u2fX54TC>jEWj!GbOA65iyPt7?H zy(1jf2L91HO5gj5))u?@NJ!@N!Nya)*LsPIJ#FvF6fXvRT+Wm6q`KX8p)tx@kQ{S0 zYyYmHE>$I=*<7P_m zjN1Ojsb^g!S#ERf@6~D8L1Xe4M?jHRcYC43Gw*s|%=qS zVB&S0+jqkEDxzDa+C_JM&aDXc3SAjJ)m+TW2p&iqA-oEfI@BJ4NfT)bdJ9+H1R!-> z#_`9h-j9XRQ`xxJUhyE_ZFDSeKC`6mAe*te?7(lJsHhfru)|}q0 z?je0Yw`kW}!*elKA8M7%kh|N>^p`*%#{QD%NH@&3bE~UG)10Q&>~S%jI$ZXO;2{$T z;(`0*NOL@6)kMR}e+RuGraZuZ-b5c1#1}+uw%fHwVRW5s*i)>Yd;snuYmzIT6xsr7 zQV7-gJ(@jVHo2yZEJz=FJ~%vY%QVxoV{$KU`e9s>26zY%{YPB3Ph9aife7>0eZIUR z-8sVz4ke$(mbF1B$2cZ|F!-J(OFS;wNiDm4o+%PqJt&u>AKlahOlP1)KnCU9j+d!g z>klLDbIz2X=kZ~LV3l*`q@g%FKpxoxtBL<@6K~TmB76WMMoS4PfNz!za=oPaz26@X zQ!r1)=;XA!E&F2j#%_SG`BT58B8*u>kMGKvwcdjmSa(r)ol4Rc`h>hBABB<{(xwM1 z_+Z;mxk2d2scXumW2W`$SKE{6;LuKb5LxJl>VHt!ZB`#a22#)KDi*#8GfWa5L&oC) zwJpQh#_gShJQ#57eb97>1PsP0ze$yl@J{G+@ASQf0xbbw$ekgAl9Fa5*UkX@Z&g#L zfZL!HOELvTGkcYIZ(dh}tLo|{!`*pv^r^OG*e5Gs^}DS;z37KfVR)T=rZ1lMm(i|A z%A@zrp8~mQ?j`+_nq6kV*m{Evhs1;V=NNy!VDJNAMA&Yy06H_y&l9vIP_BgFrYQVX zA{?1G^u3WOAa_{)O00U}Zr%bllqO)_!r(##k#SDur25~_)}NFyX2~xFwz<%j2$bS{ z>oc9Yf8t`CRxl>I&Mpp^242X|S6q@liAc)jXX|HquOa@4s>|0k);KXltBcF#BwW!* zK_cEOWBzNgiqqV1wrlXH9G#-{2EI!@rj_%kh$^bI9q$ZWs7&5p-B#~D8^-25>ow2x zZEVxr6!&nJ?7E?tD5`=tPd8;MttC^p7!tXvJdX>5B`F7=ZhYWPif+ukSVl5FG9x?; z#6!G?;jW9m@7qsRwxfC&;^v#pnt$;IK-OP41OPZ{A~q`lJ+*-MrR(>#YeA%m4JNyL z#tiqb8EqRgdS$A(Oq46IULfQ{^^j3-`8ia74ZHMKym4 z_3V$oc|dq;e?($G+L-kGj_(22%1My@c+d+3Y1>Ua$(BvE{-(0&(+Zufp~K*zQan=K zoRMrMFv^WjL4BE8LNT;b=Af#x*F9aP>K@f?)ZDotL zXT~Zu-DQbxe9wLHAJMe!W)P-?P-{6s*1x0fkNL#i&^im4%ofD1P^&GiAR&I_2PQ4d z^P5UQWc+njdFDqgEjty$K_8i}Pl-_12M1!}P( zXLL%dScjb)TP7a^Y~MnZn2KjFZ<8Jc+FtM~JYm?0GC@8l^&*gMJ3^ZCCyS<~G?qwJ zK||h8(1@Vq$bqN7`Iw%}EgC8%mB*E^rq1kb`Gv+(3s>ozojf01sX^gn=a~ zfQpaf%RoY^Q^f$Uh1@(xE^!jOsm#8+qijBT{BsevFC@YMBzgSv1pp)vS86v$LryUo z{MG^Lldjn2qBudGSKnXrWNWMT32T@1@!|YEONJ?}$c{?Dn^SO;%G_BNw3->lg zdEfn`UzcMf&x<}xc#HT1K!eDPB)z6OF}I*O2Vc_Gp+!B)KJpwZ9khZ~C472~sjL|t zVQ$Ph4{M_Xum@bwaB4${XeKzw%SuY-0#^-d#9Yg4B1XlQ0_Gq@v{2}otI0Vy-jcW4 z<|E$oxQ2vnfa-YQ!e_80(9(rEt{+lXX$tC)AF<{X^h`B5wCmjgO|W8nf0Yk3J$ zBYp5Tm6bFjiEI8)cA|W!uV+MA>_nnw2E~|h{d@5a-xv7HmUor0AdcrDVKzLJ-0r#o6Bx9^Ei!CoyQ=e(?he;CA3@id$5kd36+m(9yoj7g<1w_rci6lN*YrPG=1dl zmx>8_Z_Xo~4x$e1%13U2Z@aVKBJ3`&r~3=SK^-|ZB(xP%Gwh3~@{-i-0}1LeLI{NP zJZz+$Kb;);5>!R{ZlTkOx*Q@-nITBtFHzBEfn$Ygp&VDu@3<;rp z7EVl%c*?a#M`lVCbXhf)5;2*LC-^iwPclJ(n*7oJFICw)@C%6;010a;ksBb$4mhKS zd&u0T(?=N?6Gg0cv2-zjcr1)4e@sqgAOK&wBehuyJa0&`4s-3HBOB*k+pxBnZb+=0 zY#LN2+aJ z9NI=8TjKJd-e6@<^0Jh0@ z9#J5Q^Z@lk$SpCw^l{{qs#ek5SF|qIS>9%b{y+{Q@DoB7t^s}+OY?%a8M5`_sLo}u zGoREfU&A~mCE;ZxFLpj}dYvaIJC8)wau-rXxL{WI(4Z&Ay zt}Yb1aH9!+yQTSnp>DQ!6r@4+Er?R2lRdE}j$eGo4^-%27|rQEfDRDiRdWx+I|)4{ zWFd=!S%E+tMi1B@qs?13U|l-#@!chFHN_muU`{2LC$5cPn+bKbkYE!&b>^Foe4GCb z=oBcL=VM)*MC~H_;^egpd7OfDxXG2npng8X&DSgTJi5l67$7F0>EQ^5lRwp zze`s<%Bi9OgPCzh>t6|_1eVGw9UZ161<@V_W%}vBEIU2A9KjP2qHlJ=arWq2 zM7}u=p}=$vycg1W!TZmoqsQX5322yj^Uuqhj8&oXRx*>smeFjYO*JacWt*RyN(a~0 zp^{F%S4&gI_Wxml?yJ#1-Fap``@<*&a`| zy#j4mzjuV$A;(60_Tp3uggwdybjcizORXW|1S_#ivs8mu8?u-^&L}ygE4Ebxp00<1 zkc!&B9P^d2oP|m~+=%8)J#YEl-G$t$!{}df7>i2&JBuWIk&pzC7|vs-0vz8#pr_?Y z?5WJMBUHpJv$*L%E#D+#)AM^FXtdA3bOaNiV6~$xT%E{E7kW%y#z_8nI>skyw2#aJ zhsI8_E*`^<9bvbr))c@{exKdHza`UXj(_f9_Lt8>?A>eqq*8mvjQbH+c`zMPcp;%t z`EC{+Hzj;IM*v@z+R?I*2zG#uM&(Mfnc07Mw7W^Okp=#mWF}ufapS)fYdwHm)|biu zWWyM-3z^-nwI|tF^jWWYW+!$%kR=CT!NEa?IZB_v=OZ74 zVTUa_;csy^>*g4jiRBZ?XKKi(UjVe`p>wu^pOM;5V1UAZVT-KH4ayaTNO^P)MYoc8 z8%Mm-ehpcP_@?XD4>IU3_}oa>LUPQ>P(DwG8?(nMN>V}(ap>5AaU{i4o1xp` z2Ra9`j(G5=gj!E)BC3#qJ?II~>h*97VY%NK#nM!!)$E{sxZZ z_3qOAxNe6I(zSsY;L(X4!h*oVJIEmW$8ab1y`pxgOOGG9MR_O28U5K5@F!5cU=noM zqPd8sGO>CNOAxrmKy`V!wH%VhG?#XR_OPU*4eH-5Ggr=MPu}(8PX0I#Z%}sfL~z`a zPOp?fyiJph)R-osJbsM*lyhzUGx!>{nP2NWPAqIB)&KF&KC-@O$N*?)hiRt#kH%=C zWW_NU^UNF<9+FE34BVkk!Jbd^Pum5R*O^hzn#=rAawf9hao&R?Y&O%RsQT4u{v=HP zyNW*I_O9{{x)W{3&0CHi%_QaEmqDzbrJwwlrgkN-S!npuvV0vln`rY%8KmP!4!la$`4S@5Oy$w+{%LX};|!o_}_NoSUj4Z}k|+#bxc$yMBq zr3L{NM{wJg>5C&%{1{njy@9$yF;H7mcWiU0cM0!6z>ddMC>y^wZ?e)Oth_9t!uXW1 zm}pKQQm*@(5vp$J&wK{eHb{(io7~Q0YN{s!FhIif<4Osw3~)cGvgx4`hLzbLtYB$k_8fC zhm7DQY3Ob8m=Nhv0@qi<#dCJ!s$QQYeTRsTMcN-aY9hbc#;RK$j$SITKAU z0TLB19e5sb1tVldhw7^*mXYcyETji|s(y!bP~uLyszP&A@nedq%fw1M&@fx1Rk5ny z!|(3W>1$~l5qwNou;^{z57tDcDPpb!aWw|F`Mi*W2vkKl3b$z9Ug1Gbk3Yx;2GzBb zp6ty_rn?5_{wS0EGQ7dW8V(Zf-PG^PSHI_WJKbzi4S4PK{BO_2J8_Sd17{ z%}TrtL;UwV+TGj-@)Sm$M`0Z#!oT^T-oWgk5I3~sxcS{}$fg*a>Mw@e(xz&$lkpoa zc-1w`4&6|apYyNNtsa@<|9S*>5J?Y@ysFNaUp45Lbe5XiJ27<&PseN7mzni>5YC1A z1)tTF2@9#&GOU44WoyQ~E(_I7?+HI}dmeNoRL}04K9%d+HToGRj*c|Vg@Ke*lyE5< z8K*Gg@UeDydHKUaMf8bUhTcz2&EAK{(@nROF5V*?6c6p(YIqOs4-Wo@5yyX2fXcq8 z$OEY0l}A+r5>A$F$YW^G2HX`8wD~Q;Si@8N@+Sn|N}YEXR@WN2K64pvWFbB2)RjiN>g7NU2H8Tl(7G zJTsc(duq1`@04tt3Yk4-M3jxUS~&AvO{g;OD~|oBu#stn(YI&A?y|G zh3c8gA*Z`kjUJzR%l51r&;?WJR=4^e1V{|&ex$#j(=_GV0YlkgCY-2J;uZC=42nri z77t7se0fzzyhG(3=Z-iJ(wX>_O{vo=ZN$jO1V0XQ=w8T+$$X%Z_cm);Ey7E!j#j$A z{l6HC$}cPm04(4|ee{64XK?L$#~#ksOBwqIs3hku3y;R%3F zqx=%A5kJYyZPH=Y`U)mP=Q~V^pVM`-HjgI-m9__8HIX++{Ri|tEH~Ow=p9<75xA%J z9qS4(;Tv*Zy}}IXp1zR>GU8Z-uZw|2eKvCTF1_tHwFv6g(>fo63c&|An(ZGWXq8sg z(Lpw<>9Zn(=}7co$%X{=8I~TvT+5p3)a7;j!r=k)z-Nc~6VJAPzZi|(Q6p@p+H<4fM zRH@|!3$%wWs9wkSRQJ%*L-$ajPqUmbomg=F+#u49(XWVk3J57%8UJwi5fa?6#a(6$zQYKv> zOhdESJBMZ|^K%(2W}C}@lex9;@Q}0!X_B5UTAVQb`~8?5AI$vqvrS7q`wH6${lU=S ztSxc=7ai%H$^1EjT-{%2(vrypP1rUHsz%wmfp$`FB{&6E6k497I3ynH5aB>oKZGg; z$z+?xrvUp+h{x3H+hJpR1w@O+J#a{H;9Hq<@&~c#2^hg1+T}YkXq(LwV4$2v~{_vXif1nH0 z9dyb}wGKZb%~2?uv0>uxom?Fnjlh}Hcnz4t37EqasD6$i>L8H3cpos$xpR__jUb*8Y+If~1n}%~HW8+f(70#GeYi(;<=Rif=~4*~h|^ zI)*8u-2NYqu8uDpDgYe%JSa#1+ZzHJKUXmqWGJoUM556DfI%V0uzLZCAwaM#TVuYM z$fX2$F>tqyU?8^2S~t>s%-d=7PqFq4{hH5jd=0TbRX9bKE%8$Bz> zNOJ|0SF*r8L~S%v1jEFkFoWlI&+t^aa{&6bHMLaLQWTbcC4!Fc(TK8pYoWIRlDr%Pe{R( zp2u9&=Hzxcym)Y2B<*;@iD{EMwqjsNfhkQ^xa^CVnObFU)qx~tzVFf+gKJZ_$VX0^ zOxfd<->gK%9lF=B9cuEAt?-KeiqM%L1{w4jSeDMC;}ze0OH?v3KEuq$8t0KuVSbS6 zB3TR5ZWjfI0$b5` znMt??%V0=}KVc8@b2S31saRLoit`d_Ow6)HV~(kzR3q}h&E(pDiQY@$1?y*{)W166 zANXRS24JB#wmR~^I(b`>7W4WX?#ru4`Px`H=Z|1SL17FCnY487Dc7P(r&szbk3Q#w(?u&&VN)ICu1Qev!}$e(=9HKb9s#p%ueCwhooF_vez*SDERpq--EE%&PW zq08I}WgjzqyUezMQZ59;eRwRm8c)w3 zPzT^pG)Ch4U$%exK8Onm3l6%K0$>MRuXX13pt=b~{ z+21r23mk`T2uq_ohJ#tW5#kqyR{CC>0TY;!Y(2Mb&7924xI$3Yr{1}_0d>mIX83hw z*73h+Xg{wB&lZIvt#G}i`uFH^CtQXoS`_!id$`bp(o;qqAHse*VOA?J-1ZpyaO*ik z5sbHJB{6)uGJ}}vHn5tgf?!TLb-H_|5}uiIt5N1bnN)m8;mGxKwQ}+&SBD)@tY#Rb z%!`w(`nGqU^yoiC5z!&qA>`1*lB{y(jTQ~Sm%UR{lotXOYlkvAXccyEQzZX0j`1%b z8UP?w&)RnX`>Ab=QuMRHv$J&P#!dul2i$olIPf*(Yyi|;0m5nno9XMR9 z6KAyfl+$Sy=~x&c0+w!0$z@6hb%dcO1GDYT{&o|-WF2RgbAE=cJ9FW@RG%dk&&3MD zI$S=VbhAjg%+~#Lo$<0=g+S{UjMcsD$e{YCbjZkBmfEpTxmmN#pWW$9I@K=S`oabCkxtwTrn_jTs_1E=yhx4WixPUK}rhcwY0J zV^eo`61h{)<}bCA{2+oI+Pvqr?>F~E7LbEVe_>^!Mv{u4r89YTt>OI+;L}E@%hlZM zLR4nMwjKZrbUr||ZTo=!SSi_c11XP!7RpiRlrQMN@)gC6+MZKArnd07T|z{C9SGjM z+>I}Lu_m-~f3BUMg`RNq3GvfV2-s{~LUU*qVq1-=D5mqw&17+>){K6WYs+!@lXH_t zkA9xbEa|3{rwL6`279w#_#J();LTsX%|ILONJ!hT+4x871|!ZY1>5w@ zeY5U5y-#6yi;c;uL5)BLXG-8x=T_cdQ`W!6&2w!9+jBFmGc~3Iuef)nH)CWn{-~{k z=Tjo(S9=_f^m2vQq1ca(*{RR8{xtlEpu<=Xf89A5L8oPyLy))t`Wb%!coeOF#sm%7Oq%%>{M6WiEwBShbzb>im~ zlz+as{6#?46z#QwM#{ z>GT)t-APK==kyx7An>R?u1fm>UIR*hcQ=y+N%QX<`I-@Xrd@z8L5L7#x<6 zAOdcOA)kPOuf`7hgo`D*F=G$#I-Y8;7gDz5y z4}Ju(P|3vfhAK#Ek{U5rd}RAGhl}q^RF(sETop@Ii@5H!XAz9gYJx)W`$B4|A^c)3 z(f*u+sG7Xn+ckUB?}7A4c}w0nuDdD7FG_LhzIx&_Vv|5Ksz*ej&|KSK6hnQ0gJn|H z*p?_Wt7Vd*5dU|>6J;*t%!!Ep?su_5Kx+Zq=`zw&*FO^8TVEt}0VGJyFChLmV9n4| z1YF&@`8j9C}y_jk-}1eA}Ue9bH6t;*tYWNmc) z>h<7*$f=d-kWeHWX3b;M?Bpph;I|jYq8X zFm3)1`X1OseO96EL7_uO_)f#Ifr*CKxMOM|N(J8F^tr2k=kJ@TuB@1!KkP7B+B0`? z6(sf3URr%MU1(vX!kIHVRzn?vnn3NG-U-^GA#&A&V+8{_<0_9nvLyey9=qe% z0{)`Wy`wV&r_QZKdF7Wl{8m-Bh7DBzv5M189_z5s3-6wc73PC@ zQG03(QAn6oFyJM&*TzPT6D3UXAUc7uQ^cxvM?9&7|Thd03e^2o-4YAq>H?S@= z>>RcUa-O*p**=lpCU``8fPCinGhn@(*^pTk@DXmt;9 zjADOtFLW()L0f!chuu)rwa}X%dw>=>X=y1(G@=V`t;i z;!>m9j>-C{1f>LP+2AKuV~@+i#MwB(f<#KOifK&2Yc%r(MDJJ()vun;&g!avW_a|q z=Dz@IF7AuX1$YgKrSy?HT9JEyWJe00u?kwsROTzKo_o7U-TX7OUX!z-_?^MS;O4ih zdZsmt8?79b>J^1r@^CH$lp4hGj;KfyXJ%YwjpJGC?v|*CH<&{~%W$johvYE zQl#(yYt{2Z5=3x>wS!~$dwL5(2GDgk9R4fl(X`cb3@GfQ5~mh7wmj;bN;v+Hi57d- z<){xw$x)4KWdDmO%QfL;3CD_e@eDM_pXkYF0(jG72sv|OT|7gbjtj&82@ zV#)mZ#tCcWsfAfamZ?<~@w9_{51|bM2RnY|vfev(lX$BunTQS)1uxlnCAW%^wM~T9 zh@(T2?ldK1Yvmi`X_67TE9iau@D%Njn9QZPW#Aqnx-c7LWw!y4U)e(xcVI~sFvl@-)-=0} zeyP5};(x-uFTaEv0EEAY-R=R}yr9Cd*-hC;w2kS%frZXN(N3M{@BG*YHq^g&3Y<1y zCidV!foKJ;h2>sfn4x}vE8t8)ndQwtAU$~(KA(qY)SlSLo(17gfcl1Nb3Pmr$?S^1 zdWwz4_f8XQSk^s;LklZUj6)-hsaYOi{ell3PVWkh*IK`Htr@V!#!~C{ns()5@`=#Fypg*l= z4CjAsZi?YK?He8uV-_349$AJMmWjA;B-MkfIB+P%_WGKRK)x?qQLwEs?>*qN&vT_J z^LKiWu6je}BxL(lTjr2;khH-5lS3J3WU_uXECQIjHcr+GzlDa<)>Hddi=W{sGBb|v z4$8~lKLzR})NTcq$u)ofP>=h^N@xNoRJ zO3;W1K7xsZt$17U?RJ)nJ;Z={Th#xx`2jtC={ExCXC9161Y8>|++-fasHLQK7y_l zN<>0_NhzJI-^AdM((p^aJRn4WqM7{LvzMbzd!yu`GlH=zMT{`XHhEL2mOV5RIStF4 zr|vD2&4^N`$}}oDo!vH!{=tazw54)5F8nB5o~llVJ{;Cc$q3CgWQS6_VG3+lK(Hx` zc8B%{4s*5NhH`Mp3vJq6i3^8MeL;7G3I zD{-SD+q5FcNli4WWA4{KXfON!OyK2-z*uLrH0l_QZQE$j*tYF7wr$&JY}>Zo*lyC;#@**TJvaLwJkP8(d%X)Y(vxY0~KppX}o{L8dCP2aU--&bj;h`ljqByorbHavB zz$y^8&KQTtE6^)3$M?}ZxT1wSCPZUOVXXZxKa9}>eKU4F%Z(J}$JQ|!mG9FWwZD23 z#maTh-Seid;0x3i6gQKu%4GnyG646*uKyf#TzrT&V{vySbxYdb$JWW6l0c7iY>M60 z-M7#o7K$o&L#AE=jCkar^%5+h=r~c!F_M-^D>G7+(ulo9ggQtG`nGGDgv05X+H@=O z7wbiWjg+h?BsqRZ8C#z!>D5a&*OFq_+0$ti0s`fRba9y@9<pQydRc}ex_{~ZlL5ML_H0V>dvmzw~oVO3d( zGza%skPOnfAWRs3@jD7L+wY7pXq;=cN9~8C{kpdbQXg!VgR*&n852hTKywBn*0bo?W#q5ypd~9SRLK#ElC-BhA$wXmu)iCH%XoI#c5BCYE=emNaRn_8>@G)FtbYM2-@R@Pp z5WTYnlsYeWPpCuEBW$x9>Lnu+ERAUc&#ja=`j;A@zg$=VT;Qj#zx`jGCi=>W4?VCP zel8{gj-`XrRQY5b%q)kdMX*Jt=v#N+hXrBFO!>t~AWNce@%nm7q^^&CPvD?z$Bhy@ zBN8T~&GFF}OPm~yMx_MpD<-8WU}+Rv$oyV_dc@xe^L3zs*#zc9W#MCnkGqwBpOycp z98_MpCc_#hwbo1VL-3Imz(+oQglzdV_=e^VCZO@m=gW`s0T!ZCYsEZLi)3mY)@Lxd z0s0j4fJn8RpdrqEZW+q<5qo$>rGI$_+ZXYRUi$C8;^m#Lxj@5OJWBd;%&k;|8pE`+ zBC{-d;RJ|OlIp^5VgXId1br;8KvO=C359Y4xbRzVtWYDhzl|HF!=^LgJ5V2c=6Oz2 z83Yf{nb~F!Re9_;^J+SFdo>??3~h)&t8k`att)VT%Oj`X zLALp5YV_uOkU#6;**uDwsDFz_7R4NM!9OA8#C_%|?-Lm)3xBA!N9T?U99e2ia4E>g zEY<9KKmB~H{FjCJk-lPJ1&9HML2WKzFMnw*Pa9OJnc<@u?>zMB^(70slQGpG4g-_b z;ZS$7H)cYSmD@+ulKgStk?k^|VoA1LVhW6(Nmmmghdac93aTV& zLqbQep5q%ChPtFgr2VOaT5+7UOOD$uWTln+IfVRnZ30m>|9UJ2S2 z3=?!YC2D&2zod-zrNbJa!%K7S5cuyKBe|p-Xd8k5wVx!)Ske%(3x)DZlVPN4sW`E3 zx?8dIrW*}bn_bFy4~`w!9=Z`n+Kx#KuE!rZK;@b1=_GFG@$cPJ?noaF-m-frzeEXU z{6#n9H_A?o{wxt08~#-!2AxG=;fDq3$c-5ry;*@ti1evDJtS!$?lDt3IdW}U?|~4A zZ9sC+D&Np_z<8%;&~0X|uzs(?znsjJ=ie6mDZT-(#@ApDE5~|*Tn7x@ZThJUttJhV zv3KnQYl46y^f%_#T8FK%!PkdCJgU2H-4$AN0-D@u&Yg`byv}Tluv$>jLXubL_+K{0 z{E}b;kkBR{)B}*Pc+6nC+|#WJFIb@_w;AOt*20is%^ATDKBa&ji_}-wB(~eNBx=IB z3$sWHGu;2Y?W`9<_edDB>)9PbLcbl8| z>!%Ht=;)zww0jaO>XWx|#oojyw>O9-a^n%QE5@2eBimz20g9}PN+}EZ*Pa^A1M?SA zsbN!7gE5crM?pbkgh|sXj@mh#<*0DCl8+a!x4AwJ@GV>!bB@K!x!CN{C}MB4E}uVc z(Trl#O+WXP5pyyfRKOubvxK(_u1E2TY+?P$e+L6U;g<(HfCpCS#t)FsrZ;ab4Siv; z_=Elap;=)14t{qAZjasHvIt>aXcO>?E|$5Oj2h;%%WGTCBTbh4lE@KNR!la%>UbLp zx;|ihddTYpbOQC;{f|K49pa} z!YJm@gnMRVh8ZYoiy;?&O1KP)l~eh>2Z#U0qxQzF0HW#rX&qlh+k;0KGG*l1)W4!45a%gDhbvx!GC!iDChXrrv%w_oC~K=`&Cyq31%i8aUPP z8a_cx!iGMpACApVB>wMZD(P21?EwJ=Ib+@ce1%ZJsi!OiI37!xHSu8wTF#l0ft1_6 zC6#llYWp7j@~%WK*8sJGwZG%-LLfd z!yg=JJif?v79IK+UMf`FaoC{D$}DTq(853X{1Y8%6{~lDhn)rGN0<$Z;Ctt##e)5a zE|a81hbz*cBpoZ_>KFm>^?ukY^H5Mc$(BW8sr=t65iKNv5CU%yf6)u^DBP+>F`W3g ze;;DK6kiG)016hKWsgBVsov-(2(W^@8h^<9%g+bluo{g1%-kvdk$$z+qI`0UA=!{_dnY`=C;tqNznKKee!;d6>f2?Wi2`TGk{d6|pC+TGzF_dh zDvyg53_mA~0AqbMeRyL6jrjUm<%cYI8Ucu)RVXzN;Ta|O(aggdCr~0Sw&>PHcM(ft%@R2m+`H_ z?ED)?el%Ya9RU)H@j?56|Gcrv>X@MK*}CEiL;U4}Of2Vqgl!~2#-m5NITPxw_^3m& z4JTc7q-0XZV*hbpPj54cL+B({V;V<=cJ(Gyr z3IpGFMRavACJibSO{CBoyl29_I%jUeh)H9x2fM|%kM2=boouLPuMC{6+o%)nGqiws zF}QvHAfk(-aQRANP3N7>UHn@neg7hN29R&Bsh0uD7+v^TOlsjPB3L*!LZi!!93=|Z zUU@%fR&6=+3!%KV-=s}^!r63vuih@{;PJu%F}kHsFd_XVN3QseDZKN=b|NLHV z=1B4u#wc+2@tGe?5CPShbkrSxm40tC=$#^(bQzM7S1Fy`NZ3ZeUNKM`n6&49o7B-L z9c*M)yX+@t2rMIN!1^`=+KDpWZlt=ut5Hr6lN7`N_3JU4LOdkXH8IrbmY?m)YJGY3-Do!u;cpmZx@PI^ps_ZpZa8sH07IC; zU#dV;R$aZ%R#X+Rw?LUr#z-qXW*Nw7>^KeFBmsfviM1|AN%X(Sx23fjfj5gGY@{gK z%Cn$31rFiy*|a&D) z14B^_xz#kz?|VrvEe&C+EUvkE1bt&!-p7B_GcUp$&M<*X(=V=i->HMGNs@ z)fCXW(!*e04S6zoM(QJ$kU!Snd`tth&F{|Fq7G?6Ir4hzuy2AB)P-vD{ne|EYB2I& zRuK6z;Q=t=h}+KqcoBDg9?kTPtrTZ3`F&j6X=MU2DdhYEvv%~hHLlhNp~+=MA$tV0 zk0$2Le++)GuY(3O?Ul)so(ryX0F*d}QxB&5VLL|#@AiB)F|Y8-we1tnYDG@+#~;o& z;au9B1HBY@Q_$+(Ny8i_!hK|taJW~FtmXDB{2$SA`;6{(lv?J80$1Uh+=0&rTdOz< zanv_4)|yicwBcZsja+?m+a+_%S$!EF)cA@d;2aubIFw*$E?!|7dTLw4;n}qIlq)rfDd*p&gSCS7;Oee%KM%$b*J|SUoZgQwX4|6!XZmd~^kYpI(BqjdW zho8ik4^M!PtJp-n|8(N;;|bgc=I0X$^hsvlkg|yQJ8%EQMHJH|w6`2(4H3`o1S;*P$mBNUXh8<*u6`unetc zq)&0#t`Zl#a$K4tKhz&nDxq5IGUNUGE}WJz23+hUQe|c)&Xbwdap@*?Hxw-Knd!j^ zlTpRc9NsFJHlV5i+dYPwa;dgKOv2<9>snS25S~fx+T*Tmf^$9@6|WxR9@W1~c1;R( zQ=)dM?qP?g%@N=4KlrUePRXD#tK*K6?u9wM6`m-t8aUzGOYe4?wJEu#WNHg`pN8dL zN9VsorVumvNZO5rCPNIYOzL%dBJZ;Y*^uh!{6M;iXOG2NECSwaoEIX>y;E6LchPCd z+EN&J5$dY`TkfQo@&j0wd+o5qMRF~Jwh>SVQK!d~K(`>tR8vTu1<6$6EOdCM^;{Jq z@rSfui`0(mqzt8AoF62-s06k7e}m31^A&V2K+rMkpU(k@JCIVLn|z)p+oNxfS8SGZ zO>mj#I0b%ty7@p0KSsNoZ$|=atTL0?4A?7RPQ{y>hZuk*-LIiq_RP`UDEX$|w*#_o z3$0HLehMTMazFY3?(K#`!xG(*PSk?nIv&|Uy>1WS1gEE8Y;#_WgBMZ0+cL}Nn&%eM zvTyaJEBNeVL##FGuEx0RePG!(XU#xgE6?>|)e00Hc#t<_l#LM?&Z;Fv6@u0C;pvET zOO}|eGT8Z2W#>Y$Mwh0XNL@7QC=9?m3kaG#iv7~%Q+S?l@HzmIKdtzxl^amSk3)i7%12>(A zVtsyI<@1SQ9Rt=niw+TO{RIQJ0jqA(%9f1uRmf_rj$6TRWg!NfzYHaKaOvLvmQ45R zBt-t`wCq{PtbZYdU>nV18Yv-Z%;eyhX;1J@9`IN zY(?{-h}v?6t~=|S62fg)HlS0L)dT36u>zdASs_pEOf^i~J-FE{}8+^Q~lRmN%m%81Q;LU0Fv> zghOGb@!Ce3&Q-;3Ce}rgS+d9k7W7CNV)PnRiPBfu`cL9GLTTXxMMOB~&CTRlSfhSI zRsz;lvT(BpfX^{9Iae9qg&BR`_LlUIERRe}>ryM!&l z_7xr_avtAz#3(>MvCU}1pP3nO4{4Md}C4>Y*`d}A0 z_K)6u_`g@3{MujmegJ%)cILMK6x=WRAo#T)nvqv}rWmC7NiKe4ll3iQ06RM2m$C)@ z45tpz)!;%+y3mz}UB!3eJ;a5TIbhYBQ- zG)E~#N;)CjK#ZYk^!+_ohxpT0C{s@e1JSUuveFs)O@jd_a99&{XpFQLE$sJS>t3J= zStShWyq;L{O1G(N_dwzM2JOD16PL17wTBz&HqI#RR)Oyy6aU*%(f@+?2f)8b2+IJ< zyg^h&KW%Pwi3KZRma*3wjr7T>4V~MjDd*Uzgw#tP?Am?q>OS)>Rb6L`g@T@)F#+RWUu`j$_TKFcZ{XEb>*$m+eF=NdSw z&-yY$HzwxS-mn`IcPI(mHcQtHh96xEhgj>2GE>>vn9Nlsi>hhijA+nb_?Wb*PhP~D9sQAj#Ugjb1g#fXeQYez8Dyig^Z zBd(>a@%2A+Ud-3I>cfEr__X46>K9qvS$j6N4#IS@>0VfW4Pubzk{S<;EDU(juilOC zI%xQHra?aWQFvY^MSB2~-W?F!fJZ;5`WHL{>(8ja-tQbTE;^9!Nhk(ZoI&G0v3MPh z3~)6%u7Hw?2csPj5LP6uX?x;KzoC!&?Vl>kT&*?$YpFp0_DX=(XM)c4bl><6`)|^l ze!&L<;0KwBJOG`S==(T(?!(VA7Et^-d!toNk$B3Pn+buK1lsdlr)CoT8Koh-5Nihp zuAPi|H!se#Y`G#ZR5|dmw>WjYhU1!W+Y^rA?44Sngp45QbmXn+6<856sM~dbHg3tI zFHjlu#7HV%p(S&NRwv8UeqWSqD^K08S9^eeH)sU!$E7&%j@0EJ&IxrdzSbPWth7@5 zk@{$~Y1@e+;6$2G2@E%8KDVD0IQ08T#huZhq5RkFilM2U0|wvvb-Ar;6OF22ZbieX z&h-nsMAn#+A@Dv)Utuz+6okDz8;0!oV1fRazX)qJSTcsyMrp)Y3PEYK%v2RGN$qe;U5>2fyCf1&Iq7MQ_e#z)n?=j-VkO<$P1f z@Goh4&#<(I8Znd+7CREoXjmR|fE3h&Ju>^5+16T@J(qkiXl#AwNl1F?!dY8Q3$`D` z61BNBU7crhcLVnaMlim{0)^CHdb!V*q20=Tb!0WO1QZn|u94jEVQ#!MX@A_7o-2KM zvYNae_7S|=eiuP!{>V-dp2=zYIn-%p;nh3D2%GNXs!y23d2)GM4T@ze;w@fxt*(G6 z@&V&*kmP{wNCTX)M!FK56zbKB-Hir?_{ytT*TV!ZTbjNlhx32$QfE-e3*l-$=cZNMMc6B72Ylc0hYhGgei_|$Kc+=p0 zkVAqe*0*oJQv0WYXY43HN6J9DT`vOTiku(FzMWSI?u^AHShCz7vfU3~xy96MiSq-q z^to2y} z+rpfQ>L*SJ6i>554L2@sZ38*kh%vKwl{XvvkMw1%zdo9`L3TKzGQny54(wP1L`7t% zjRPtc%=9G^{@lRLzp!B*OdCAp=@eoFY0Wy!h+VdG{jinxqx_d69KL*n0DL@dqpgE{ z4!yZ|d{=RL8)8rr)QL-LWzSaV77a&}fC2}0np$3mf~at`vqy$U z^zGiAu0=Wgqy0gAhp5_!as?vq3TCt)-eo~}?KTbQBVyZn*$9n@wzRWHhE|q^n8cAm zKz1lPxC2o{&EHO443a1o(N5|fje#dBbhS)ScV}&;@Dm>Ek5xn=8P(Odz?6e!>Y|OD z$8ufl^z_mU#Fv-b9-(iY7MpCq%`eKU`nx1R)*;V?hm(8~EfMKk4Nr<4Z{k#La{&?* zrF<}#d9%nc+Sg7&HY&q#2$e2(s!I>l39ol-;`GHmmlPS6xni`o2sZJ-B$QcM;TbJ# zN`=3?o4we<7j}55yk4Sbmy(nxvD`1txis(Kq4J~pMD@&}@fMOJ|77P=i`&?SPbp*K z4y)T%b|V(3KESlJU7}SK7zQ^})$Af3H$e`Nz^2aMH&r$BoNGiavD46oOyA!SyxPx1 zZ^+O5B`!S9>pwzBxhjh>aeDr1xL=j`EZ%fmJzYt8+?V1p3`vD@K0GM6%3JwOym-TT zgB~6cd{Lc$CorJ}ID2K3`wx;Ngr#bYH8d-6)95Ev;M^o{t8K==Jm~Tj;7~w-9qcAW z0gE5XE*721-RzY7fwme?PB)D;zx_#0xi&IqVQ5)W=@2V(jFpc^A1c*~JPs6i;T|hp z&&r<)HG{-MUgtiRxs%gO1}{%xxOc00hKN3<=h}n;5mwLrWXDnEv}sHH4PYJHu-e*& z`4Uh3IcHYeY6n~8s0OAiX-SRYmc|TIM{fqdLtEgRd=2hsCYIM@qd4wRTnhBJ2g=UV zRIw1IFB&|NRIORV@%rAPE=I!qDUKTgLoA{Enu5Hk9SBj3QD}dmq5AJ+o+RXo@?#vh z=m;qe!$=+fp+p7LbXMYa1ML$@fBKjIJihS50Qe`14phJ!Z%~>Kz7r}pyg#{SEqXl3 zkip|D=!M~ytcM537W~?YDkKL&)lnC`qP{mnrgr3lCsS#I3scm%(KwLXtvhD2Dg&QK z6_{#cwN+~t7st_hCQB6Sb4kp;$fY|rFK$FL0riUI`3dO;wcD*uHxD3F3!a29`DhbL zELMQd>Kzt(*J=MYkxy{l30dg3NN(=X4IQ>@i%re92d4VCi)S!^ImIy&9#P-Ix@;S4(mGwjUaOq72h zP8C*tB+hI)pR)9AF<{$Bzj00O)Qk>&t_XOvf? zy}#Ja7H&53n0Q6LV4>-Tu*0L`3hk%t97+bdiGyE`Wus(Pyo+dxC`kfODPOaF`f05j zdsj%KoQpg9>sP6y3De+0&#DS|dyrv=v$s1RsN__P3L1 zqKMxuqxo6?w!Hj;zqlg-+y=a9EB_hrS`;PuF}U4Exzth$Hew(U^f&^frf%*ZPyw$c zxFZ%qL2Q8<0u_7$c%Co7mQNf)TtwgKp!0BWe`bm@)9Ni+J~*ligvJ~CO4iS((iOU* z7;5+NPL`^1#Lb{*tfQrh^eEWCLo&~2A}Tvp@&@K1@o^}f5T6iB^NG%-G8i)b7m|u#XKCnP}4@S>4PUE`Y4U`r3EAX+sP(RBF;L(#W_^GA8>G;EZWGb z4Jc%g^T$6dYRxwbdQX$#{G}P}_#ow9kHh}hQ&sl0nW-|hrD225AMtWkW$*OK(K2dp zFml<0N(q+s$&2PzhLWwX7s)?gwu{R)0zHHH{CzTx)w|~X{e9kOBlXBkjQFqpz%vU+ zk?uF}$$l$q>p&zOZ=`b86CQqYx!d0d<%g#nF(9#6IFbhrzgZ)uFvpaaCtGfURnn{b zq1Px(8D1W;S69``1nUwbE*-!51wVgtH`6)tBN`$~hIagpqJ)Wb!80n8+Br9&kB2xr8|PzB2&rvS0Z?P#@olw9pGXVRgfeW zrGG;SyRjT}=34#CVh%Y&U{Rj_SxC>S9Y{ILJ3W%fJ0JsWI5R>){KmK^V1+Oem=Pj| zW3$^qk89&*6DJ_YpD&9}04zf4&-=qp>!b$*+8k@3$*De`(PUnTO#xvUe=#ZHGyr53 zc32~oQyKSRxS@XP`u9#6PN1^;i{I`MxgB<>471^{oFG3gIDM1*Qy zfs_I%M471M-cG&e147Z^6go&|(%%4Qt;6n*P)YtpC%kprlU{hsgVTN}g@Y~kIxAW? z*Yz`AJXoA*KRu!DzGk=uQfy}{8`l5DDw+24&`D~9C-CPXG1=gtFZ$W09*a!b*pKTJ z6IIQ>48UiA)(TOe90rkAcKabLKh{>3lW)O*j?h|5u!bijIMHXcsZIRGabJ4fv5@gC zP^pNJXm|~jXeWKK8lsVeuSWvU3*4yMxM;$X4e-$Q3T`sg79Cc;{S6lo07~;pu`f{C z!IREXT?|fMut})=FS%dR7kMmz{EuveHt^CLxvTx`a0CqI3A(BvI{C{KDeCXXJeS*+ zJ2Vfxo@C+os)HRUG|b6Cj~FZruUKm+hOyzX0c%bCok6hCxO)i(Ll%)%3hQAjrU?&nLTK^82p7l_mA{c_e@ z^<|YGx4l|wI{F=BtG(*`$*5d>;IIM`(I~H$!sRw*qCE^1|xQLSH1swt$?YG^0Zb4Iz@hc5)EDMVTK$y z*$u?G(3&K8KdaUE|8Ui&3pQLwje`lARjJ62zpKVdyXAcFvi@E)2A-e_iV78XfF65w zw-R!E@|^ET)isCvm;m+NmIqFgvJ)#ho9~&%#XJEu7HAE35amtRBsJ~3&D{%zKxHuf zez}WSVC2&2akhOTP7x?z1LLqUFctbtMEQyX&1LSt zX`T5+8xNqB@~uz>`BZ)LDr2w+?Vy|)4Q{60Fi^(twVaY7IJhJk34={^sje=4GXGT~ z+41X9)J)3{|Mk(L&T>2aoFwG)<#8XfydA9Lr8I`^qyd;B>098*rX!g%El=1`E#4s`o9ts3Qyd{i@+z(atW$!nv7Zi=lol3sSDb zQc253dDCz^RqQi3u{=!#lwd&Zq@ts^_4_e`__;mC$V{T#lZMG!xr$d(D7`0o(&@ZK z58%NL=~g6qt2}*_l>tC8wJ7<1J1cddX48pStunSCK1on3qRQO!OzB}OP4}~n#K%fGn*475GKJ(x9#%Jjl zA>gUcKXcN_uTe=GS8S@rF^Gq=R%2dp>@d$?>_1E?LFdRoQ(f9SdY{E8G(A#59>l!+ zlIEpZ^OVWWud=ErCy;+1AimKDoin|H6-?CQ5UNW`7yIRFNKZ^f`mwbZF?-Rtd9*NU~8xOv$;4Uz4x=ZK<~aHkmIDPXfH5 ze%^wbo#va>zcLhl1z)s@09pf(_z2+nH+m!)?>GLSAdeQ2wNN1NN+-s2&Yhw~*P+XK zp>E)XHV34@JsQsMigA<(eu@bOEcSOsql*KFQ{Te8IhN~Ke;n2tWEWM~>pUIyn5W*m zfCj9>t7*X-i~r8q9q|2un`bvZ9qvm>gc*?vMOfw$G4sru8&4_a;Sd(ND2}_YT~;}# zNnEO20OZpx4P+xU`B6NpDI4@zy0!R5q)W zDps4z)(k8KRZDRFoD}C^ZF%&mlCdoIimuC~Mtns^01MxV`q!kr_jZjyCx^xTm%ML# zou5QoBpkQiF?`{gz7ZYx6?N9(uVIBQevg902xA!xy(r#Glc^DzVY}-NF6t$olp)9N zyR&{D6h%#HKUpyE?MJIaf0KCiI3Dg}wk5*^UC9~{aeg^MVd+t3icDc$*69T>+K(yM zK?iZOC9+_Ljz)O48SFKcV>UsHGdDpU-hl#!BOwDH!EJ=k}u&tVcc4nFN)}b=y69myt;o$r0ZUbSiI8XP;2LC3_;!8$yq0_W#W73skuAe39 zSE_JU(n4mj+H&94;b`f|7v!HXFq*DmHuQP{**-}pZNzp|X8s9<5lcou+4yg6TZ8aPSql2N- z%>e7e%4!@5{J*%p)nB+N0NlC$hA6^(6V5b7+4_?+_L-}KoJX3?3V+ts1^#%t|vvU<8Kr|N}77YF86W(1B!qD zv@O+9EsLKox(uC5eF=_*lGmDe93wf|hAoP2tm4m6xOXZ1o~ICexS?^zQ8#w1{X?18 zma3Mn($3XEuy27vqxYGf)dx+Iggm)SjiZxO;{RG7)PE7D0*G_Ze7OO=FDVAjLE_x| zfky}Kzb(sb8c@SesvrY{cKTL0m}5aZYboO(4%BXXgm6*r!=4Sn8%_OY{{_~=k%ir4 z4x%(yESn@KVj{J;HICdQxze;pn1FbpX55?6w9k?g6kcM0EG&jRjK_nn>R}m!g{n(g zj`!oK6)E#!*qmf?cGM2eu1!O_d&8(qWc+?JPf9*)iE-VG%K=VA3Vg;iJV@(yB$qmo z^E!59kG($n<3K&s@V5{8$zr41XcAJYHBgRXZO@zOSh(d&ZCnz9GdV@6c`gfcu_;B~ zzm+b3^A~d(fEh~&UK-HflwDFsk(F~4P2Lg>E zb6R@@xf*~ut<1K=k4lDP3Fq7~P#%y=t5pcLYWmvC(h^eP1qFdRj5%}H(3gZ;KSpSj zEr5>3i}s-h49lg{%NAc1nt4&0wEv|+cW?aBxIh{sCh%svP>4k+@T}*wm0{t)ML`kH z>7LQa&BEStPOsYx>~QrS7W_eOSQ(Ir`@8wzo3S1$cbB~K;4sE_ot&}PT%lfcmfWmw zqWY;~rO-Hv^44$(sz_%~KLhrmhmKaOrY}*VYAx6yz>krp3yl!+zbR(X3g_K7clB8dM3IMu9YYpLpu?)YXu(0t^=sR(V!|#a!n1t>D0B+R)AtKUe>UPEDJZ z_%AO8;HT;lnlUpA_ftUEYMXDjSZ(zvXB!8j?;cY9)~V7KdNQ^3|EX z_XZ6y#P8zkjtq_`$;G7NDC7Zb=|oJbC@rIj4ghNFu$!g>k!$HzBuMAk%EcZ*C~MVXSzeK`X=UU$NLI`R?rVj<^S(5d+dul z3&6dH;(7_Z_C}t^x2=;lwfG)zKVU4bKzTH-M|hB1T-_i>E(NT>oq5SXP!?QlIRGPk z261j>BCOoXGd$^Cd?{JZknE4%AYxJ`?Y)bg+Ty=VOp zAJ7-(j?XVsCbGkc`0{|x9t>KHr=a4q-WIfiXd^T+`>y48CCi&zSbvA$+Ba3_LKa;#&82Ir0@lR)+%6$Qx@8VXQ zJxOsjtGFdqMh>SZ69p8C7L)68$%f3M(!|y-T|tsfs7WGkbiaMWbl&_%RKoraHv- zD>SWJHU<$VD!%&~nz@FH%_oWFaU5++&O6Xa_!~6Tq9kU{xlt+{<%^?m4n5ReF#MO@ zZ}y8l2f*(C&0GrbNQSIwv{`E+ssBi;uSQ*`D=fPMFX<=pA+)wmMxRY?dGsQ`6mstk zQO42y#1U9jBp6&Ne1~wu6zwLxXVffH7Y_m{bdOvG(1_$Gpqm|f%4hPFaeh5Z_zPz5 ztHOf9ejefyNW0|)$Zz?uL*PPIukPH>@XgiDA#4Tt@GBpkAMK{cCuxE49TQGD55ty` zxgz`gNVTOYJSCKg$(eTNz}jxkHbBUsB5$t0js`g4d;K*7Ft`28d++WyIE}Bx)syHa zn1!AtQ|VJxqFPL37l0x2FiQ*LeidjX!1^bbmSr5cAf3 zK4L}>SN8sFqag$1;_6=Bd_K)0V3%XIi|Mig-!)Q$3)){mcWw1riMLiyo!~O;HDECX zEp=VrWChJ}gF%9@hXJ(&PWxn+DforCEN%IJ*aR1Zgwmumx z?)x&c@Mv~3hK$$W1pPv8Gqptc*2e0R>wf)j?yr0q$O9Nq##Dv*PoW3pB@Z3?F{!Ed zJD;^e7<-F)q?ax(HpXGcQI4vF!l@drK*H8*{dg%&9kzq!CFhu-q5bCiSfP2EA?ioy zvpp4FRYj{da!wtP@y#f!c*txFV+~c_jU{bxpE%Ym5AS+P@;$>2Io6#|gLE-*v4*KAi{%N*rul zi$u_NLc-@8SN~;!^)LQ>0Dqz&I5^-a1UN?|s7E6D+qUcwdzmoI+`#eY0ZsfXWAv`! zgBC9v-Mw`9h^Th)vOjfO=Dzbr|%d?sgBi zKXF`UHZ=Y~Vt5OqFsoDL>g-W=yNb@HB%4D@uft4tFwntGT3ay)!c~Hghr0%$ zW$h?8Eij#+9rJ9FD@RO4itBH$l-3>B`f1~;X^xvHfBWby4U4 z#jpQ>^xK1=%E$PBS;CLP|smEAnE#2&*1!T7?t%E1Ut=wpgb!QgD zqDC4;MN85Mmuw6ra;MyG?gUJrfVu~V4@Kc9*O>Bmct-;+K>DS2dM?|I0%mz=If9XbRwZ zJRRop2Og>a;!b}M8m+LcCe(uV={P^&ixjLDs5U`CqJ<#cs;G*TuliOSKAawif&h!iLpLuR0?A$o)EGA zYL+Sazu|51vg1`Z!lzRPIw+(=6d{m*6bQWH+ld!{r$NtBBVC8PKM?$$4*o3+Oqg?Y zlzOJJLQbLpMaG<-OipO;TC(8^+AMmq2%#pE7^yQ!k;6P%srUFp^j(&XlCCx70Im!Di z=YesM0@O}GL+->E;#33?bRuCs#g>mMM|-ux@(id->sebxDw>}|CNv+WzmKQPxB@YS z!za7DMecAu*r^YNFG^dtHh=j^7(d?`GQQgkjaDtkD-g4tUr|~g!UyjZk0J4C%xKuk zE~D)^v5Ja*=40d4`U8d=&;gAhexzJp_sx$Jv!&{i?SsgayI_$LYhF`&OpG9PDpr+o za%GXgE#Y^D;EA?;VQc_nx+ znj|Ee6X87#eq5^Ng-aw_LLgsFy!3}_%Vx@Wn^Ig$j6I6(5D>QbB5!n2je-}+gTEKyM~BG zc&CF4DV2!EU*`d@?6`S&@N^te@0DRLAgCSe z*tp3`ql`OZfLCQBG0N!xuZAw$$aNjg^C87{j79P-omcODP)bZt5q7cAGfsv&Op_CY z&)2$4c3mby?VS33`$}O=mzFJ;k6BRusa9L_|A@K=_Pn~TUBIzzH8z_xwr#Vq-PlRf z*tTukW@B58ZD-%_@#Ne23+G(xT60Z|ku~c2ho)2gn&0II1L1-U=VF#SC-j`mn3N#LrWbgTxxuBI3ayd%;86&*U*=aC2|Cdv)kEmQyM+JUmYd! zxLFd%m>vp8F}8*{eqLA=_MBo*pb99YLk%1FU4k-0PGAl_>lgnRDs;X-g9F(fB{6!I zL@Dl}>?@M!!->zL8eAy1GXtO5!!EOE+@f;B0>vd!R?Q_W~HO~JNGOcnZVSa;bty`k2E7h7!7*u~-I+qKmC44dg z!(TlkEQh5RYLA4PKbck=!Vmfw(Xg2W3;j19q2BZ}7Vf_wN&pb5@F+HbSb*}JTAkzB zm<2VMx@nS@TMzEZeM>B_F```9hdR&Eqp*;krnrBpFO+GO@U?>vk`)rIq5W8EmSC&G z$7L45!XFvQqYFkdMcpI9OsY1qRTMfZ7iCEVN;|l6n>h4;t)gefgs7T`T|}zlArG>o zKmU#?n`Hq#Fa>ov^R%EMG%i+Y$Qv~Q4NY6m30tuo$t8C}vTm?X1>u#~&nT2D{Yzo} z5=#q5xaN4(rnqtl+tP4)(ZkQ?=)}3j&K@F{8ih4*TIX5Hj9c{|1QeLN20VzKM&pb) z@4{sX9%PGZC2vfkwGXc*toqz%EIfTFlmZm4U~9euJ`m*Y6?X(UD@VSOW9(bB$b4fv zFlgilAJVE!TGNEU?aj?C8cSmNq{A!e%i`5Zle2^d{2999tx6j4I{Uw<>ZL1`R<^s5 zHclM(q8^ydl`}zE&%F?C6ZU-Sv<%grsWD?mcC6p_pNkf|`|$O?k5!B|AwFNLFulh7 z60ABXM{N&&zkr;9T1OevT|R_-?Y=C6kL>iZ5viwURFHBDoHjKsB``h!eN zj=e;-K(P$U3YViMSdR{08PGEeRUfm+xmzFa`&y{ad0aU~S!%lcijqs~dHQ8Zlu!F} z}!yM%{_?Qh|LFI?#+C2eE8EBNIT(zCvW%Y{w%+)i{vOY_!C zCY!+T3C=x^UWwJ!KmT*IPzPGqm3ZH8M|93V;;f>NpSpj1;F>sc)T8I0XE$D{)9U2w zuuTuA!j~;lVc7nlX#UwX@UyXlA8<$gE_sFbmuIZ!x;@5)JDT0BH6*IMwS+hU z>L~qMhQ2>DhDvsdt0jmz7XW@B)JnSXOtXdcJPJE!3D@1xd}Y;*lB2uArCfJ2>s?mvmdeu zHwZ0Oq;w!QFvnD(%8m2a+Szc|OL+6T`ao6)F{-PtlV$QJ8)hY`D`rTSL2bPw*W%k~ zx&{67zL`uX@)JvIp|B&r?o~2$g87H8%qFH2KzRIW3D7U!hhzRytAlY;F%Rpn6it(r zLkM+z-i z^^9by1z+N&`j=%_bLQokFKu147dAQD7la4?PXs2%pH&61O(l1vpgZ4fHNG;*XLcK6 zGO7&u$7+cdegzv8)a{|t7Dk{6&E0v^)W~{j&*Og9lUPBQJ)v)pvVf8ux(+q; zCDeDES~hYKxw~LT*$YDK(PfQUiiJ{D?;mo@QQc5i%$5QLVENT-j@IO(i1LQYYXX%e zP0j;<;}(5QqNhQ7)+8HYK#S}k?KUJX|8JoX>dT-KVDM1t1q`@MM;;LNfUY3IvC%No z@vSLH_ui4j%XAW)h=LW7dp$w_gU55~-a>i9kqdhA&r_ZIaWLAL24sqB$!{ave|S*o zMooglqHrq6PY&Xyx!0fm41*Zr^oXKk!c?+62`71aP-TRACl2IgWXS4zZ(J;E__M);p9JQr7 zAnaEnNAFy?A*roZt^N7g&B5gwvO~j;%WB9|?%o9Hgpm^KdNsvT>#m?nGQ}7B#$7ip z9Ldn*15~A$beXB${V$q+7Ws4P=nMBHQ3a4tX2+8K-^Vg_#k75Y{qF}T*OL0&HlLsA zMt^IgiR#gJGQ7TL^0Ytm?Q*<{&h6vB4NMC#HTF93`D2)31Hv^WMO+SdA+%%<#u1u# zZx`4n(?_|LR`DNdi*m~7t$^(g;e`atxW(pR?EOSkBTNrdFgh6%s8Z;GX!0aiMEp)A z2A+Tg!T||7C<*zs+^|P;>9!wuZ`Y=$A zuIpL!Xx)zQw{HITTJR%%K~w`E=-+2j|98JqD~;E?0r~zcH!R|G=KI24dgLD-!;Uv) z2)72Oyq<95fouUHDooq7RMP4E*D->2k*k=tw`;8$~lsp?Aa(pY7tPB$w+gbt)%UP7!!$EjG%|2>VD zDNvG4e+C5FmqZOfB0A+k5|EPT3~ZsnRw4huBcZJtKGMm>N@62hUPkWl_lK{-msSPB zE5-c5fC=2P@*f-04Hh|zPKG{4obz>uc*$Q!38qqlDkRXlFF; zOKXP%1sdV*=%q7A89IL(H*OI8j*Ubn)U*|T>AR8prvx>0B4qte(Q%QTyz#Gw0Q~Or zNR2PCBoX6EWShrewRTd(l49p29N9P@al!UsaA5eAuFMJ8WolR>thkC#*ABE`+0RK% zV>cp&V@<0OrAGO@oXn(QsKimDQxvl4$ua|or{MG)eD{6+e>@BDNJ0spg+#kCzYuBx z2wXA}6oB9B6P;xg@@bP4aolfcPb~Pog+0XTt-+H(&b7lCt9YeX$8J&&irV4wlHd-e ztz*n;^&YkeP&S2_@Sbcr27;FGg0|&4tcP_UUdK6>2Zg(PdcuEBI@KkAvdyRWUz}Kr zo3ki0M#D)erht#c^>?^J+SHnMsK>3N2>?l1HX>_~828>lCZv#C)W8kiY0j#{WTjYe zwc*{DMrG^-{uNaEqXD+mEQ!2ta2z>fpx)diy4`4<4WewOR;fp=&s-Oa|L>#Lej3h~Qvy=|{yEQYw`?IA!{48}ZaDE|IfT|u#r^!Ni+7br+>UG&w!PeT#mp^?Yu#EY zRco8o!fA)6`uos5KR;I_bE(i|s%I<@b7&=wxaKFLPbm!wrqwr+?RZ1!r$nMw>;Mh! zU{9REUv03FBrX%mPAGTr1+bL z5>4~!F9P8n zX?xmHxK_%)`NC%5@jR}7iwUrB9P%(Zo{colq(Ia%n*DMC!xvxFV>~41T9(L-6RpbU zma<6yP5hlzhwl5D7G*2p25ElqwK;A`cff%7R>&eWtCM4hMs6o$kwqm&e%DoCA9#nD zkJZd&?Hyu+ocU%EDH}q#DK!*L+EMt<_Gd(pe1S9oK(Gx9?Ex1q=<|-``z@y9?^qMd zWYNA+eYCg;I$V=gkC8oeOl%tnJ0Dz5WOt)-kM7l9iqV+)7V-=KPhd@3$HQS5Xhl7}C}g=qzbR2bFte&-7a`;Y}p4 z#`T>#arFJ?-6c|hy>RXfH}>ZvRk_6Lg*hk;M*mL4l~W0m3+10hsq7J>aJwVa)?fZL zJF1*=6=5rCsgZ&y^V;L6be_EOZ4N%H+bkLTRzZ96E5fwG!w*?FRSZ>nnk5WOk3N6E z2sUvm$A4CId*a+=EIr_8e45k2{31B_VM__(bz~1H$6W=cLEMOE;Ov`^p`qDmutVgMp zV(|vmQz~e9EJqMBqwxmIJn!TmQ5l8VECzL9X8`}?zM|3)A47WG3ubidZ=0SjSRN9Z zry!y>nt{Vt2>J$Iz68Z(wk0NN&LiPj{)n_SIpyrXNalY&@);r2UpP$w9DX=gML<(u zC}+;CHam%^??Tc2)wUljWUSwehk~+n#(jQmDm;nc-D8i-SD(E)2lNmUasRbkH7$UmFAq@Jb52-5Xsp9Zp82pfJ@y1HXXl_4Sy5PAqhc&%XG$zgv3tqe3j1BL#Jq!~b>sM&T(`yDgX=FS@i0cPX_rj#04vdWwcjgFHO8LK?oBmdy1e1_fk9S~5 zV0Rb+T!MA9!-T?q1AqN%pYiw*PYC>Gu5cjp=sb_P^;MW36SJz)^FH?KX+FZMRMqJ~ zj!UU;)|v7&B!p!jCqwBM-B^V+Oi=54lHI+iVkJu_woAs|T%|BHX3b7!XkUZziUuu_Un-36wZj4kIjIixoicN!Tch^hc zbL4lW?5V5d?W;0d4_6bPd7Acu4D1?~rNtwhfEynwjT7oJ!-=nySoXrnUEP)UP!FxJ zNKSccNXqo1Ic=oR;|3l49*=_mQu0Gn7S#t{g6F>SdAJ?T(ZW9REvx@xqu^p2&4{?B z%4ceU+46-0lTDDNGTCu)3=3>y@zcvjc-`|sMe6+w5|%HDRshA}h(!aSb3A#l?TmOz zI-f@T0FS=QB&&b0txSyVHdo{tw#SokwC?11HwcyW@WtDJ^!xQR=wN4Y>i>?Sk_ z4mNV44(@AaY9OchRQ-8r{esh+7U30xVxsJ$(Vf9D%(wr$O!gIS&ooGF74TEPcF zs2!9%NobqLp%4|`jfeOfYIz{rv-^m|_KXx<1H zu3t#Avo)hYB+0BG5&J+lXH5K^{K>puG4FHVCX2|BdyAW!)-K+kg7Iky5buMUEAH^^ zK+{X!S)99O;zOF8CAe)%0N-5o)opN@Wj-A&^Sxi6*}?M#(hdN@M2-Cm{QkPk8RVv- zV3`)%G-#5d1rwL|j99dMAFVy3um_1dGqAMw0PG#1=UF7CAK%y@j{2v(`S`_}+dJr7 z>)URPuLxLgHmtZVGPHa$lJ^s@Y?|~n{^p7k{3%{)ziLA!+=HG_B)OZ0L(VpI0P=>Y zn8{uDR2NFwcr@mM6Dh-9(ilJX<}k-$>bkt!QFS!|SlN&KPCWBq!>;>*Sj}h@c+H|i zXN9ox^>7GYB%a>obZz?BZ52E{KsnzKKGQ^5Z7q!)YHmM1_0Uoss48O3}Q zF^~_~SMY%5mLlbMk6fX3%2`EC?w{9^D$k5hQJChdepqF)NE2cK_XWlLmEL}PtF+Bp z%zXrZLB=@ETl3`Nv?v(0;fXMz9euF>e!0(@Y|BS@R2CA z!C;3%?J0yz=XtAsm8cy>a|ND+{!ZfiZ}s(Q>}TWlV&iQURSqtgq!G_8V^Sxt5zXR13yw47ucJInmsrH;m}mydPpQDL|G_v=wQ{xbP|-1 z^@&|^;o1vSizy)kSZc0SU!aFS2B|sbK_1NVmtwV=ANx;{ZgJZa(9Wn99s9|-S4ciE zfj?8GEw!>ir%yrRZGl!;tbY}-=|)qiFEQPr9TGV4JU)X){EMXvz_J}bl?vz|PE+DLpBLi}%7`z!BVkrA%Ye{3 z!oRpyau<7)WFN~|s8v~uv?C%5Jc|vF(O>$$5a7(*Lft-;9rIwv4(GS&kB6B&*x-Ks z8gzyiKRAXkRjw-grV(-@hMZMiqW%~Ci1-~F{+9)&m1Ji^UC2GLsDl_HHX}&%i1C|) zpl`_WOlCIlQ*65t`9>H}T0OCj99yyAkEXF{*h=|4g+KG$hbhl3ptCDvVSS=^qgj5$ zY}*&VR*S|37^1_^I8-lyz41;c`=U&Z^3iKxdjlh{=9pI+zNt(NlKZB&r&BwC-TuVU zhdHmUpvO^-({}3_>TyaatE_!e?SRpE{>&TcFPCnB%dznyIN))C2>neIX=2>|JP|Ef zV3SBh9K!l%=xd8xA&< z=A1$+)e$B-Q;&uuOay1cmKZltOo@(F`o|IeeBkpML4rxwiB^XjFlORzE^}OVgW(yA zjHI#Xs8(wSo$FhnuG4{5hsTMtc&%9Qtw_t1R@o#Sz9E<)>v4Tf%7OM6r}r8hf$#mj zbC_P!?&=cAp{{< zzC$2R2|Ge+5TrGi=QhC|2=ROYBeCD`V7Dm$O}IEEQpq2#hq6aGtT}C@3*Mt+Y{GbP zD!hIHm-Uq(?Wx2G`ThAsA|vmY7TJiO-&~iZhtfB_kq)a#l}4^?oWZaPRGdDRC|rz` z^+|FB>ls+ds=VRE^@-OucbddG#BGwJIkZpHi1R*UMd=Hq7XSjMJRb=7C!E)hbDSL1 znAfSv;!)G5&ao2`V?_4v$3oo8Fu$#uw-h{7C>$JDb`myPpty0FLmRXX7R+HD8hl-bYkpHefs&oj{p{&W^f+9nJ{Qw^3Q8(#F`Jk8m zGnfmRnWT+u^VE#L7o4VUsZ6+`95S)!;GcD~?9zwFs=HW_2yT?giO5yksFZR^mfDbv zcYLl%8$_CfE!ERSx&4#D7#~__9@xsJ%D#C@dv9Xenm^@RESoS*pyw2!F3OT{fQ2EM z1F}?gFnNW+W0uEO-oVpdvVQG|T5}s#E^hkR>1Up(eR=c&JSsu3tpNQ7=)3=wbiXtz zk_+_hrmjc^2*XJ zHKt5uy8cy^fKHTfZ)6X0TTUB<7(I_bsIIFsemxy|xc*(uYoGj#$UzYE(cg>)?hrl> zxZ14peQ7ynBDmNha)NxGsryz3)kQ2Dc_4K$bd*!Hb&I5FS&3UY{k)E9b7mx*?*u-0G`*UU{0 zm17H7wdn_)$7lH#DOi2JwJ2j)^0`=nzR(bc7@@{8+pD*&?k94JX0|~)=2;5UTs}DZ z>-F_;-(wJg<(Js9a*d@kM$So`Wf*9J%4XDf3Z%8tOFUrDK&WcQanr7xZgnQV#IODF z|ES$K);y%*>DLwMyj6r$WWI;HYc76BQkfqJgN(%`=v1e&`_dNmHFT538=XYgX(r*4MnyqBxES% zQpX^7c?NnG;;xf{AQTgRL}W(aM7@(mxH-bGLp^w|mx^P`wSKejM+x*f6A}l!AKpzP zHtewxtKTh<`xaJz)U?a0JRQ|jYun-!P_jd0pD~AyI6Spjh-!&r9fa=>4zAil;N$8($v_@mo^QH$=Ez28IwHM5$Qa{{z9Mhc!WA3FY8Hrd5{G3&UzmsQj$b$nqvh^2d2 zY6onFbLsFa#4GQRxaq{}w{CL|~uP7#%bEeWHW4J9KERL+s_ zs5&!f(Qk-skeSM5MQEj@t>HPRg}AIy21T^-za=;$PJEMPgX8jk7m`QfBX@vXh zb2)(D;tOIJ03k_%to6T>ZLdV?==bbmDe53@zkHRU6kj;4i8vaXY0CcM`=N+=Q%68k zT+<}}xSRDxsG^#)mm93;+mSPd7rh=nF@Dd7wheVoz7%pM@@5Kwy7i=L-%uVy8~ZLC zAKx^)wTx{IZ2H!%pN5D`;;Ld3J*9;>P_L+z<`y}bwxHwX^xS|aN`IN6d4-WWJOvXj ziL#D{-va?;^W`uC zaDZ5VZv-rh00}BH9Ze43E#$-hNYRWK|IxF5kh52N0Pa3?D3iQIe60z(hbO31Omkce zlZtFKEJ9CigX^J&#NtlQWH-2O8ad%lS;JM+J24LX{I3RfHS{!Dn+ANnht&v;T=l$n znM0$3y}@$VcL^ij=8^b&!t#70Ee!ed;cvm`dU5=j%Sb3)Ln9%Xq%dR%RLFO+3>S0K zemw3O2Frp-AYLLKXEiQRx5cS7ZN0F$$pg7(WpK z`B;7R0eSDb%3PV)0_^a0_WgAJ=IBK{QFB%rr)uqV@@)VZ{cw27y`)EVHI!oKC-YTD zXUk8MvXj+Yt`*oQPs}G~|88^Bfvk97KT?j%4bKX{Ry2XQ7^e9ZawN6cdX*q2JN)%BH}kCcnJQz*2NqA zu#AI)9Fu7e@2~^GoYkRHhbX?#kg89pCubUvb>H~Wl(jwSRDVWTGCszO1;WIw0ol!* z8q?6dO5WdI?+C=4hG5O_EJ3T;>=u23R6@+_=lebLM|hP@@rnrb?ZQxoxF2l-Z$tSY zCKjro)G%8^lu`W|LDC!Vi9MI)*f?C}O^qeWYfD(>0u;w=Rs88p^??A1~on@{7_Miqn|CSC+NG z7xLFBS+-_8)m?l>qSM!~j{%0ghLhzMwCU&7;;iQ%JE1*MSJ*~W%Jaa3yq-%YuW;uKIkRx`7Rhw%bP&@suG>Ge9-~y4rdT6(6qO_mOSAV622v$z(RV=3+wg#$YOfa5kyy#3F zUglfQdAIrIK$S&*5bOY2=AVYSoSFNR|0BzqIkjH`C&9Z6lkXvrisEL*Az6k6qzrNF zd#s(-xJ@dESrFhLObr|qwFZ6>W^ zjlNRZF8JWP&1n=m0#>juxX68Dev*6ro~x6#?DhE;1FOxbWmGrl{#x(-2@x24hmZ#HV^;c3`+up;|b3Yo__JdH5 zsKHucHuK+zl^;RtdQ2(&G>0q8Q{6}Wldt8_cafw9j)YvV{UE29yL*{1#>l!tllOZz z8a>jzPigf2-r~sLZI%Vf76Vj{+GnJH5;G!6rA2yb>&CBG=lvSi@kEWPQG^ZLLah4- zC}aFepmM#7iob!0y!BoPy%N@;tb;9u@td#!O0Rb>p?mgGX%{IItNC;=it@BzXO3SW zdXEqZbxpgB!R~OvELt4lbEfC}MKA#%AfOHA0nGI1o}*K&C9Xn653%1!_nF}+C`5(` zqR7}pH=vK&Aj>lQ&lXUu@`TKynMa2!6;bjp(gUuAP_9lnUKhg^Fkmi+ps}i8{9ErLtjxdFRE@RdBmw?}Qw z-^%rQJQ;(|qwi$0#bp-IdS1b~-Sdu7>KFA*(~m$h&2&+FYgs?2=~YoVRtP#cEj;WN z6dxg4xx6aJAaKvGh*X&uI&qhX1sZKwB}eZi-*TQjJC@4==5v^ycTJ#Fn*}7l({Xqd zAmRpg$U^BQyvuN>B>JKS;8=Lsv+W|pJX-;8M+~8fLMU*!oIZzWNBG~^MS8EI;`zdi#A{f* z&KPq{tlRd$l7EMC!r=$yf<`e~3eK1v%Z@j&Piu#YpkCJI0px#qu!63w&baY zkY73DG*fGt4VbNs0wEuO3!)`%o`bnb^}pGRg&=!%HJ88%47domgnEw*95mduG#28$+@jcC-%P$pOQdpXrP-Yacm3#Q` z?20y}53u=5lc0*3_e%?zhsxlu`vi(KC z>X4=sfY+;{9|N*YC2%u+Z26(8Qqh9rmE8C4Vc`oIZTo&Z&H)@N$reW1tCMOWtq+v` zii@mH*-_lBv>mKD9r_YxUF*YU%pZ92E-jytsf$erwe}EJAw!y9ovva)9MDIZ7Ju|C zknCDT8v0{kdG5YqH|n#zRd>{v#SFkAy>OZ3f8|WG`cEgUiw<&R&S^`iJ7^Whxx`0# z%PXD5>s5&wyoZ{EfoOHC#g| z-gV2V4nv$Av`dz=!O%%u+J4H>*n>Ej-Q6_~6S+B5V#@azfMv%^Jap}=x>tdD2g9*$ zrzq5>^`u{-*zn#huLRpJ7!Y#*9`fR!q0Z=4mVS5}M7v6SFrJEJ*nIF-2~1X?kmoQY zE6Sr+{>;O;FNRqF19A7W1z?5%`lD=IIP!7E(sI@2hWbGE#mfuv3g_dc4rX z3o~$`k=s|GCQS!X^6<;R;`=bn(Hof#6w-=0hJ~zs6^KtF-B>g9xo|GILE>$lxVq^U ztyOJ6x?eROLGLL9M9=P!CL>J*cx@a6K83lWiCV1aL?Zo4wop9S@4!La-w2vlZQ!;o zdsaLl8!-EwqGK<{Zok3zbZ?=Di-;Q@*9qO_5uuBC5pE7gY)&fU(X-VbA5A07Ul+H} zAFwSP;*9I?uUuiQj#eoAJwzPzHP3s~h%VJDiwjLMs7r(u?q)_;n4QG}q~4Z3cV_sL zz98lR5M~(kxqvf>^Lt}gn29v5kW*Y#Ua=`2sW?VD&Z24vrEcIQf$qs_A5L%dZ~0y) z`*eWm4~W<#n5^j{#H7(Q8aILjJ~)Fr@nWodawh5G!02)t5s7?_7o4L>$$Jvc0$gX0 z*3Z2=L-Sd%D>>PwsI6Oum;-0)7v9jcvdnufb9D?Y6QT5R!le$xJqrp^l7)sR8L~zH zqiJxz+Bfmw-CXFj@%ZJ}67gNv)l<~<>M$c;S>+|29R{R#(poKF(QqOIzeYydK*o@b zKpewBjQInQf}E+M56%#ZD3Xeq#G))`gF>0df*2>%aQpRvg(D01|J%w-`;wRkNT^)e z5(EBy=au&@wa5+{to;-UcZ4`hV*d!U4z9$Ic8C};)t2HD7ZYoB#qhP)0{w!X<+auI zX97p_QMzgLMn|3|>HVwG{cpB@Di|RITTv>cpKvqO4{Z8hSsuD^vMf& zSoZ~QtiQ*tCO9hQtlT|4fx~D;)!(8!51QSYEj}XIqtT=P-j43fFNy^Kg@SV5E?{43 z$y2G>a@}due9Ra2#dhF{yQaLa?aun5^mw3$rt!pns)Vy7DZ$N}6j8q43(%q)%Lpb9 zJDbcI1LP@wavp&f&()_2=lXhV69&n83qura1IzZkviz`rk@gIIX!_w9af|qBG4TWPoh!EB?Roq>3s)R-La#bJK zN?&6v&8^=nP`cPR%4))f2a+LP8|T%J^;`K;FZ|tK= zr?f8tKfeOk8Gz4e%MdmsN#C7mPJ3)Aq2;eMKOXmIs`8gDr%O z62X;dZ2-eW#DRS33WEtV$mXlH_!bb@%W1{`aQ?;{YV)Jcp)&Yq+BOK5vKfIT>*IwM zQde!kitkRXvE?Ys$poQFyY>0UbT^3ZiV1p%s~h$*Yde#~75G-mLSV#A`K*fi8lpGe zN+UbJ!Ukk51HsKOmYh*$zb>d)kCP|y5#*9FK9%XgZ#6*$=4Ue{TPJ!uZ%5+rWL3iC ziVRHntH}*I<>Xum$kZUUv-CfPGD$MZca4<^%@;i6)9poh&S;b-Faz zc?~rDF*zKq&z9K%h(|OL2z?ne*wo@W2VQb(6$i(o^}lN1`s4kMAU$2MpMOzh!>FUg z9Pek{ID=>ZjZyioLu?(YU-KX#`15j$;7H0`M~~-J>r-FxE_11t8YOlJr5&;8zsp5 z9p!{p*yOhFNZsU~7B}mXYcA?!MbTq^{c+E}+Jm*I2X6)v8Ra<8MZ);#2QG4f42qDKU`?so8stcB{W9>E1a zbg5oyJaH5k33E3O`0dVg{TiNOYn^(p?W+tKwf43ao1k5VF%ahKP?4JJ&z05giZ6>5 zfJKU$iU!C>{i}xP1pr1f738g znVhGd^jyqSBL5In&;uzKdee&Gb@&X0Ol-iFBB5QZ!9W>fcE$Ye*Jkuz?uZB1ST= zbuKotgH^=c*@teWbA$1pAYc^pN1+5AE zb|PrCsyyMF@?yquS}G~PU_S7AYDjnXh~G@nCSAtj-72a_8$HkT_oKZTMb`SS&L_^7 z`Y!!fP_TDPuND2&?|}!}(x!QMzyFh@x>Q1=&HWYt9oxvSLxmWR3&;MjQeZcDeg+EB z4zarR$5?$L_C{yvV7fs$2tR9pf#vmXoMF~^Jf-lR+@%t~79>a~na z5&BDu)C1kH`JZ4aaMUeF4#5QCW)1u^ev4_TW zKYo|x%zhNMhu3gX%m!=j#|PPv>eumil-x`fN}CEddv=d#6}GF>37eiq%I@u9l6wMA zG)ieu#ebC#Fh3mCC={vv@ej1QfBS!iUc;Be8o+@p&(Iz)TcEpVp^UICI4&PzkT*%k9#v$`1*3j;|T()E>Sk#g0|EN*bDK#Ys`k;<%YE&{P&=d zu7huvJ=i!7M6>kC5HA92GLl=(B_GD*J$q z$LLa2Z1-3|a6c&g%|hk-ng2j7Uli*AiXZpi_5c*nBB|&B=#$G@6h=8iBvNq1FS6A! zf|;d`>IZbtauT(@WVlh@%$q)aQ>YQ@A%QpjS0!zDpx1A=>NpQvqk%9s`K5F%#Su;- z6YiQco~o0(ZUHVY@%&yV1qmqnz4!*czFxDPcP-fR5Tj*YG8jGtOQ#hlfVv}2LtYqt zB?~FYIX|}>8l-S|XIZF7BS_1YPs!KN`F4CKyI)+F+F{`~*oI2SAG1RX;S9QBM{X0z zFn+Pc%kF#&=9i7)4Loc^q+u@W-ss4!w_o=ANjXjkEu1T-s97wGMx16Dn382ZIbC~} zZzK*=FqjS2X~D7uW8)eX^8m2RkuIj0yhm8=FJA(#B+DR@1j+^ zBd8xk>z%U?Y9XO<2?VBX_7w8pe{L(3^Fp@zr$8C$xIsGp2&WQRV(>TSt?&k&6#d={ zl}Oj3!rrY#4Ihqb-$l6 ziF&D$&ecm(PYe>|0m{X!WYzEBEVC(tB31w%PfBHQN+VDRWPkIKuk^ilb7i-4O!(0-d+lV6YekV;lk!>Bn(asZ)O8}53WBb z^e6@Dl{Y`)==>lMXuJ%AjXqvaTJDIaDqMSzM7EZrO@m|d+s!Q_Q{sM{SU{ai{@CnR zJzTW{9xXcYNgx6;8*D)!ZFyQW@2m-?+{#@7>gJ-JX&O6&d#9|YiIK{m?~)6KLFAIH z>s=yk)JLq{7eo-@8SRoS-PJ#BQTf}1-B7R|N8VPyIexV79Eh48^{oP1g~!hB`9%na zh=;%arj}*V$F9-J8uCLp{dyNW2;pk=R80A1)o0B0e~sqW|K4pN4roBl6UvHyD`lbD zxo$fAx?~Pg6^cf`_!qHqwkC^WrHuCqBK~n zA=KDY;kyt}Tc?IEoOD&DvzymJ{TL}TCbh2|X=P6ISdVo`^n-Z3uZJMm z@cAc-Od9ZbTHZt&RL$pHZukpe8vp>L|0Dtkw`F;cv2|I8GOK?@^#?<4^-McA-M5d0sK*oeOsi5V_>KpK_{#!FLYgUrtFY`j%6n=ym$9YIEhlhp^-(?&~ z(Z0jo;V7p~!bb6N`YE!wk)~4XRX^J~;GpZ}q?qI$ImJ_7VZ|6C<~6&&Rh_YHX-`H{ zJW0jCUvnMFJXHb@5lV5p7x4W77o^xSD6KiADfK6ti0y?gH4{*SVtD916-v z-bP5EeAtjYvsGS}Z!R4-E1c7QH$IjL#z3B;?GDOrI!sJ(SUc8IY zz6#h}JDeE=vD-uG`eVhYJ$z!?IxB~qTHvtL&w9I7Y+!N27ZHoxn{J6PiO;2c*@$<5 zIiIEQ>Sq$WaUp9Yl^{lBH-FqEz5Ix$NtLZGK0Fx+`YKJMR^=_{O!#+0qmXi8O72Y< zof;8+a%`dy#NgrpMi)$20bGm)jpIC)MZ!dB@cel~R#T-jcl$;CI>DB?7CsIstj%!frdc(S$n5 zJ;rJ3g!iN1(eebS=vc2E?$5aAl$hvp_r7A6@dTPMW(8AqhtD~@QMr3_%d((5Aq@nK z)k9dzjNsX4R?L23>;W)rR$3GOcbWuK0e8EPSicwqv|Yno-@fqmy9mlo9v3mkjm{G# zpRn+DllRnK*HT6tIXspmq|P$M;cny#s7765@u9=5{Ew?_?8EH$0&KKvkB`ZI_{I>Y zhnMnJj*%UnS$TeI@KT7P%>vdxKL!9z@8qm=Ta$QVZiSmJbBOR&j8~x{xveS~nW|6U zN`*VaJ%UiO^`Pfw39RTb#OuK-{3pA;H^&INh4=ZvQRu1o#1R~mAU)sv z4&V%FK>Q=i3M1#H`Ofye-?7J@qe|pFJ3O&Rb|n%OcPZzxg$|2SqbrI`CJ?JehCh#< z0WE$(`~yHt;t<;cwwxej2wEcT7OOElgY6g7l#zvB1^gc#3A@Tcg;x>6%B%@2QYeDz zH}j*SHytk)ZK21s0yGp8(`ajbU@p_C$o0w(f>K9OLOib{F>e~tX+}_uY_c(yKwtSr zeAF5bWd0vf=fIdpw6*QnMq`^zgC=Qg+qP}nwr$&LY})Bi=RG}Ne!_Ll?0fdC zHM6!Zm{}3^d$lunoZD_Pm$EJTLzm<`Ia(%(8@)~a-Y&< zCE{JiWoj&J0eFK0U%A}ywG6njKTS%tsFNKiWIK0K!~uVKDgNz+*1jwp0W2sCq96hG zLS(b~;(M-6xhy!vB&75-tH{@>skXn{?y|f>#X26CV#GnZor-aX?%Caqz;knTi-Y&& zCybk4qS$XS#k{&qoKQJBR6nyS_`^M z531D>r3!MB_bjs;J@F@Q9kx=XRZVaBuL9q#F9pW{1$i$7Pyg8ts(p^?Ni=yU4T)t8 zek2i(thXwz%QqE?zJEkzpQhUXLcjMkbDqDrXJ;r{AB1>l;`W=aYu~qv2nlgo?rB35YuYaPn2xKd?lZ`8TUxJl!_E8wpzbHE*~+_sN_{&`%2g#USg)k~OT zY$OHFMr}fnbAXHi#WHJ_(^%ZnRABX+l+J{{tfJs%B19koT>& z_sXAK!|7X_$Q}>|dq&E3srR$P?zo-^MtJ_S&x3+AkJyXcB~0Mp5XN8i2U`I#syC7i zXya&|U50kF2><#Gyn9~?P5=t*%1v1TqaVJ{GP7}vT&fVRlh|TAu{jz^l4fcr(O<)D z_F5OL(D*&=Db5=K;Ty*mJk-4YoFUso=N(Q>bQ+0vR<&+Y z5!P_0ySkILW*O&%vIg~Hd$e~!LSv(yvkmS@&I6`qymo(&p~`P{ zQ@&Gr5S++{(L}VI-2sQ}BL!(>jQ0Ni+gYwZeDO#13V*yTa}*mVU;J1i0aWzVplE%6 z-3qRBH(;;GP`q-;8r1`p#*BwlfX&`^U)_e9Z^XxIU2YTv9h1k{fV;thuY+fOH(jNg z|Ln}fl1zPX3dz$sGFAU^K0bxq&n=7=`h{tHdvd`&=4=cs!}TMtPR1_gn zMz#+Tyff>kZw_NpB*$FwUM$J{8MA0q(in5Y<0Yk+Nc_{tBgk=MwUACTrn~!a-nT7_ zsJw^=VuB6hIG^UJ4mtSldzttS4|QTBCJMwDsR9(_S<<21BznTotadQ?h@nca|A!gP(-*raAn@^~89V z&F7aI4!3dj4H50)@h68M+XR)-iN09DTs1oHmXk=Ck!CELpYo3=;U_w1=j}IyYEj%) zSnHo}0nXOasB5K<^M8HES8esvoi;sQ2h%9dF8@f=e-s0)%OV~?kb>&ggMyh_F%rRTI88!ki5(&Ole1*!Y>dn)9RFru_ndz3)W5Psb8b`(r^eT zoYy)kAq%*N5@grXeXG{Wdr{AjUY~!3X(M>I{rRsD;(PIRVb1{-|`J4_#wz2^3{U?IK!#jR_%N{n=zVbs4 zsfOG_dEQr-wxtull)+|!^3d&PBHeCeH#^!u2ePpc+jP|*Nmp{pLDt8r^Y8yQI5%IY ze*mac(pMCK;kWdtP_j7;UIU&LlJn17d95QS8wf_w4a5w*_Z^bA8SJEa4lTgKe=?hN zp(r?bVRVu?**+CcZ~d2;s&|n&w&>7_`zw0~_Hd-qG_8jyQHr~|yPMd#B2z{gl+{cd z2!)~rh*F#|u3v7~@@gkXxhki;*P^zrJEYGwv&<{?WTu&R2l~iJcTow&_qrQ5x`txW32qk zsEBmPphRMGzWV4qP_r7YaI+e0jR&vmS+1C4*3uIp1`I|<}={V zE^9f^a=5+2F^12-(h1%=GB!6NrV&;0lFi0batwyj+~&F#!dZ2iX|Jy?`)CP(VmR`m zj$oHF#tTQ!qWG50(?oa$HmfowEy(B$BXN2BO5f+C;ylj+RV4d^8w%G+^D$QNniF*p zR4zxz`XeciIwLx9RICnuG%NkbP5iiKvd&f%Nj@$pnC=oIu>XoW(93=TUM>WTkquEj zwPy!>!mOJJ%6J^I2X{Dn((zTT_9{A4ymVYaOq8^5?%e{fQ?_4sFqd7$zr1?-qP_%B zkJfeD00umN?;!+tG-ALTr%2l)-paqOJrx;Xek03MFf_5l4!7S~7V(Gou@rN^w5J9$ z-rhgruDd2L8vRZt2^AjWZj?z4Z;Mb=WR@e$UGb}5FXT2%K3MP1dPywM>LQvK3AN6NPrBWN52!Q@{qq&K%1AoT^8*5??*sw20 zdBoiMw$t}Hc7b%Wx^1pEJ| zbX^0eZO4bPfcM^3LaU07O1d0W4D~?i?>mMFNI}phskbltPQkxv(38-dkr9f4M9ZwV zMSp^tfC^P5r!;jUzaO_b5WL215Q(_g%){FULr21W{xqJ4iyBsl!hTrFoYEzFc;B4F z(H0-S7f}F6F`6J--G>%o?U|-@EC0EoW#l~~8wC>*z zCXMqwl$dr0iRK!g0J-CI52n(vG^KNni9^?`PnR-;{1v#V0p?{`Y3pcT^d}!0D@fmd zH8?)0PNHCa;u7xz8dZR4lv=SBNh-F8Pmf!T<9N%#l{<^AewU=GHRFHu`zc*_HL}FA7$wuu4YTO2IMNc;~ zz}~t{f%dH9uRqR(FI1gMDxonuLx3P=*=3L_R?P$_iZ{JV%~XZpCendV2WD8E&;l5O z`8ZPj&R>13#bis(LI;T>GHVoq=zP0vHo^88xHoRukP$sziKvUZ)^^UYtd}3}?@JKlZpQe)a5s!`mfu}c#(2*L-Pp3!G zsax*7{I{H4xG6{F8ncm4F|iQ7UkNPP$WNap$8!!s8dX@^HFjQ9QvFb92gu1A9aQ@? z5m{c((yh<+DbVxgBo4Th%u{4Nzllr08=USv|)_UwjHn7E)dcmk<)W;^Tw-%N3E9ltz$aQQ9Zg_YJ%O&A9 z=Ps1GAy_im-x&`**&r59gNd4XLfbFweSs}tBv=Ug@9pjj`vran0MGdor1gK1UZg)V zKc_SF;KjyN^)3b|y==!Y*2-+Zj@oF2ju?x!IDXl9wO}l$1!epa`5+ zn8`tcwgudx{Xr)_`X0+4%-qJ_4*K^FV*Nk~(NVkz4SL(=_#NkxD3G3Fx;QwWNFwuN zK*@RBFORc82gOvGpdkWkg@0@FR^wM_YY_-jsvi&na<(5x($n8i>hFu5mz?0&Fwb1Z z*!t4h=?YM>t=8Op%u3M&X8V*}N{_{JA$O9k-E7|3)sW6Phl0PQ6*-nGXkH;9mK)|j zFCJ@{uObY(x=j6NW~HzKv11VjC$1D)mES`hdZ?zK!ACmd`q*STNwCm~SL5&Qj~&?||qO%!OAgn%uppo)+|1tAu_> z|2_LS&v}mt*cI0{y9sc0R$AR(K7+^e%97wY7ST4%a71bQ!>aL?y3K}IopnRTLt4-E z(6Q#-Gn|l8D}87b?EVIdB!&XMcGJ)x#I{xA^uoAauTzc}Tg~gK=IG@Dp#+`EvYM;_ zbfb(RRM#+-m=7&DKr?0TD@b z_OTSoz@{CKP>BGqE!fLpg{Ic|H#e6nv#lCcn4mWI1H4UcICPeK#sq?GNvu6~PWhQa zqvwzkbmxI*S>MQ|zwpe|q&KOqXGTrl_-X3Foh}#}VJ($TSYrkvWz#foObX}S z|CT%U7ye%Wz6CA?CZHa*;`!4@!lgE04Les!yJZ|oR~HI$+fY}P9*s13fMCy85w-?# z`MsA96YW*~dzT0XI0JeVe;&umS{;f6)Nqsf#2$Ot23H!PyVrqN{d4xR2pV!i^?Oau z3kO&`@#ZKM3c?C2AFo$$)!n7MboyUXYgJNPD8vzKA9)uL>=Crkk==roW$NVCs5V6% zm^91-Q6ek=^7Q5#^Ye2JO@KRRNDMi(*r zSKl<=8*A&5)N3?KPWJK!T!+K`bnRmit|ZYs+_QZWqhDsfW&2nX1Y2(2mUlNU$6=j>Jk7KfBbajg;0V85yT4hnlCjQK!F$c@Rl6azm z?ANrn-i3gHC7*z*)ncP{5{fG=NQtihd?0yV)l>v}((T4h@auUYKCXGuF_>_Ss3s`o z#K3^==bsqpu>Vyg$-eks0Q}EI0rr5wa=@>l2C{T3gP38{zSV}rVx8fesGN^&6avP; z(8&;fm!1Ns-}Co1PxP^bkBmHJDX%^6$VjkbgDri*q69ZuYa4p`-hve2%EZt-j}m-~ zgMM7973;kSv^Z%SrG@-e)1)Usvi`|63M^{StU`aD@%^{PJT0x5QSQDj-;;$NR$MLyo2AMy`LOJ$$LHb8+_k==TC|uyY40@nk>b7=R{zA1#=L|e%So*# z^D|lQO0Xt#nI5qL*Twc3V^x(&pN?AC-w~gm=z_;NR7YWs4=>TsLVCnw|Am2NOc*&8inJFLx(EKQNaI$`DZ&_wN^;^YVtG zb=4C^W)!Ij51*{2W*hp?Xd4U>28Qz_o(_w$T4Y|kh$QL8tyOskt*8y(bzSh(Gn5?y z=;a|gFw8J^&K9C+;0yCSfIT^wBw|ynoRt->d5Mn-)xd54tg{-y%K3;3D2lI(RNniG zU-(os5Sj z3B{qHWq6`U5koZ2Z=8>?4iO#8G|#W4g46e=3^ z16p1AGuw}H(iJ~>Yn5{ugHIxfl#GB~2bl0kRiJia40MPlb}~VsM0i(W$Vr7z+-*QL zeeutpq_6C01(;W8Mk!O}s<6pKX5D^RO>WuTLf5hJi9+P31BLzGdJr$QQF77><7 zt+U5I!lWdLwi2C}H5uWcen~WiR7QzbkokBP=>aRgjS@0=2W+R!5!jn;6lp_lOI!1emZ$8CJC8ZUPK-=-^YP|m8j`J zWMispr-ly7E1ofe$aSfdKIn-JUIEDt{(!NZszAsXe(P3?J7^z4eC4Nt%>6i&HFHGz z9J>A`2Rrq;BDH#J=j5;1NpkR!J}}M(UOvMTx=7$lQM@FO1_pC}F02xSs0zu;5woH$ z4MyWNfE;EDR%F7=J(V6(^~=FuGpllG00IR9(*ON!7tpVyyK9XT-wf_u z26^c6yTas4sn~p?IFU_u@`2=Il>J+Bl1r_EEX>CIo-S463bh`+bn2cs2JiEymY}oJ zls_p6Q4xA&nZS}o=M8IlTFvB`2s^$D2vd7*vl5$+$1_UK%`yY`B*=m(Y0LLW}HwRx!+$LRVknAwvqvX*+%`6(bV zhfs|z$0xtVJ$&_S&>PcNmX%?J{~$tii(^k@pL-=~tgdZ>F*kh9A8vd&A#cv$^cnAv zolFZhNB~M;-+u_f*Yt+Rh0;L(uV2agB?1^Af{@;k_CNms6X-ISgGLEk}8v}xKkuoAo>wv$2Qj6-8nieE;ESUVUO6vJ3&$NOD{Whj z8@LVLye5hkP?1DGBdMAZcdp?T6TRicXWtq@aqcFH<%0N*M!a{^lplgD-(NsQf6D$6 zaZiZ2^OLIC=H|8R=3giXe3<|Nn4k?NRRla4Rw+j~!sm*QjEzNY47VcWQ%IQ|C|+0e zr_<<}>y-Agr<8U%(J*ywCqp!q*=LBs(t1eyQ-l{z;!v&7tyyAOSZB>6-(3k;GBw|r zs3kHdYn}!zy+(@KFe9_6x9aSXD{zl(gMqg}WjXTJ{h)o26i2^Tptxy=H1FopG#(AHt)te6Vhxi*Qj`dM*$p= zyEw{&PVjX(S?jPa$^B|k>UU_polOQJZly}pY(>c21{8Ncr~}T)oE`N-v#zHYduXOC za2HmN?x^YCy_|F_`K>EZlaf`0vW=6O5#)BfZ<#5j%lAI2;W9htT2Y-Ddq@n)>mqJ{bKRqizg|BY7VmH6TZ1Mpw!6bk&O zXX)B0s?S*ZrskikDCdk_jdQHaPKgNlcU6DeQQ<{aK`(w~Qmln97@?2F(7*;Kl2n$_ zmX+?I^cwhG2<|@jFV!-SsUoh?d_ZNonts0v*z<6IMhqWrCtZyd3eY^k%I2hQ${aVj zQ-Bf$jrjUn$~O>LI@2YzIl7NFx3k{)*}%*X$e5pn8B}=hksEbSV_HlPE&2AW8-BbZK?`*8* z^B{U~HrNgLnNAm+e1Y?SCss^{bVYpB{_PHAz8ru990V~}CZf3iwJDNnD$kVtc2K=gBhQ*54EQg84 z$>U;ZFfiO%#bE=`=>4_5`Opr(c5pRxa4KRb*XOfR5Z^_sbHpVA6xR5x#vk#w7x-H5 zo@|joY!g3(=X(1;lwM1|TN;z|XLaLLQPwJfoZ%G@)*;xlp_%9OVzeGR)L`dHb8v!DL0n{dS1#Sr;P%^^Vqf7aw-V3Lo zWn+ywNXTCWN!a-nG&`UOqi4W}r{8kOFJQD+_Hug~-!aR-md~59Bk>k2==ji)O7y-v zrPHA;v8?r8{Jh#Ji^{7%Qs`W=DIDdmL|tVTZp`JtvHEZx3p-*N-jQNrfiv>4z4_Md zV6|3Ixl^@eP5zVEw{sLGcLtYF396ybw7QqP7!QR_os1u0bDVwv=WN5;-XN6W>?lJpxY`os zn>RBgeE+r!%3l^B0TwLWycYr61!@uVR>?+A^gceTZNpm!B-5CPj%(@p-81>8`MK&- zV8{>jwx**_)dw`&K%Tf_(-2iT_Ci<%NNh|vlZSRAIm@f_rem@46&hZ;J6TX6XRo+@ zCr@d(X!!*q$RlK%?MfJ&#`fFkoWQd6+nv+pNNYD;;wCk$Z1Xb2^K35nrmif<2A3WL zw*dnU47)!~Fa~o9Y>GF{*fMbv^c1~uE>Su#DlTj2*35ki^V<{F-M~-a$oAc@*UkcH z)n^>!i9Fp+jKE%}n=(682^2_vs8r6!yOq8s5kRPK^dI&zD{`Y6dqoWYRluwMr2q<` zK!%6s6rg}!_$cU+yffXqfzm(yg@Hs@)BbGP`(|TkfN)?d8(hgv&U7#)V*@(9Vherg z>UUGz=;wg*3O0orQn?c({ZB5eUbFh(weN#!#p)KLp@)CM2D(_=fSia)nYC&XNQ;O3ltlNHU$|^-Wv;X07LF(1a6^U?p3G zeIl4Fw@(Ap)`uOb9w3L!bCzDXLnu$u#ywC8o83D$)6{U5G^Cx89AxzpNr+%yWyhpV z(A0Pi#`6@cSfrFz$XHHYEcR4UHJ5|qLSRye{0l}P?Jot;00rTU2ML=Jns22Ktuk zO_dh$s2j*hXm(i{9qF?xi$4>0{A7f(4M z?in?#H?>vV!;ix)vF@4BXoptZQgjQ)HYWF~E{|{Q`k>RxOBH6Z&WAI5Bcuus3{xEM z&dY9Ot|;p)&mE}kGz0!XZOI;v`ZaF!tVuLt?(8YV4EPDI>ydtI6%04yjh4TMnBm}B z5=djxj|+aoa^?E`ivqnbd>8=!QTc5w;NJ%*pRQUee(>q|5T*m+%UzIB)x;k>nwqRF zo#87Ak_;M*jxo`j`aK9Fxz#zxF-9|13lC(8NExg~MB?X9uYJExsks7*!o-?$(=#D# zZU-{kv5y{y=yI7NP}>G|Vwrgjkz;K7wJ-|ai3{qrw@067h5OCbKr$nvJv)CNPX>Ot zKpU?eZC~Yi`9fpG<+kQp@Ba4SwNnZq{qwqT2-6`Z4O|V!1=&ko#XlwlxCB`8A>WS7 z9C??t6#Wr{*W%_attY62>3&9V3uqa9i-wOIkgqJp=KM>_xFGjg~`3EaAID&*!swa2hatOJZo0mL0H)w?;iqblAE4}(Zkc?Pfyam^J|{8;d{R4A zE473@RK>j?%9WytkgPoE5zr?i0p-aiNCkxceXAyWUepk@d+G9V%hn}wK&o~dR5(Lw zH^WK&UMo}0&R|@SlxS#)$ttz=0hd^(_)?VNec<}=L^gZp24AL!mK$hskd^K1yM2F= zjnYRu!;0Q!2cUpJuhq+rVLd|6P-*b5yTohuWeE;oY4iEg7UUBe@Ee$?zmSJ78%Uhb zUIBG_zJE?T5a4{dG@@bJnSdJKux~Z>5_J`_Q-?J{O<*kN1v{ z(W7*6i8+2tA=yc1(du}xk$~3NNGwZrb-VD&Hn|X>>N_aVCnQX|T^_0|^>3#jKUy_l z7V;6)>$j@hy^Mj*8BP1q!qBZk#g!bkaC6T6cMGxE4$B{+S=~K5w>QkSv2O#tQzAA! zWtnEcD&6TlrR+IcEPm6mE5+(3uCus=Pb~3g_5Q{UdB6%$;%*xjG^kv{KS@AVUy|Sf zl5A}{;{iq4m2hzLEirZL_pPl#rh=7*WR!zhw{!5`bS$|O+8KuoQuG#Sc%{14eGE^< zYSgMDe27KNKap=8*)ht~?GxU$C7Eg_c@HYQttG$dtYJ?nnH+S;RFt2f`-bt*_9NXX zl=Bo@dTKaBE;5cI-%ckEQ#O(nScZUTc(=rk~CD3P$;vWs&#K_f-_vjBes_)NsOb`)&8&U4MxP3UMsYOSG`zh2wL5 z)f*k@hXX}xO*8D_t$#JtU*x9@mzwMFpgLUMAT983O{-+X^M^5PxXR2J+M@*``G*^1 zvPt^Z6_AFHFn91jj#Te(9I-iE2XBq}1Y^zF;TXJCWMk47Q-LW%6qv5@1bE!+Iz*rJ zF?2*t;!qkOzk?k7i8MHUUKrVX@lyXENX>5Gj=8eFj$Apx!EBx1htLuqc>zF?Wnlg_S?`JGw_IamvHn>o@vN z!qV}A`HZB+Gogtb&xORCEd(KPzD$JcLI#Iwf6wcn^S{8Ys~Jk8V}7?tC|Kvs9J=dA zDR{SQh36r&-SvM@#7$ssPNIv85;~A6E$0qB81IYOlomXx-+L0bFZY&T4Wp7F4<0t4 zK;371eJ&{K66yE4E`PIaGo4tYENC+ht^Y4XT)$)>0c6;0uKEKWIg2?L6t@nGEk?_@ z;>sDGJ-;|(%KX(H7zAK7-UlM0)U!=v>`VPGDK_OBC!;Bp^_Mv3p>KetSmEZE8$5+X zn%L3!f~fVbSiv4my0#TASK~$NHy|wM#nG`M;IiUZzuQHSX!s)MM~9ll%ue4WUrnJx zG)nge9T;BjlJX&@S49cl8n!_&c3b4m{7!Q1cBH`5{L3n5qwbfbL7a1*{YTKV4YvOk ziv73$)cRz-EaFnMxC_EZ)iNn~%R)cmM>VIs7V7AATV9!y`Jhm(<5i;ZnDd%HmDwTI z+9l2u{duEe>NV|9tGvwq|8m0f%L6jNLrsya;Qu{x)P9sd?aX&Osjz#GI7%_yCzJ-` zURu})=haAaIp*o!bi$Jw;#W+%d=NOiC}8dyHXhYC2oDKyaI$5Qj_y(;`ACZWEp`Bs zv#Vu!LBQ^GJ?hbY>$8~e?^=VR<4tCOnIqd-;+RQ$p{kW3xt8ZlB3z*-txXo5suJWw zZs#K~C|!fVIE!e7!3r464N(g(h`qTYD+hc74CZ0>voX^q{=_8!GRwW2w|x8|TW48s zA07rP4%X%}K0Oc@40u~$q)zxIUZCSCdV#5hGb7Sh6Da<3Y-rifuRBvh+ktX}y{56# zTuY5i=dMZmu;?v+R3f^~G@79JpvU)-S^9E1P{xT4h79L_S>p3$2?b!ugiik#$S2m@ zqBDhD)Uw_0dI8y088#v-s?`d)0#lAWZVpu6zauWETO>1(r;gaU#SbP7)TaY{5)=&e z2=J2NmQYE+dvlo>K9jSVmAW+C?GUx-+&7jaG3mC>hIAMneFn#Tn%U563Pd^((|+o( zJn~)Goqb{m5>r<1y{Baczh11CF)#u+<8~X96jn;9IK~W#xiJ_dBY7)?B7R|XPHPD( zC@+Q#$Yp3AfbZT2EfXdSRle9G?ExVp8$i5G)BCmH-knmvS{JT-r`C+LXcR5i#c{`+|JeN*48*1GUscARw6Ihp9pBfwc8&O*<}ggEDymVL4-34) z*?Q%yI8BfIbD8_sWk65lPdCv27Y($A z$ez}s$@_$)jOgJhKeMiaq`dXjfrpq-CX^tQc_dZ3NaTeEPqCP=(6I- zwMu$WT9%;62HOLgf#n0wma48P)!tF|NL&gypyMT{t|F4N064I-yat9Q{K9mz$9&#& z_LS2nVz7R)%uVz&@JUE?LpZAGQd_SWbDNvg`##27atS_CgzEjE%TGwOz4%`Z0~{79 za^Rr2QJ3o%Yccv$O4f>w$4{QQ;eHxkXDDk9KY*Zjl(broY`&wk@HFBFJDl`@aY|$* z(fW^E>j^!cHV$6BZGQ^cR= z*5T(=?3DW4XC-l#tKNe5n146puGfm}`fW0yG8;~^Y2$jLGfy~1te$%ABC2;8zryRIc z@LpY(h?QZy?jC~H-*OHy<0sq^5XV$T05$CBfS@L`MTpTA3GL z1+h#Gw7~IMJ}(qn35>mktPU7$ClKCpO!sEZQv$^uQi4jXT~2SM^Df`B8H*Ny4!bex zKZMAX!9*)k4X5oyjFlQjgw8vg@zq5$bOV$bcee-mGA`uyPPRS?s0&DzI91V5@>FS&w! z`MvTdZ{(c9BqLclob}(ER`i#}Zvcx(=CGz9pNwxF$Y^Hv*Pzka!AHpqRfs-5Gx0Wt z!`*ImibVSw1!Yk8A92L;xS}?$X2>Mgxxxn%HVjiUpM{LduoZBy3!8n zNo^mGvD_&BupikWGH0$QAfIVt%BP%s_RZW)?1Uz$*h!(4^xvEQ_&(%6+8mdKIEb*{7akjp;9L@*5`30 z*bgE|wVF7s*Fu|tEiuP;QRo)VL+KsZ4Jz-%Cj3ol^rZeb?wA-m6>I98%YmsY9BGLM zlGJ|bJ7LY42XW-aN#!!yua|_{{dHC2(_j0bL=9tj-g+9~Rlj`_Fi6=QtNC|mpbmh_1yM%9XU zv<{Nwwn~6XOP$}RYiWg7Ed0@bgDQojzD*Fp)Ttbmn)k2Z;8X7_XxM4&OK-HmkY^im zzI6vpyCw~Q?j-thyrXKt;AM^d8y3x*{N)c5;BUv%DF)<|5b)u^Npe|{O%XEl^NXz4 zWh&^;W&Q25Jaf6aUPy;>nN}5zz6kfG`adX-b7r4ZhE7m-CL>oop>X@HRF^~dCh~1q zv}2fwC^|+N?1ws1%1bm24YIh%^Si%Lqh^hXE!-;oS56p0iB5{VFS^JUY2oc+Yt6$} zA2cyivEq<6ix{KOa6L6F7WOz0m%ur5UhWu9cU8AIeSr=?2Tw%)+&BfKt1XMb<=qsR z3JS?rcF#~(N^C&~J{_U7M%-)H`zQLBnn!@ZI27J$xK?Gt<3&^@oX!>{n0%>=IZ%#*SLQT{!uD( z$pZ!-0yL%T04I8s{VxyGzuaK~+$FE*QGtBoz5!Qwq$_mDpfC6GKUh`mOs=o+-#oUm zoSCNlA;6m7GW#Yz8*K$Rpe?eAj{LmK-|~LP2yieqFSvbGuZiXy$3jr%b`K%t(i*3z zsb5%_$t<#{XC3tb=k+Aw0-3>UI>E6Cg+ zaUlw#Dern$Z77P-;OePx6l)^JWP{x+k;I+Z*+7lxjOROrYAGoA41|m}SXIT{O9=V} z%t`h1_6)7BaqfK^D~x_CVToelL*7PRl2FH%7U=(RO1u#0YlA#R_Gu+yl|X4jSesV) zzgkn)mp5#Hx7%;D=D<5|6rhuI6}dv!CG`6rUWzzDdPRyT@0Ct$(mqKyO?EnEygHkhXiUh(r0#N&Jk0)o}wZQ8%%}*n_n| z_2hBRT9jr0^_{|R9&lasROr_bd>CcGYISub)Hf2@%$=TBOVzI3E|MME?J^rCA>EbY z!%BWwy5LEukul~5!qWxf*5FPWvllDzQv!R-1J ziPJfomfi{}aVpAi#7RTK{VPFy$NLj1MF-mFSe|HrvpOEqJfOP9CI*WK=Poyd%R2@kmeStrl!no@#xNlQZt+wJ_5;yfTMG|`Iq4oOd|5Bpt3mgvs?$fVW04M`3 zx`dSx2Qp@8M$T4>jHR_sUx(DeZ{aH3=1GYJm0`2`)iC)Q*0128z$-k`c*>hseI^md z;zT%cAExBLJ=Mp|+tV4U?KzNJv)=fflL{R+W&N;VBfZ{Hv{YeD+kS5>y^X&&o!VVt zB-HIt170`IERu-W@QYKM#-cO=1)HFk8$`w037P)sCucUNWR`WJ2wLWh2&jTs5dpga5PpMjprF9Y}h1J8JQ#{aqJ-$ni;l#SiY2o}!2dWSxz zRfc1b_h1gG*2xjg=-!S0(aQFnVVzCjDx6%M$6k?oGv!b$R7$;Uk+q(#7w(vc`2bTK zrZGd=qZL6Sj6B@QjcW5cdqx6I}*)=X{`JEz*N0t2EBnc2E*wga7$Vy$mJq!Qj~N zvK;*zmp36NI?@+h;YUckTza`T4CEl={%VI1yk-gAOEx!l55u#tV~BuojyN;gzr|kr z1x^3}?{P`o1YUUqei|OAL9jQvf6Wjb?^LElP$7~J2${W!Ai`IS_9cYuDyJf7m8|MO zPf#>M$h=TI`yq1l)6;q8$d4x=0zB$jO5aJ#RlO<8kN zvM9J&sv-U#OzX+QI-EJMOLdz_=3%NHCeo3&nayr#!Tp)(z{I7B%DpZK2!7PQiz zG^>j41K#_QojkG0ADFatgKQQ5t@Fk&WgW2f3c8mURW>Ic5hBzu`a>a?}I&iuO$7V@dBb65W7Le>Wo{%96 zgOBfGWvFK)16?p(>?(rzeRJXhSKegK)Eq&&_Gc90;kaz!4lGrPZT{Y+Z1umD z-ui`31V9h!$ZQ0(Q-K=>)tx1XsaP2?$RFGt4$q;*_~LXc`zYrktp&M&Y6mhMELuH~*l^*hIPQYxC6NQBHK@pnSh5K@zGuE~Y)uM#-x=fgON zk~X#i?NQvdD9QEA&kkfjag{_9mfLp|kJaW)gVy}hn6$_gd(EF%J3aQe#fa$&>Gtt7 zz)EBk%^sDthe^POO%jd?KvJI3>wiO~D=v%(giVWKz#Cm*j#0!wF(gVU{Kko9SCbWO z+lE8l<@(tX(UUQyNm~G(IWY84^KFkx_&sG6C4gmlyfEeC)f@i57J53qED!@M5I<;5 z0J`gEHKtAY^3zoh+6!7dE?lw^KO#mHL_bD3$^v|--2?|et#P~RENMOaFa_JD$AZYm zjvp{ryGOXIb{z}S!dsawQPcy!(MtwvgQc`I2aV@m#?&kBsS>Pc3L&zeQ%0Ib$l#2Iru){FV3g{wneg9+jB)L@=WoTdc@)- zloeIr1&2=Ikft;>-~$GJEKdib?b-wz)KnU&gw==iZ}oS7i68-pFcU$g0yI?@6=pQ$ z9uO}Ek5IJY>f5mz=kJZg;|S_pVhmae7K-XH2)`u36K5`Mj~P1%qO`B0i+ELZZINx z#(nD1bqsW;;S@y>=SzToCsA8X3IQt{&RAwHn9zHmdvB5`lWgEV_MI--2zD?XCL zO6PU|5;aeWl%hFwmCl80$99<&2BK-K@%_b8oQmmBxAtI^;wd{OfyECxK8m4Bn{`W}OImC6 zz;8SlNqGBkvFC!Eot&h0RByg*nT=c(8ywZBqVZ$lhYko;%R#6eAjPc2jZqwmmNx=X z@%HtE^*8CH1`jsUF)=eE=iKiHRIkk6)a>2E?rXzS{Mml;BI=pagZv)o(vJ{01TEuh zS9Y9XBmQ5Z8)*2;0vW&pp6C%PU_kkzGJKhby;ui@SodaPqKmwVIHc5fZ}%oVFeyB5 zt*2qyTVe^xU=`&ph};2@ao!X{GczBiVX#BV@VHEfO&hRIumpajYXdFU2X=m10Ymhy zY5ir#xq&0kq3dzMT-JTO?vgu`9tg$APPd~R$+13x#CT9crkNnWyZ9MAvZu_vZ+Z*% zJuw72VVU)cL(#t{43>VJ$37<@+i8BjscMECT%bFSx;n6?4jdLo1C!vK z_6|-)f+q907c|_uv8>t)F{OHeZu(+2tza(?NpVZdRo$nv^HV^HSRMK;tQmk}Xq*0> zs2_VLOxbU>I(?FpZp>)ZTuXVKvbXOo{ngIyWlqHTi$LIQ0uk?^)e+U>SkrKHN76#n z;7R2^D6m?Gr{JgNa#3cbPrR*Xk;ZkzRqt-6gGx-#b@GR*p?-zWbOO@%f8j9oWq|@< zA$I=C3Ha$hrL>&||MCfo4S9Fi{=-|=#$2czy+y=SUmmx&|3}n4u-6$a4FisCCk-0g zwynmEoivS|#EnqIHNCgFvR^C3r>!mvrVmhiD)j#=17ac&gaq@7C0lurCr@xL)x-g@FpsK#%eQ z_Q7Td(MzZy*YyRAPB@t(HA8IpNr(giSWjihiDcLhdP z(gaF^NV$Qx3Bk|T1@3rP$!Boz)v=%UTgNE-s6skgKkn@)yGG@!{bNAPfN@m*Ya%fF zWkLZkscLc;`;SSqak`FJ=vj#&cEYQ5SQ;*Ob9JRW(5(;L&Wh=BlJAc+9AwNewGsYi zR`FKBNvV(DSd9|gMpvit@XI0dzccU`%ER0U7)*uV(!Kb@VtCc$3HrvYDQ%CUDSM#n zJn%QF$rwr2GwAn>674P()fwkT;Sp@F?hmCj_oMEECB-}5vLFmp#gXPFbnyEUNxb$2 z8_+Y-TsR%Z9h-`&iiv0J6HrMv2jP`qW)ZAL9B7mWiz8s6z+OqWmJ?@gZTW|T@%L0I zK3ryn5N`OPr3%l#`{XJlsR3Jz%p;xx3x-sXQ?rA8>nYj0W^&{Ox-A8c6do{v-}tWy z(BhW~CBWo@xo{g`vf$NxIFjFURf@X9RTyOUBJmKf3{PlZxKI+Y&huEt5E9e2G?M_#pGx8~JKO4+O29T1N-_0skkkjoCCz_> zevBIMF8n<@vPel3d3{z2uFxSw9G}CqFok}~qT)_ceZL`A5?UBTb>Lc?&)9%0hQADTH!t;6s>O%C z%GXv|6i1xBtuDPf5C`hyi1CUOg&KqL_Dm~xb7_=@XZFM5kB()==^#Gaz3bw|8i(&W zJ2E3u>9~*$Igx!@*`TT9{JKKZy6+%jeK6_B0wIuUOnAWMcP~&OE>*H5Y8-`q9TD^C zg}aNQ4)7vf;cE$XyvX{sbhM>gtv#%2C4q`z`+`4z5$C+(W9}tK4vQ33k5s5dtF75U z0Jrs@Y+Ad_$eCqG7UGUU2=gVB8O%&^Au6YmYkntG@45eA?>Y~_-}&*RDebvNF59vC zzhj<_FA!<~h@uE1IAC}4f-2feG!yvg+C0pq0yIxdTWu%G4XKrmcE z`LOmJO`UgeRPxraWWimDIB~$_As)8hlc2FZuqtD`yK1r+S>+iebY;zEXkR{jmf)H_ z_q+_JzDO%>!jK%21%vJiPIuRiCu3vU*sp+kCFS!u?H8fn%=~e!#{=JoPHNb4;1%TH z@xoI^wPl#tP6w5!ED$)MJ&h~ym|Ul=t`j-R1SbOPuRtuBbX$Y|0zKBr9XZMWnvv8MM~QEVTkcUZzB}E)W`7{TZR2$SHMjWh2D0dD>QF+l0K z?>rv@_WqY4u75}J?^D@1a?74Y%|hnG7>>In_MACnUkH=6zU^C7ALuof*5=tLDcua- z;&shrrNg2HU^^1IK}=yFaLsTIIj_uoKMrJNbkEqyA@ot#v5gOJ{Jf|?FtvcLRmuU0 znQoUDd28%{vupS9mj@ldBTa6T6!`9~v@FGeHPKTh`!~;1Bq`po*LsBj&?}T8*a+Gk zO>)jpz2$@*ld5Gg2R#Y0yf-|z-C~OHPU;-e0i3+&*&N(sq9q9W+~%PjS%dP1x95T+ z%?mYgd-CMiDD_skYCqUfkoVb1Ay-IVa0<<1A#~WYx;~GK%b*S_1CLHt7Ku5ZJ#4vo z4ZbQHc^9X7grjhn!^s80Y|9a9HSJ0>q@GO-NcbFnNf*_HhFyox?9;nZjZw&iSvOnO z9NdQ487HE!_5EX0O#exuJ6?o3+gz|&80`&-X6(q~EF*A;?L{)T(DWPnzxpzM62c1; zvS+NfgK__glJhSP`u}`9!nsBP!xBL91Co&m%Xdf(Dx>9*9G@{<6LB)nTQ8YHe#r~p zj+gYxHn7~i%;u~y<~q3P!BQ@voLHvK(do;GAHFg5cm#BbLnu|dckYwa zG9j&+n@k`S;87GhAw#g^bXP<2N82J!3>gly`e-DEjf}rH^PxF1N4LMmVo`jV_vfd*B#V z|4NbTF9`;K1Yj+d$^VleP#V(HVx%{x(v@X1^wo^E=ljGU_Z6J!i$RrQa<2JK1gV!a z+Iy8e<5oTiLmfV!aYMaoMgJxa(*msJJ2FS>=`@7kl50#-}Xuw+$w-J1zGXlA;8kPn!S~G{B zjsoUl{oykH-wexin+#MWOw){Anud2v>G#giCaz%5yS zH{u!0y6$kY8h*QO>`;p@w&0Hcxswgek-O?ma-Pg&6^HyocWT#xjhWtVPrkl0CQkV+G<8NMcG+vJ{5ACNFY zFqb6eePwP`qG}nfze66tCd&2Z1E0(5?#d5EgFDilHvF*#rw8EhbhfW{b%RbSq8JAE zmvhv5XUr^`NFLN`P9{o&BOs|TZeiz4GHbA#-5;#DV-!ctI7D2p88{disMK1Fh2%mw5f`0+84L<&4)a4`zTz@CnlS zf9{QV5t72>#(>B6L>b4~($Z1ZSo~W?*@@lxdXrsgLK6`g6s@Nt|MQK+Nr|SwcP`LH z$B{qonw;ptNyOn={Z-?8qyA3>rRUC}$7npzR6e2GfFMn3Vp-GwG}fr z4THr^s}_NDfIY>2S;Kl;hrOu^rM9CXq`cHpC4&r41(4WU$XF%&f2_S0$_6%la4{y{ zoVx>Y{<-cFa4l8dr}IA(eV>pySWb3+aFt9I40Bm~stjiBxU@DUmNKy+`jx~nzWm84c4R_7L z#n3`7Yrb3FxW!EYyM)Gw`k*b}aiRiU;Xec26$vdajJziyY4J>wx6N}xqQy*QPQ8P| zS+PI4(~h=t;Z8RLxoW~LrJWLbkLNi5E@rP67FN33L+hu;FG-~){FQ{Z)y%H3Am}yk z0elLnj6SO!@|9ol7sinqheIKN94u@Tdp%IS5cfP{^dH{jK{r@FI ztN@SvS~)F1<0HyOFlq#>b9g-Xq5zDXzpXAgI>=emG0Qex8!FMA3hGEnXCPgKcD94R zd{`MlWfSDh`en4^leeSdQA|c!Odp%ivhPZb85oivjX9IC;JuQo?S2@657BlYgxQ@hVv!}lx90Ps+3_(T?ECme6j8|=T zFF?5x6!Ez55ET^;tct-Ts=SBDMkF{eIxg38wj^;(au@Mf;mXG!`26dm>4x~iU;|*3 zy_x`le1ZV}`9~s_$#2d}0&;%r@szU20jwY}fbp_b=c+WVU&%%4X&UA%o)cKj?gI#Z5Ind1kz3 z_3+5B?e`Fv_iMy&oRE{f2HZ^1kad^!!3h>jSyQ* zppTH(t*!Szj{(;^;4=o0{?wEaQ_dsYtBf`(wJpm{EYs;P&90Yu7V`apB`4@n;Yaok z#f}}g072he7MT?_eY&v~-$DxtnJ2qH@)jI8R6Q|#-5Ia%<-C>?#^%;ds|q|+8VH>5 z0>#*t`!AuueCeD+h`2XQ{l2!xDM}I= z-Qb$tv>898E*!>8knj2TSMOo>xEFH+Z=IfElqhSDJhcMasCy=0PNuJKo*!2x<2BcG zEHOO5zg|z;ClIV7!@VR^eej9VPDrFooN~GWcyG@1ftX@}3|x0uHdhwei>bX%$9z~m`zNWSq*n1U19&WM`2K6o;uZIWP$#ZMSvrO z9TQi1`9f<1==y+4R0O#WB4B_Kz8M!HczI3pr& zoq7>?l?wfB&(n2^I5d1Iyqf#vO63=ZT$6S<;!cA;#SV_-qmiAlg<_1;U`#rN{CwZ_ zP5na}QbIIQK4#=37}^)FYvp|3^KqMwpp#Z_B#N%fP(0)|X`fxfp86?AE+d`gu@nMyGj2J#O%7deNlY?#mksp zKJ_l)r0oIYNxVCcJ+=A48NISa)Ti6+ zdyQ9^Dx0T&r79507YQeTgo;}u3*>Y8%@ruw=lJYBK~yKm0x-W^*72^A;ueY7bm^Ot zP1K%a7pJEcwi$9)&Oy8o!(&G4;Gx}7 zaEyDq2rEW8>>%Qh{zpku1+GW!gQ8w%90l9u-t7LJ-L(%45d!QI8j1d1MkY<~5RlRF z>)JZ2@9Q}+Qr)g*KCUATh|#Z4_V@JlUXiGL(MjbZAEn%}>v2VFZcE;_g#-1t4^}XF z6&4n+!YisV>V<1{o|mE0M856?c4MW}3CrKMX=9K)6S&J>mJw!Qds4FJjY}Eh?3L(* zwQyY2{Muj!&CtDBc?=$ll2q9w^Qg>rs4$+rHTToPSqC+Z%0eO7Tdd++WYaJ<{9v;w zC48bdG2!3%#o8Q=3mZ5ml7ekNosUND@=a9C_JPS~>vj`mxs9;wn8?!t`YsVaD(z&BS%H7=J zoL3-W4QZ?XbNlC!%M@sXVePGSHT8-6=RTL`$$D0>m=rM;IG?@C{9_ z$XeTMlf&QRu|?S@3yQb7lqW0cF(gpYD^+r1rF)6mp59TPU8)p2$`vA-m_0g?$Bzz~3=DX@Tq zNZECV8$^d~hA-o%+y(pUWNQYkOFT2e3;af1&m@=3pEu35` zXCNrlNr|Uh9cU`es+`|&p5+g=M$N{bu@yNP9W=lT`l}Z|$aPF*zfIz0qNZx8X2=3E zpIBN58r}E_&c{yiin&0vdbihc=Dbxj;`LO!uGZxM;(8 z@pE7=ruz8QHM4l&S;CUX(Q`22V|E{($O8^e@Cs1^_oe5a8Vl^4_1zpbL?wP%ZJ-bC-kmGbxzUol24V-D_ zZT1bE9F>)zHUm+@(2?R_bw>DQ!3(fR^a=<99N7Z(#gQ)V-np9oS!l_FN7ww+xnI90 zG*-cnRDY1gwlE!Rf*v3x5Cl6X`L!qqZN6n5;Kq91dpIASe~;D?T)A+W9Dm(fllsb1O0bB>XYWuFrb$kIrX07A^ncQO3os<#2sJR)YT(UT+OWL67#KA z#?91D^NWdL(Aki}qOhGG<8rbTg#tmdHln2wFPZY*h;eD%f0A`4j69}q8~)d{Lh)t5 z4=_Mz|5OC|%z1Mm*MatpI%|>Kz$i6EW34p^i-7nuuVj%?X-E+zK@$pjh0WXz@vYG3 ziRcNDrL>M>4CYMVlbv0a5N(m$;sS>|=B#ENBDl^i2*;^rZ(xk_OhNrZ%pw!%hiz;NKY@?rhf5Gq$l&G%1U zB!pM}7iNP+S4p5VO43YMWoL8oTvL2nQCpy(4Z-xE%o*u=nDa2Y4MI?IWzBMAU7Pi} zN23#=t$pilf*(zyX?UZoTUv z#owqJeq5Cwa;sV4CLpoqA6=)NnKj;0Op3Jbu=~6~&T+k_3>v&U`xvb$v?plSD3XK3a6bpeq05<4R|F%q$SwiWqvclwZ9SzQ*bEIZ@_tr~dvdd$GyOP=BVvwU82@0+; zX(7zQK3R6N&Jy-7tI>P~PXG`+Ms6)UK&urtHk`rHFGGb(^-Q`p^Q5+I7fUV-C?F|2 z7Ne@!ZywP!iAMob?|{bn#upcnya-lhtRmC67-m7*-aMr;ApX((#{r7(uC2Nwi9xY|V? zF<`F;l%82rW>H%o6vVif3Ufg)3NXF3#JO67Jyf8|x8)2GF6HBlRhkb|F_-IJjTG1E-99~`~)Kkf9@?|VT|Rs-&8 zYC}f(rJ&U4x#)~8d^$KR33p&>7F8Jm-|qH|tPn8DXS9afx|u4k&-Sc(VKUb|xID>e zlI;9Tt-jwWQFRL`X81b^4P}BZPm&(Nzs$w__54DB=NIE`^9NjJv_f%y1%hm9)t0SY z&3V-0Mw>BY>j(0wv#MbGH$%w*{D{}wId@n;S;C>jvmzgO^gC@eE>nIC|Ghr^HUCA} z>iERPpQZ6X^qQNa-!jx#FR5ZpnO3m8W+^#@&jqx{pmS1RQsavk7oVA@-s|!VDU2AQ2Y*N!HN4m(Uo4lpgDGY!~;2@#yIpO zYeqJ%fWSl5K&G}dJ!tJ-hz$P(N&`ebNYFewh~DQHCbv$E;yksqkYA1B>mePkBkf~D zS5Y5+>H_Ot&D;+E$@Fjgu>kiMiU|f!!!jP2Jo!iwhBcpPjQSZ&SY`XX_u0#-j9peuTLjrxbZtY=^FwzK4lbB=+rLF{ zvq{)s|I;5iDHXowH~9p9;U2iMbuJ3Q&e2@>MwYYlSbc4Ov;1hOp3)oPX{PYzr@hg} zaP+ObVH>2+7>?baHB$qsO4txUthLiX%AFVegRf>BU0y zji%OqcCtN^;fH(jj5YSBlKEMW$BkBk%NoUmQdpq>`fFVjck9`4j*9i<<7j(wUbqgV zjp@ULA2sSI23s!<@-Kbye+5bu5Gbd?=t)2Y#!&uE+{U3S{V!Z37r$vZ4PDKMr$8BB z&9iMkx7WxybmF5a3|h~NjH6Yjm?K^hq3QJc4s`kz@UfYT(Rl7%OHF!YcB45p|4j5fz?P4%r?$G1U@1vAlgoUT5q-L?#uw=u`#KZs(QDwX%`MfYKKR zNTC@$1^(qRp)UwA00gPuCOqIoZiT@bnd2e#skPtcXytx|QFsH06XQHAR`}1!gwi?( z>pfa8Cdx84w(o&9U06ZHuPogw=NXmy7^1a+d-)KutuQNwrofDEjRu5ivc=9P*D_9% zkvgI8B}I~!JNI3N9&(^|HN)g?@~?zc^ZXC__}`a*9uN9a@K!(O8mpCB8|z#A6<)hCe4M}~t?uRZr(Ac& zt0?tWZ3$vivB^ix!}N37Y^l#+GQXm2c3%veVek*$Xic-x%A2fchGPNpy66*fy|1rM=eM2#@^ z&l&mT)VY{{nztjq8iTez@)EYtPqSa1^FaJ4zjnIx48c`ME@iMKiZ|>;_?2ElN)c-R ztuq6X`?8P(SkwnsYl3`^yty%2bPYkvbz*^(fqrB09ldnrG>~U6TSP=Dpp9qSDcs8T zpQGo6Wv*RzKjpEE2GKyp$1nO0<}mzWBhwKHl<+NQ!V}`#urBSeX48%QOq)jwtR5_i zF@61?Kq6XBuQIBAUpaZvBY)f_viVM#tpdcpfY&oV^+|Bnp7U|LstM36wFmi>s2LAn zhO{$B5F&)(F-_4h@xJ@>*rBLv_#hsH@SanUOo4EK^78wQ!`Xv3M0lnllO*AnKop_fs8;gl zo*J_@iv7Be2FrKfcg#uB$l6P`^tGf}(>i6y>w-@#uT-k>3%ZK+p4anvN3|T{c zHQhe1T`FOZ%87wmMb3N9E9?d{oUo*4>VsL_ zlqp>_HAo_OW9}H!mGt^Icx%%bu->+_7afh;WPcpu59t$WpzmU(&)!&7c9G0}v)rSD z3GHRi&6rVQV2&hR*o9-c(0zbcw_`qeWK4M-UzQMjY?2=mb}T%)a31 z;^D`Vrv1x_3SU8%0tC75)r13Zc!JB=J!?x|YIDXq5_ZK~^54At9w(onmX+j5_KJb^s4kCX#c5^}8o`Pc(4Q$Z$lYenEi=9wNwg-a1 z@UQ<@=c#;QNCPk=lw@cD-+ppd1Mc(fd*RRhE+&iL`y6t%c@Q;2QYX9~<#Pxw=c7@z|#cK6NdQ_QrA_?goL4TY?PqNnNsQWyT^L(A;784r5vnS!>+Qc5 z0_tBDG60MF_ap@1_qRm|!gIpM9B61fY&-TRbYj73Qkj zrJ`_$pH0r#bU$G>{KOC96NyB;>=kMu8%EwC!y~4bF$XN67Q8!LL*+zycQp-+{r#1H z7I%D1A&5h*&!JxyI{!UG5$HblmeFNig4L*ji@&&@+v_3P&#<;dcVI6Q=iVUh_ok|+ zJ>nIKKl|7E1*H82BMX4>%2B=q`2>EWgJ@a8YD8M%Yt37qznq%z>381fJ@?jED-j=Z zGvCp4%x{m8Hjv_(GL+ipoDixb)1Q4wPn|H9mrj<>>bvsL-?Y_p?{zMM0jfk94SIQ9 z)|3|a_}vKtE$5Aj7+~RY4TXV11U>5!RYHy)<8#`c)O>rN(qghZQT{{Do*CuR=&t%w z-Ufqzoi`DV8GYg&J2YTHRZA1fbz{#xvk$o3KmazcxTp|!=Chu_6MhqaE67;Lq{E3$ zHYB7dDu20$!jY-NhUuLQyoBc|e4nW87HAH_>Yg9~nIK2-QYQeGt5`mJ#IT&4jg0A| zF>(8ww7i~{VxN-mluUKo?;rmwvGl%hmt9yeFY33m4*JtOfrKOx4mea^`wp}wzvs2iAp6X&V0)*@99n9? zlwl}-hZV*xk9%NUJ62-+k{zhL{dMkBx;Pt?9ZB6*Q*-JJQstH1`F5THxBsl9LH$G* z#k{o2Uk8l(NZtHG_DZ)9v~WgW<%lZtcJ4rjE-nX0p@CaTNstMPw>v!snT)HaklZ1C;Lx&o7`f@)Rkq)9WQCLu48#@(71VA9cf z`2fT-?JW7v?xZ}k3a4%x^t^ekJiQPpCz~z(k=HKD{rR5C$WvL*a}7qh0$e3!8+=4` zt~*CIBOB|_gVCRRi%;`olF#FD`bZ_<^c#p)@8REIAMxPrl=Qwc$}v6_GyW!+w^MkW zH~8pRtF}Y#KABpgnmxoOqXm{^1`T<4W^W69LZ3J+P+sHYpq3}9BFZKuv)P9&Ed6kS zM^KW*Gki!h*oV5kMQNy}oxywMX6``r1^$;Te|31^|1mHIyif-j?Gki_#7LSnRC?*)2np_+W&Y6# z<%u&4>E4c~npSp?{(79|n&F?Gkth^ZUy0Wi36WBfJNGSZ$QkZnKx2DTZHLX#TnXrH zVg*T96+9oPoLD*k!hlE8aSh3;YD$SQQM;2HJsLUu?{79e zu|V)C?s}T?rRoep>~H=2tVOg=VD3%4iGsNygkeN1uVcN$Zgc77L}AUpjq4D`FhMpr za>=0fCDSCPKJ(UFmY##7{>uPl@x`D3V4#J-0Rar7&~j;0q% zYKXfOnXI=lpCdLxi#FY}%kPSCWkA}Alx(@g2ZAo03x?Wb zP^YBucQ&y;(av-p(_?%?>X{+KSEIbm5jd2ICSGH5L3Hu(d0SIp9B$u}cI7U(&W8SyLq~V;QyPGrz<<$aXN$|HTMiO} zEY{0$D6UF}#Ur1`lQa0QMz{5sh9W>C5bg2|aBTuqb(3vX2AO&)f*N{KHZHP<7RsO! z4CL&%35W~DL~km+Ap9SYfV3`|M#!)tkF710s8z2T)(FOtv+9S5$ccfO-YIiQ=dvY1 zl*OjMVkdeg@K7P&2rl4U5pEw3&oA|7nOl7ra|8655d1Orej4`cc2L9)nP87hm2U3E z%_z@#VVyeD?_M2dFW#uySOisv zk#WlZFGJXWAt(V5);5l90PhZ=!V`q=%EVz}ckkBjD6Q9&R5r=duCZz>G-XA!eyCeA zawaMp)j7(@f+{!dcLG679q5O?^0}LDCe?(WW|4z)MRSp_D;Mr~4-%F#xwc0N1@oB;+@Kxl`gdUtJ|F!7UbZQMySxg-BG@o1lOZp<2B(yO}$vP zIDEgE)QL&-T&(ok4$g9TY8Lvs2QL0fAx3jmTOWkWq*EB>nAC3!88jOua0xF4(1+_e zM%Kha?X02HARu~Kb{vK(^jHZ;fXBh*GpTCsRqa=}m4JYO5g_reAxFUZ3qTnFusNi{ z0~pZ;>RDZStcjz1J|5QDUYojuQYwI5L;>w1i?{Yqr@qkJn6hyyCynspA8_gvuY6{4 z8QJPJdEHrbuRiJf1eeMo>I!VHKykp*f2+Ru5mK3LV3_L%RqB0LVL%tqW>c30XT4J= zg&ERAsyfe_tH;H@yKwQC%P9^^@|P8v((TXh;6q=o8h3sRoEiV55Y*ACTn~E?Xi3ut zFRJesX8PH&!;|@kT6*_*UhxwJ37#K_cbDTWs)Z?(LEl#l3%YJ13T?I?8iss+%o0Z& zP6(}FA$7lO+A;Z6TRHjDh{T=8j!`Oz@&3ySZeIc_00D#byI#PL#APJI`G#%&1@vKh z#7H)FCw3T=thc3Rzb?Y@zKrI6O1?~Zp!`xNOfSjFpCEsg&j||S^o*L`AT})c%8Yvy zjwvUZ64W>~|5Vy;jA5S$U2t*%i<(>xa5tc=QA-7gq%m&ncNoQ%+xhR*A?26Nm$Ii7 zvzu>zY;PE#oJp8y{I}6j4Hy0t@hQA_RkWhd#R-e6l8=1!$f5Un{RTNGFUecgf|{RK zpJ=JVQfRHrC27oVQ(D^;)v5lqa>OKhBwdteHfq$M6@v!N)+<8drK{YRfAfO&qh?R{ z=#@i3Ml!SjO9hrh2d{H^{^bMDF9%hC!^Q95Q@{b>84JZP?4)RUza*Sk4?6plU!@lo zsa__bzqT<9ZElyZ?Bn6ngDC{$t{*G>BqkqL3X-7lMmAsRnq^WK7a`$0{0sY;nR)No zvb7p8SRSBylxOY*&7K{1jA01o6KSeD#1wAdCyEb7v}e4Y6eC6v=s@`GO-5gPQ?GH^ zavL}d)E+zpSrJ}Wur?3;D2Mt7y}V`1fs3oCGwH#mmjdUmi8QL&#=OxuqUr1o|7zn8 z9qa?1vKn|Hkb!qfODi&ZhfMyQ7ED_qu{Rk=T4@H?PA&ha73}X<+#+KH13CAQ-oGWY z2_NV~wQzu#D=h%OaxW$>xtf{R^42@eL4(%9P!X-;1o4ySm=KiA) z2>hb>381JpXBGv#jl3)o{_)x`b)QCeHI=mTM?M|!2c*WwA5{{G%^Dv|YM~PYro&1L z)ODHemrm7e8uAhAo&29CjAaKk>7Bvip?2L<-u0mx0vS^0%i>Pi@GLXmKPg<2JPbz6 zeQ(r27u{#`s_P!m9^{OOHh}O}ic+DH@=V@S_&w&(gwD}^N3snz&Ri0dxIQR^Q&y7G zNVP_$2AOsta&?+G2jozs_6=!QGnjUCKUnV+;(Ua+4iq)%kO_4d$^L0vsVKDlw z?YcF|y0FsjjCYok81w|(3cpy#ei(LrRrwbX*e2T(@4e)BK*HX&*f9yFiUlvZDjYRs z+t1jm35g`sBplAqVn^ENd6`N~eG7X#Px0sk`RHq4nXr_#qCAgzIVKCIV51}`^-LGa z=m+Ft$4wrGsXl$PPLea#z#(Y40XHJs$91+NM@Q6y5Op*FzCgy&q` zaKvjDP@B+4VFKl8jR~nCn>22xb`o>3}}j*Qz=eM+LwxQR2OsMpr;e>s-yRm0xszaAspgoIG{@DFFp7hHjNE_s51Nnen+|Yx za{7BE=v|cg8B?iV;YpVMW$gl*Yl48PaQFtyl-wM&Uz*>TnCc<>&_)tqN~=o^nvq!3 zC!y0ns5lfRp1<~sskef(P2rCa(%a*i!oRvH<}2WufPi->{}utfiXv0x)W0KW)@~(1 z6LwiFWXAHhRAlLI((TXawZG;Hrw{!R(;XL`NSjn&SF8~%GS2y(ySm2n+uVTT?_kD6 z?po{WEZPY9PAC86#J>)%y!M!Knm$CUyJ*enIF2J0W^#fmwxqwHhx(w#%cDzmi&xP8 z3T_7535$t$sB3pl-jZA7{0VfAQ3p#rOWUxIlx(;F#-crf!O!*`kC4^Yy5i{yT^FU5 zcw7#+)l37el;pJ5>~9lk`3ULrhZ|PF3cL=>`I*HZ8&QzriXS3u>i&m+D8pYWPS1W^VgZRG|0tsIhS^x_)^DzgIPsBGmpuFZTW)p8EDAq}_ zC=51ti0MKxmHXRkt5wN!k(5tF?Ork2mS*>2I1k)K0%^7C1#1DM+F=i+igG5H#*0=y zLsCgpFa2+?iiS3KyA>?Q(I|gTLvsBHN(}~2Towmg{TCUN$JIa#>vx;h3*DABw(j_Vt>rgH>`=vr1!fKBB)VBr(PZJ#dC@h^_? zotYg_3Dd2MwGuD|xND&qgQ$W0phOFA$Pv}5ahGA8Ww~)?-UVT+nn9zF#K+XL|1T9# z$`_S3fC`8se_HRVm<|> z1{e>pn@MoopH;1LAYqItdZ^)DNUdGY(O>PX&d0d+(YPNE=JI>s-tQ09p<|Bis_$;W z1|6cSoQ4tk#DuWgR}QUd;~GaF`d6v0U8Z;pQDog!9VGcZSPW~m$^e+q@ z07f_2=RDvSmiBH7uFK!cKRi&Ru@@DAmVF(=CDdV#R>$7FoH+iWK;>7=YQV&DZw~Sc zZ==EU?zRFs7`dS;tK1&#+;8@R2SXT|lbiheQGRPx#MledIn9R=7%4$f^=^=^9n8g_ zTz<^^s)&S zIv*oJ3EH^&0tx}Wp zoD)yeM40FE<0!cfdct*-Ahe?}le?ni7ofcepm*0~h+c3@L4l8{D`kk2jzpE~sTK<8+9 zE@CD^yplo%PPWQT##!F~~oZP>4Qqekhh_c-7GNoe*{ z(4=65D!Ug4Qd+h^ursN0jWIWoxX@mc^2e~f#wth6U2bT2(YxWwIv=;@7>>h|LlpB; zfm-f)1e!eOwzc{R`N6Z5oB7Ig>HGx@C?&t+fSHcFcSv~yy(4f2CA?mE?& zLlrrR+^t*CV*0ODgm8`wEIWy2lbH86Z&FxTu&&%5XlfO^0u6BB4UG(gP3Nar(+U0w2575c7OA1)l~{4zY%hA&8@ie<;iPZM++t z1|VbU#H~Q0dqd>{GD)wtb!HBGcMn3LNE8D-7fPkPO#&Pejxz163i`*ubK7NVybDRC zn4DSytl3{cRIcyzKMi|iKlnG9vs>S&R(X4R8->{SplphOCw`3~l?^-{wJ3HD?RSmY(Zvbab`?na&A4G?4aUS!bu+)B{$MLZlycusdOhsp4e9xg zgkYzNZsk_NMMm#z#o$qOFa}F)H@j>Y@)-hr*1ccLy#-}`L2Ja?VsQIWJjN6sh1}CM^C6uRb8WcpE+p zfGH_xm?kuAFTCxA?FF??jR1qsX0;@Ce?9hJeNiJMsKPGUMFhI)IP$X;@pv~h5!c>Dwb|+Q%2+tpT zKIkwtCrLr2THeiFg~!X60cwPyE8Q!Id+#Z-vJ6&OsF{4WIP1S{ykX$Z^(7_f5s^60 z`P}wm7G@s6?{QtFS0{KIqm`qv1~xxogG!m~VM#}HpANU)9oRDyR=0&>=R z8Y?&skeqZKJW%*tXNyw*B9~^YnZ267FmE`s`V=X6}(mDc^9+?(!8ony?0j6oE5m zEwx>H=ust;b;}i#_k@Xp zyKR`$>KBQ6UEerR*?8WL^gI-_yCA#d9b9T`k?Eh_*~EWgSpCIe2;d0DkLLjVQPPjN z9`(PBCwu=Msz7oj9&tLS1U`Kb_D!-@@x&Seg6ae31?@fSI`y^q&dY+dI7aE5GkT=n z&2GbwZ-g%QtK?fVYnDfX-Y%QN=Vuy9S>xc+@!s0FTfF(5n9x$D_w9Yk56I-myJ>NW zS||SbO?J7K zMQ2L>ia(o}gPH?3z104obkzw|_cwhUkmAoX?z>%WiDN0P23-jCI|*Gk@sCgr42Da_ z-f_%EEi652Zx4st^C;Bd@%{-rRK}rzGuw;u)ph$1L!kZ(!w7(Zq4>)kux<-AkU7t~ zER;<$I4p;o_gb`8-s??92h~UKWS0?UnF%e4!SIMe!)&tTrd{547|qHp|D zVok*sEfRMg6pv!2*>_BzR+0<^(2DFjcLo=0zEkxcofdKwvF*s*hLRQczjz#7ump+K zQr?NIZc~ZWDYNYrM6UkYyVx|kHPrBFnps9J{Y#GKFAZaW1~1lqImoBHtDRP3Qb zl4rZyr_nbBh-L+=}&x05%Qs%U>|yVW92c;WIPO(kGy z#r667Q}AjpT-}<<%5G?7m;*9Nj;$^D;1ipkX$7iw=o9e;a>tn8YACnrH^?`CWz5;yYMw&&_#J3APbO+wGLe6H*!DNQ>OE;WF#d_yv?lf7L2;u(I_)LKW za#a3g)HPM1XIZJpS>9#@!5$2Cl*OV2VxBZLy?>$8`&GRep!(yo<4ZuNUb0D#j=The z2ErXn!x<8=wOi(6&f(r%I+zs8GoLq9l)tlM>dJr(5ONb>cOaA^PSB!49HNg2!&6A+ zPm^;T7k$*wUhLL{MzS#K*a!qHbU6YeMy@Y-D8bi}tGK$1;=d;hfLy@Vl;b{dZjvrV zJW1{OwT$~#Fk*kASbu{KB1pM*`ta;j;1SQO84`7t<<3?nShN%5Mr3YO4RfqJztrn& zo1xmm>{+-FxXAA_rWxQ|CkoFZuf-c}aepU~TRA4WEKIYV&Cn1zBf)`bezZ6*|06$d z(21)HlH2P2FK~edziKxJ)LwdVRsp>BM)x3GbH`}sXWtp@5PuOIafzusPWsdiO=_&v8OZ};!i$6k3(Pi{0%ALYpnwp|wIT!aRbJsJcJ zgNE2HS<0}=wx_Tws4?1kGLEfOq(mbRVRDAvD~a^?d^^`wT8PiYGJ(N#u9qit zGUJhg_pP+RRx3+b(|?ZUWS~!^G_(WrAy0Ca1ltF5U{Vh` z`B~0FZUzN#m6~{@L&e%(3X_X8=8J)#szn1vop{0r7TJKd|2^{~Uv*mm>PC*1=>Yjm zfAioNo$QOgF!{tP$>dR(i2>Zni3JsYjDka@?&0(2n44pq!6#6xFQgHxcMt)jQQU)* zxa3Y8O|!MOlFm&|T@qhNC1=>{3AkS}*6Eq$2cMpkj6&&%M*Jj6;}}$0(m2{)WOiqS zsht?->fjE@H5E2d&AVV}HQy z&<#QQFl!aKuyV7zS~XodrUryVl}o$GorMElk#SfXB3WJqjcj zf(lJG!<{2AHJ&|Q<2%iz$-hmTmkDCvlF<(|ja0SM(3ttuy0(+|o#o1jz2UCugFm9N zP~Rzjn;)S9DH9Lvp-x^l^=O^UgjJHoMpc_SQSu>GB@6_S)ddpGm|H^GgZOrvn9{iw zQ)fvpih_y0Bg4f>bO2E$MrGnYX^fVAxiHOa2u%QE$`C376wTo}O*R(g@bPdkr?a#< zJ&@AFVk?`(IZqtBy8B>X@XkI7gVGCmrKmgNuc1!5|o+hFu0O+}Jk$=9rPHNp(+Ucsd}M!R4^-IW7kB zEwZ`ZZ7t*s{-JJYd{ae5mhykq+Q~TNsFlQv0C~w@8zn_Gjzj!?IVf~PH5A-s#;5wY zJnObygaTLBRJLh+CnpE}uZ+SS*nZ zd6!1(V)zF?!LlHW9KcG!UEc=RRt=g1De;dPm$uc<3t5EaG6nPVD?*vU4|IwQk@USc zK?Dl-DhApBU4MNr$R1QjkI{w3BtEs&P;L9x&DFmiO^*KYE)s5TYcsxq$)-}7Q~2!! zqCW%Fm(?31sIu#fB3R%rzOgkJTw-qdsUlwNQY%L_doz}h2uX;g&fc@C;6s zdBUoJG*u@lu}4?W=lI!|;8LlaXZ+fXF}iYk>9k`*Nu*r3)gKO4W|k$5|$S zj@BQ2iU;+B3Nx3m{%_;t+Ly&IfJMKaQY^^l#GA`>Jzel4?8Y<1kZn=|6(^0S312!Tl@UoU?uMw3wixGw6wBJKzrp4yqu*O)AY zP~+mB83pe!++QG+d1jp+6fdkQbyW|nh9Nzlw0IuNi>*$>qaz7}iA!C4d1p2iU2zUc zmPDBQ!(CFPvpKb+`rLT+^p0rlHFr>C#4Y9zd9$X>1QuiKq)jx0c8Elib&iofQoO18 zc%KPOxqqHc#oe&k?Xh#7!C~*Rt*R#MPrCyhl3vOC9Ks}{`6Oq!OrvjOoGX9^nFmwI z3(20Z{Id$16~Nt1x)3~9&9&Vc5}dCzbRlTjAIJn;mq&2=hW$D>#l^=63dL&sCtrfU zJqqx;8&gJBjH{Rz-lY*!7QJsbuh@R_geCc0H_Ckz6eZuK8<5$$kAXf4Nv~Xe3)K9j z2)mY;Dl8cKvsF?Yn|(NBG^#uZeRN8wI#p>`Bwv{G6J&wtY3#JN*74KU1kit&$rO{)H^P@HP zA}Jq)$+~UliP&$u+>JVgyPnU<1?qn}xb-#Awt#^y*Cj>*`Q&@^j9M<9{A|}G?^Zby zf@&A%e;L`VJ-8}NHbCz!;_xl9g|O;T$=a2Sdd5WPv6%Gw-WJqWoMWxH3nuF0y0j8^DoFjw3_~+m{ zZlVIq?Cbe-9Zuqp34?eNRv!xfbxEVgvIV||#e(HhFsBvY0NPqm|=b$QA+C4I))!LcFtvH2nFiuA;geIHWWBF2z3v zY!Cs@V`CUh25|k+mgyXI%808D)~LiMwIh+Xao=Dz#X)__NrH9zjGqg` zoDIm$Z78_N6We^wf`N4-5h@kqeDHH-f= z;LQ*&%38-iF`|+*54^Y1j#aq^$I-dZezBy3A)Q(>F|CH#L4*c+1JC9}KqQ=k*{Zh{A92fbDGa zDwvcurfjvtZ`D(vr71d)w#8^D<2zMdX=YN}VYHGEPhKeY?EG`li7I?t2i#b~UOBip zC;HZFdzwl+#tQLQa(zR2hvPpxTI?v8ovC(Dh_1giNO_~*tsjQD9wp1#yN-G!V>9>& z&*&)H+a&<&7TfU(r9J7M&lm76{rKcW_)oz1^h>}2AaK>zqzt$)nC^c59(TYVw8rRN z)#o^ePGjK&1n!<^cVafa5d`#Ca`)7B(jWyB^0Ef4r=+J!08PG7`h!M<^KV-34q)4~ zzvuh)Awa)zA=?~EL0-bC`bD7@l$P@Tuq*o2p#!a;$YjN8>=a15=?7dbZaUmok@MC z?;6Xy8^_oAH>UK33dH+(OfzyiQrmL0gslx~XWUT;EJn$lDX0DrU9U_egrl>qRtBWR%SJvIbO%x8sT&-MWAv_D` ziT%w&mTb^!ed?BaUwFbtcx3%XGmS#W8`a*9$veHrwePzXU3?+3_ zieIwI4i%bZq~!^UbE?;ELBR(cO_2x^#W{Y|X7ujFKG|vLKvoa7!fp^~zR7F3+$|j_OJpLxk zLjbD-^j)!Tl=8wig5Lh$<~X3+uYq;~3^Zg1aTmxZ=bL-ffDkpub;sDQ=k)QQdAmBZ zkf&@#sT4h@DF5yUEcT7^jW%RG;hlLIS_5i-l~kbvLRev+pq)+jjbw*iP0C0!2xJ#8aKKiZ7%s}oyCM=y!7?yZdq)h z_`}Rvec}It4-ue`Cb`VL1k>$ksg!qPln)#u_ulGVxaXc!KalUVXR4Ho77JT*EL_68 zndGD{&Wk%Zv;+)ue+ET>=M4iCk7}J}sJ|5&#ka9x`}k;PE)x80Z+*Sj|1}YO_%d|{nBrR0D*WGuLt7&_ zWmv7G1t>dhs2c>`q;M+p&UU2=f>WJ90yt8f-?GNK-ifmFm^Yn{$33}N^SM=>&XiI4O1u!giIe+ja=N6P9b|pJP zi0>ldH=F;vldqL4Id%i|pc8P`? zR=gORDFkeAfC#)^C<$#~ZKGaAbx&XP5&~zK`mO(XjnZ&1((!-l%?Lx)r+nN!%kw&U z@<(a0F0Ymwkc^vz#7q^>g#4GDFJA&K00B*41Zcp_oK5Ir7%^%S){%Plqu3eT`y&`6 zmbn~c*+>6&IY5*wn5Wp!spn~DDDJL|BT2?pgMy^GmYDf2L}fC_p<|$Nsr2ZQ=$7w} zwvmg9zE+^#TNS_8-xBcB`#K+`t4N@f>gvmL{mC+%L+!>%T^-$gws55_r@>q%#n9LZ z+PcrUJw^VSW=<-KpwyJhe>J8Ie}!=`q8_g`ROzEJNTYp_%$>tIwnwbg{cQex$23f$ zq*T$9d&cb>H{o^?s!fGQ&xfVEk(bwL<@zvfTZp0|>qQX2$hC#mA{X6`kFJc6mp`o; z18#~pqCe4J`l$u>FG@eYEL;H=FyUc_fWyBKtoQs#V)7gWs`?)3N!-S<1WuZ=gWlGA zyp-IKCFiN#LMz@7ia?kfi6YcI2Ux*i#s>GoTk(yHoF6>0G|-8%+VbY|sw;ZFec!f* zz>uYMEUhiEQvBLIFAx6~$W*^kt;D*@l%N{H#-iQhB1uK5N!Ah|_DQxE?GQQv^+gfP zfKj|M=ED&qOX&F4&Ug8no2>f+Spw>io@0X#&Bii4xGMNj}I< zFT|_*KL{WY`2W94pc?=}{Wk0W7BQeK5o-8)R<;?_yqy{m;oWgHOmNB06wXWA+(5FT zxdrkhgkzeI`i`GNFr(f;Z#uU%t*VVcz#6)xR0SJiaGdZaN;b|5^BO6Li#(UUMKzIN z&_kW4QeNzKy}}BdO?U-e9Kxf+ibM`wqweTO%VO>MGOE ze^(Y(?|-ewCIB2V02f{qOgK;V4CNn z)60c~hrhw6;Gly3G|fg=x?h7yhTOvMTYDVN@m^YV@>fGCHu?bQkgucxIQS)!M7 zV=h14ze}#!>n1N$_-Qbf%wz;j%w~{8#OUYrQ|kHE+l85O<@gA1e&+Wg^?O7vG?C0N zFUYL?H6JSTn z;(xEATknv*59Bd2PJYkB!4aO%BSFcLR^&S7!>l=pfi2vUN7~tbAHbd}R}oo7>-?ZW zhspQN%BIYw>lFHzYS3S!=K&bKAW+s)z=cmhwvPo2sce6-8P_)l>nha!OL+L)vUWqt zlkiZw=u3Zq5x8zt$e>XrSzOS$t;o{+l`id^x&BTmOSfk@il=>`8)vnQZtCSZC&K+W_J zvX2G3 z9Je5ZIQ=iEkiTm70@REv0j31_Dg;}6c*2{c_wdI%2ItNT3-s*gS$m^0Tc#c>e_t02 z+F>GAcGA+@3@iOpfBCDQbG5rJ8nUL8j%*}|C}?i7=RwjQIS_9K z1Oc1m=^g}Sd+MKZGkg+Gfy>NFu%!>8=5UzW12?QA0^Pd;>_I!qn*yyg2ohF=rh(|Y zK83uH_~I#-`uU%iY9dJrH6N>@FXM5xz!Kt>^U?uRw4)`KrdSq@Mv_6o+!{)TW;CMK zxkH%GRSc4ZRA{q(4HJg4;>dG z81T+zz39BXp?M?i;@`1$u-tA6v>f2&#J2p~s^N?EW#9uanAFdN1I*x{+&Mr#?og|a zf!&gxIyXt^GRGIrF4wov;_5fG%|?3MW)SI;G0#w*)i|V2uBOaeALI2-1g!<>tvf(L z`COwLR#ht3-@h2o)a!s7jD%l@N)vrvM3tqhY z&J=N?w8t105ta5^0S{?6M23eMnAwqeVaqgp?)XCN?M}Qq#e(J>ugaB%&KPO^xY{^a zhfj`CIE1ihHi z7miS}>i|bPYQ6Oaxqr_+-j{$cKp-Gi5EJliklcmwq1f8b#}ZQbBJI~06oHF_tdi6q zRDfN0uw$M1K`_EO$+=K}p|Qm(9;T>g_nrbl3Iu2KQP=!W4L{OUL?Q7BkG7P3Ul#gM z#e{&ceisQu>E-~W)gr_^;i5`ivWhn@a$@GFrk{MJe`7WslxgS1b}z_IX0eR7)PwO< zj>?DOT@b_YoS7n{U+2wXKaF%ypmjp(i^X7L4t&?)%Sf73S&mNyv5)97D zO({;X+$&L-&3?Z!x-;yXmhvVlmE^=$5(IyTV>pi~$hoM`D*RcgCvWE%`|tTD{Hom# zP`hFBEE(Wa1ET^{pxJk+2Si#+;OR_oTY5JBEv{qYAjB(j#lD`<`&1H%1$K{BlzVTX zI=UFK3GfCq`mj_Wt2sK}N`x#^ssX~}WaAiqGllWb43T?S8-LV5E7;Rs_2Gztb)TOg zw6@wZ4$#MbkbT3UuSiUO+Q06h{AV2LAzKO`AW5$s))$#xP<#R<1t?62xYve!P?ODq zJEEk>n43Kh6WEMoaL8M5k-tytb1fwJT!|vzR=4i*)3K74nhS?S%6yP8Z;TD@1`ivj zB)Gk9w1}+S^D%A@V^FgsDW0D@J_Jz(xYgHu1n+#vNZV}o{7V9oF9&~s!(S1xd_WRF z_`@gnTl;EBYa)y3K*opVEq*2=-=kLN*ai`}>L|}=sZrIceb0^fHZnVG^9*Pt)lQ(w z&`WMQ)BC3G%g8b}OdqJ>7^)M6h~%X5FvRaK{Z2}YhbXgBLjMaT&Ee3SrgC?sue*My z^T!5G+5zRCG1!$sww$xN-#3ZRM5uA);Hs&IwP)qFS3VRJ80WsCKTg8ekb2dvbh%6OSoMTpiE~WFicPF_!UNjU>c*g54vC0Y9M2|qZPQ{`BePQzvx0NW zrgK`wVK!QmZ~9FLiKn`B){pWu<;DNa-hC;)3<3ZKIq&n~|CffhXoW#q;0!xAsichk zx0(qOnu=R}(Pu6Z* z*onl}QYt{L{n}G|f}+bAKL=q^8%_3l`BMXv^y_Jh8e6>DZR(Yp1gfNH%tCpnq*W_?6d2=2n$^lMU$eI3-I*2ueu}r?zE3i*?&l?Un_wl zxMPkMMqATx+WnUWG+zdR0D}W7nsmU~THvR(d>%4Sw#GcoMzi@t;cgN1AY6r=Kr-KF z!F45ZdPhFFbj_3ybgLng>MK&_++B`$<|p)M^h-`7MHe-fPkdYMBKb`8=)Qr|kt{l( z(QGp;+?$%l1?y4HW8XxS*MS#pqgY!=33e{m@+=#Khd^^M){xhUMg@TuY66{YE|vG5 z@V6FCv2FA2G%*Zns__k*SPo!ksX`%)c=(^;efiC+)_7QwIrfS*U~pAHP4ATKS+>c3 z`ES_^Dsh(L`uaei=xg{_^B`Gx0hBHCB?ku+b3ZnZ-;i?StZRZU%SsXo=Zc;1qvpN- z#R9_@LlA(Wtqmp-z(5|6LMFLEbxN#`ky_95{ONFrK0)LGy-qC(Q=;(0k{huBdC9i< zyP;tWotpTIbP)Js&*P^m)4p1P!Q$^w_E^kSG{X^_)1&#kZ>(zgBiOQ7Dh06(a?-?Z z!KG@=SOzwD?xE=UjL49p6{#b#Cf?tC3%wbCc55eJ2nBzH*6~}yi*bf@Hiw2SNmRuT zqa0VtG5_)J&MH)z?c18eHcnQ~hFRKiaj^h4n2JMuUUY=HP>$YCFR+OW&w)5I#yqAp zY9T^JG)aKajoOp*RyZ72mN4So)ey$YPoq-$3sysx{FyX{oKK=!`!5xkzZil641xw- zg@D&M(58S=feub;2?ukZdIBQH-d3|2Ut1hpa;cxLjwg(n1Bdga)ansc7=HTM&?tK8 zz4?u_Z3{{kHs@tCM;R5UWz!#otK0q|fs|h9w{zL+(&7DHo1c9A}3^tyMxIbXY?EF_-zrm*+ZsUs_*?%VF|1L}UY-%1^0(m+C zi)?X=Yr|1E6t!-RgmtyR?KY-!%;o#woK0V-x);75U4|1^a31RWQK(Up0r!gXlta2h z8aku9$nQ!|WbVX--8InnfsC`I_@p7a_N)Cr0$=tof)D@!=?1JN;1*PRsM`va88+y116c&n3AB5}W1u|g;b*RrAHM0{l2 z?eGEKWm7AP)Li!yoRaBfC$DdJ@yQt$H-vs+96IbzYpi9FUSp*GSPDVXUWxV#5oNKd z&}ieqXqSkbTBcjpH8-ot1erOM6MZM5yXjtTDO9q8fzD${$~yWLH?PXiN*wN*vO1mG zq%7o0blSOWf5g)l%>YVy@2zz-5@$|!B~le zSkM8=R|iifJz&A64{Ba@c?D%9m9g;!AUS;?)zZ))Rdt`94oN#FJ}TSaFEQikzL0H# zgoo}&4-Hu70@C2tAvnFDvTc@Fug(ICRE)%sq2=~?39GcFUsl z5=vAH>X?t)f2H|bbL zIPJi+7J`fx0$3Ye(Vs1zX{)}yF=)90#tj2o0aZa^ujfn*Lc_2{z7f?UHosWky09R9 z`O`i48w)gMO|XeMLk!<+j|)V1n3ffY$=uz`&ivgrOsvr9lyFu> zw8p=Kiy)&Rd9F(<$~fHg3G&R zX$e8k83Z1pFQy}W6V(o_q@9l>B>Kl67u<_c*;*=c3o|Fo`!Vpcts|Fxm$R7YUq*<2 zIYa;)-dLm@0Cx-lzyEmW-|T`zP3Ip@Ld~p7#V3jkJt|u1Cx4GCw^n@qNNMD8*u)No zDwr@-r7(4c9=uvH9zUm;(h{sLqQSpj1ql;jHhS8#X4ag~3b*k$+8 zleu@69VHfgBt(P}Oo;Hst0Y>^$y`HT&K4@B7-Pj_6a1K2aZUX9rJKF3#i~|Nx?-4B za5luNS-px(LoLv7<8Sb}rn`2?3C3Pw6bMsPhG=U(sm$5R*a&wN#D9??`6Uqvkf1Ii zuLVeepuRy54*ik|o?EZL7=upvDEUnNX4gVM%t?;Vu_v_j_qQcQ@dS~FbMogU93R_{DbNKD2#N30CZRziD?ZUG~u)Ip&<4+(eE@EEi5+u^^rBg~|C`)e+TFiUO^VSIu zx`^$;IhAQTMC3!b;^#x9KL|L9^RNx4%SEeHrr}8)`3C&Ua~FI_YKn)F?R86o*)mM( zfrFrJ5eTE^GNnJR8C_x?`>9J0PEcEwJeD^|n?^d2(EsIx?3Y9oKqAS!srdi;KtfAe zZ|Yh+A{^Md;3I?Eg>w(#{p}m{7|Dn}tEoWid~XWS!S!cM#V2=p?BvQ6ZOBoBV5omG zOqe`!&(uN;fq~gQJ8z6mO*x%6)EVqpw&lUvazL3wI9)KVJPo_E*L35RcQ^{@K^0_5 zYtpOhXE)-rLWei2F$h#FvWdS@M&ib9tq@d`RZLSJfqrzxlvJ!(6{jOlrtyQas$Q*| zu#zO$olh5y7G6DXFpW1uD=&NC%I|F*8%Y&h=X4PqsrXI4(*sRiCAxfovm6|9<*8j{%pw*Fq49IlUsG-PW4iNp%#_uIa2ISHwjZxYa?AC(tADmGei66hg>I5o&7DQSz%UL_Q-ti&5vWAQ28^GKlc1+WT z_C0ai;%E~CdJO{?;cdlEPMF3My#c(2NTQV1;ld{2^m{seb@ ztA@}SmXgfEer`LoQbd(hIdthyIz(h;zrVJCGuXGU!4VGaJrKfN$f4>F%+3fG#_&~d zMU-e(j6NQq+2;Y=`DRvmM}J?HFZ4tsSJ)UhOT6i{7omKK#-FPbqb{oi!jA<`Wj6y~ z8Qoe)EboUnzUQtfym~B09M0M*tSw_}(dt(}d&^^E17|IRd(IuKYD~B5m|rOYl;dQW zOcpR#8<1K^J%H$l9$s6nqqVJ_O}joIr}4LXj`TuQJ=VJXDvJE0G$Ip(fa*t{Qi`a9 z!oD3#T~+$OpwRx}hy!r2Gigl#KfEnyDkIyvM?+-l?;yz93T?UQ4LGzWG_|GjRtO0< z^A-k~N~{Q`;zUbo48>bG#o#h69f@o5-ZS0y1|@RcVp~8y6gMa!dG;1YkN9@Kp>cZY z+RPdlm6(*;3L>h@pTm}~!;K5K$d$rXaz5ge@GMDI%>7*fl%IWHyy4JDE zEyNP(Nl+(aP|nrYwxCNx-~f~Pkh3wl^VUn8p(!z@s2?hXD(;ab$wB}kdrl>;ldN24 ziO@_8mC1hG|Aq0_O(YNQO<{Z8w0aK+Wl zAAn3bG0y&AdpQ`cqMtagcfy7_WNw9ouD4yMepUruI&I^+HfY4Szyu5Jru`0u0Mdf6 zH?YEj4~&yaWh==g9=C>)0UFFiCfJ*~qQ8oM`F$`#TmYU7-?`|$k;>wXnF+S*l}O8W z?$X3E_K=B_>m2ub^sA<^}hgZr`t>$=eNF9X4x2{20HoQRNbVV#pd8nRotU3L13A|uH`v1bjk zbB$DGjSdvPTSM0WYlP=(_=S=HKnZnA!Udo#2j&tP(l!MZ1T zhpoRUV0+t}uE1~Cci3MryBIN&7ThcWHmMD#iAomMqU*tlm_CB}yH;986Nq%DV=p6; zg;Y6BKs7d-z7^Bi$zh8T0(Ud0Xb3&SKHaAouR3&6RAg{?4)w9xixUELqk3;C9?WS<5YxhZWuy6 zQB^1Yko~gIoW`MZ!L8%%{vje|m}s&T7zW`W#fw2?Pdq+cLs=h>=Iu7dg8n$qH3iKVLP?=doq6rX4`I>GoY;;OCP$it**33UUu zv+Mpc+*Yt_n&6B3O_bxp@Vil{z7K z%f}FFc>d`(B-;^iGN0aa(7qe!?W(c^n)t&Iq0V0#7oq)*1%!4Uk)pMG5f!fh`iQy7 z4`r6(f*=eKzAR!x8c}XsX~DC=LI$d3T_%4xO~HZ8cT0V$7uNGes~H*)1TuP3Kj&hejU-RS)Kg29>>3r2Jlddsq0 zAB=QwM!yMPIKUftPp-efR|#49ihN(cJxXv&Br@l8v^%faS932lCgG6X_S^Hdfm1h= zC&W~WpE2P9Phy$m{$G^Xd~qZLID~f8a{+HC3(jS4QMp(4lLS$Wh?g*VxT~VfpG9nLE zZepVZnKdoO%3{Nrl>XCh*a>P=$G+-UquINXOT;>=fY}WuIvF;*I(r|Gp{0RW{J$0=f$cfvwE+IJP_NLAv=y zjI8^;nWHW%+-)F|FdHD&fau%p_&sDVo%fx)@2GS)8?jZ5G0LvKbWuR)2Rv|yU3Tru zhX0FHU;8hI6oA7wCv8MPceDi;4;vkHjFF{oomzu70ywqhp!Sc}ng}X*{Z7@!=Rcf9 z8yj1Rw<4^wt}s2_K5IozT!Q@{@<-_>KWiciuO%qSnLs&(j|Okjku+=>?L!j~SI(U% z+z(<-W&3GeQ`*o4y`)8mDiRIsQy*RDh~>OB2gtCkE)3`gNiLW?+Y{~vfX}p{@FSV# z$5<6__z1^biry}-_vU`oK>_PsO<%v=v(CR#$yd#E5{&e32{K(nC^_>2jvJ^F@6Wkg zPTHD&&M$G*`*ibrYxPFZ|DDXpSv;7v1G8V}7CR|5um`prf2^TP^!Q&|IDa9e0uYv} zOX~p$WQ*(FHqtI@S{l|=LetTlT}pCiD&#Ssm7Xy&vkQ}RB+-{ybcd7)lztI%Viv{d zcaH)Z;J!C7C0F)MK3jZ&l}>&6da`%mgShZ)9_w15rZ?xQ6SRlcZA5PK`W(Mfg5WAB zI|MChHg-5(tgr=gUq#SzE&HmA1(}N7B^GhOcf$)4()RsZ4%OXL=5#7XMSt}Xs^P~r zkTf)+Q_xk2Pn-ibj7Q}9QBKKom>-FCIJL73)1FAH1Q%d}VNyIe35GA794L^yr-Jp~ zNehDh{-E}1W0K(lU{$!&>2HUMPUnHpgnAvVNB_!T!a=AK-6P+kOP&~DPzf`1> zbxO)??o>%~sawcjD|lS5VCSKq@&?Jxj*P+fLf|Hnf!78~mbtY!{gAXwAd`VQK4Z-n zr?eP_>F6 z5JXdgb|QVKC*O~fJnX;!5p;Wf5u^hM#$+6R{9pgblyz>c8lW}DbmTFUwFS9_hc-l= z_w;O|F7m#kmisV3hh%YHP-Nna1V0e% z8jI4in+?P$XCMY^yq-ns9X_%mu8*ABGB3YeUhfa|}_$!&V?&UX%~NtbkAd#{S+;I@SE09~Lg zWu@_$Y}xH^BgJ7O2P&~rLnykVP=o<_-JS$K>C&)?2gN*z)%o!qZ0N-OT~^g1-8IdG z;?;95L&{S1<(Od(zK1U4l^%eJEE2NZ2iD?8D<D8Bc3bn65d-zIkw1pbKBr}Mf=5g|R!Ih2k>n*=Y1sTkXt#v` zB!+jNw0?K$52bIw%dmA4JGcXurVao28>5^%Uu}Bv!(!D1+X(6t-ga_&Pl{^}D5+Z3 z<9`+cL0=Y`0E?Ni)nmX*D)8^b0uusG0dV8ErO~kn$u28+PFoba_taq(LTr3c4hl6v zK*L$+WwyU7((rZA^p+|ysfukw8WRp<>0?0%%}UB488Y(4%D))XP}8J`paXJSeg;+f z7P;@3!0{i&2=1N&4+W7Qe)h4ICKB})wHibUuRG*@_z$7=bkS>U6osbQy!GN2IzPeyJiz1JS=JQC29$j&w{n=xpAZ2bS zIS5HtFBe&)SUXHKcHOC~zGwhZx(e+sQelauX=EES|INe!uAf^sD{3ez+{yL7U7}%M z6j=a@Ez~Slz+WcW_!;TRWv~ceGbUdF<*j;7EQ|5sh=4`L&y$UH z$x`qp_!uLeeEOJDdd4&(+HeLH?rEm6Zf4)=a%E@s<2N$t%2-$BLA97XE=iEP&q0r>#<0}&{m%xJWn zC@=sOcWa~}0)OA2e0q zfPDdELsB=~l3@*uG8HSa@<%UD0-0E&!7~!mDVEV;Wv^`lH0(ais*^(cao0#bnVeZF zxC3+*Km_{TacQF-RmPF{`;kT6c9TkQq#6-aJwl`dM2K+{hZb+EM-q$Yw_&}hgFr#jK}%DbXxN7E;D%3x zpxPU=6?aZT>bvPP4$M)vZ&cUu9RgR6>t8^`ehK6N1PbsRO92Ap(HgNc%%%>Y0(R12 z1p`0RG-oagUuni4q>qVDj2zK_Ch2z|iwHojiLUDN#+6M$p0)Myxmhr^O$^$F>X=@L zXW3DWDHqNsQj@NJ5(s*zDP&pCmMQ%u_u@f_+6}%9W%X0~Lu(%VV|yqB-s~Hh|8Fe> z)I*k%T)Xix-h>-EKHylE@j=!19(NWhWALU zeVPM!#YeZ=Dl2XFTf)if>kLr&1uVRH19(5q;U1A6_@L=GujWm-AaYLD^g)05TkmS*)ZxecqHHd#zJCH@iWBw}VY+D5*}8vGk95>%I{f8E0z zJ|m%%w$!kb6~4Isu#rHr42wmd@Z2;^C1o+76(PQx!q(%lhbBlE0ChVO8bdt6NHlE7r`8yhI%o&0=B z)MD?4RJD$zQd>vjur2X>L!hTVQ6-usZ>MupB4@ck`eW_32Sn2oXlbbR@ebDw(L!E8t z0cwAx+R;Z1q{|GWS#EFJK!J!D^U}t%GynHZFd+X6p$LFLayOp@@<|E!djR^)0wcW^=R5& z6)D55hK?745i}*aB#*!Hre-p<0%Vc-7zoPWPVy2^cD1s~jsjle5WHJ~1U)6%EW&Hz z)G#555`%lPaOts?`C(PD+69oLfCb<@SO3fNpZ5KRQD?p7*|Ep6aFWf>Y5IK@mKmc+F5_KDsWHhAjzch3u}s=M@xUl(M&D-K6{5SsR|z zXO%GXr+rTyflkQEVWB-JlSbIr(fyt+{u$|%|^B=(4 zbZi0=$)C($Q8cNzhtYtc23*4#gU_`ym>f=Qq>t{Hc%vdA6>Ua2Q6(eeF>jR#WnW)3 zz<@Z??6P-Oq)KK|K};}KDBnu@8Wq+YoM8m%feY6?;AGUw8JoKCCeiQ9HA&#mK8o|u4t0E&b=$eW3HUTXB9aQ`WuXqXGDOnB~`vOF0RRh?O2i#Dqcb6 z%NtQl|9!vA-MmwGl!J*_j7S&S@*6u1Rc5k1%KtQmp!Y1P_o@K)60un}^+bKHmw}l3 zFemHkB*U$)YpUQ+TeJR`D`j5_B>)8`|1lWAp(_S(gLcG2=ra##-2o*LszwivD$Sc# z?~8ciC|ysr>O0enf{~jj>j3cBAJwxN!)US$a6jq7wY`(ke)|$4=HSq)x}n4aQ|!*+ zj8D#5_cF^EWCL!wfo8C`br8ZWk#*^y^Fx_yL0!=QVMhvML%O)6l1Bb0yT?z~AFZE8 z12K3ECPBrag+;UvZ+;p2K-eP8-&U+;PHw>M1ts<(Tpg7Zo%bq}GL3QU%U|x`TQw7^ zMCmWVAP1SGd^0Pw!hgKQ5&QH4ld`nq&^!P(Y-%hAfoQ)zN$R*jaspple;IEU^e;>* zzsfHKl)nuWW(l|iXfgle%KF6V!FXMNkd1e>9<65eLdFSz4^5FaXWC633 z;FEQ5@5Rrni5m_L!8eP>ATw^HBQOZ3n=N2?)b?+>GW>$&=~?mNM2)UbA)Il(Ye!d~ z9px)6kjMg8Sz-r3zok%uC{9U44D*TFgFN)C?hfiH?2`>l@dnp$q1UY%dKP|kqo~#9 zRb~QHUkw_JM-uqCMgi-XsT&Kyg+9QA-dguYywuRi54wEhiK{3k#EkMdJ!gFX-3_Y~ zSZ1K)-XA}OB?oEgi2d#K7xS@&&bMGf(P@=K4xneQYdpzcbAr~WYZ~YjHRu1{%mZq_ zG|B)PuL+8YfOWhj`08JvuJ5z5IU$YR01bCHU{;L2iXE~E<%igVsD9tH!iwerJY4ul-XRto+|YmVJC z9uXqXCVeoOno|s=;dVqb>TdL6pt0F#q6xz($bD(6l~deUMsrChvZzln(TEwk|JEuR zzC_9aBF=B8tiT^{i^Et!c>HImarq=k3I&ZYkUv0vG~)3Q$9iOZSFOefof8hv%cCt= zq{JikJfT`?2*m(R+Yz48pxg=uzDzJ5Dk&$sy0H^*n81&?foEPcjq>~#OR(n3BeB&tIa zmJPQW?xmhmlb)=<**1DNF!OPut?`i^bk@?jGVZNh(nUKk?N_EEeVj3sW=mv^^ie){HZV&JI6B_nh-r-n0 zvNkE#z5SB`YWb3>0LXOG%!~oQ0lt@bUSY?$Z(qR$|G1HTEut`<{@&{{L#p@p(^*i1 zqq!HMGC?rJESe6mKP{Y1+?SJ&noJ)XK7k+E{OEaJ0uztZ?dNld@Fje17lXpKdO+uj zHi}2Td%KcXZBoL3yySTEg#X3foOJA}lVM4W_hB`0pNEJg5gaY9^B8&yf}ym{N5BB? z^V$cOHm~Rgy(~x}B=T@-{{qTZ*^;;8PNl?*?(gpqzdqE&kZs?G*F#!4GmJ22uG~JV zpqMRYPEYIXwPq;dh#Gx`_(C3Gsj_5EUF;h82{&A3Yw3)YyTN1F6{tJM-a7(AdP+jU zE65P2sw~eVHO6}G{<-wFf4NiwT-;%}5B~Sn#Pq<7^oMh0(d@qeXM3k023-0izOSLb z%?r=gl9?2&-0!#U8*>@{SXPMx>SNZCB8VT_RPyd|t>b2F+@0gg3iW}b`#4ZDA;pR# z`Ugxhfc@XE$g>TW_7AtYt(O7{&d<|%noYc%u=oC$I3~Dg+y$@|O$7U;AX_V4r6 z>Wk6dT}FJ5FTu;5nQHI;`)cy*3#AHx!mKmV`M-xn+weFPS&D7nhc|e8g>*E$VGT@0 z7Y_dJ2iH^KLNCL|Or4e4GhST&g&!~4!fXfg0l3~+V(Fg6GuZOktOW9f8 z1G0aXkGUVuRL=$@b0{hVCH3*|ht>F;RNqHAHy4Cr8x&=#QC~Ara}AbK-J12_P!eZ^ z-o9k6CZDB(NCc&49MXc*SD6uJ{0b;YoYo7qTXoM27ol;p z6vBX}uzIwwfj^zdBUU3K{A0Iq!uMai^nJNh16+3G#EL*Z2?0;bO2PNd1B0yOX$_p1 zFoV~1rMX%Uf2~SphF&o2Lf$~vJBhmGV`yVu>8D$1j{d}3Od!nMkBS2s$ap}j@wSqK}s z=74y*A&Qu0q(A;j=^KV=C6_#ov|RV;BVFbPI25aEFXI}S|`XHy)3>GUov zG+{^o(18r>{6$peu;5D^b9}Qt=odvrVVcL-6bqu4U@^9&R)jRx0>o#6HYec0NG8Gp zg~Y%2Qq_pH6V3Kt{tSK5)d1+=X^>1oKE(iZ4+tM!df-)SJs?weH7P~4>jn<$5W>ya zm1HcGvcK+iIXn%!hc9X25;=xQ6v5BQ?G0DvC|8K!ZQiig2mAHX^EU|S7t(@KXD^ml zoKn=j-#qo_KJ0Aog&EWr*LaOj?`v}m%Z&T`=A7!K%{O!8kge@lS2{PVc;1+>eQ1#dLP(E_H!w|ruV3JL1}9?jm-FW*{#FDjijGRUX& z+oG8Z`9={e#Mr7LsyWa8pFS&3VQN=FGPIg{LE-48@iDt8%Mb0AUuP~z`nA{=;U?=t za{(^TW|n!&SmOv67=5p@@rg=YwC&`gxT~#|em^$N8e?G9aY!T+-BlCW%<&t$q_=IN z^b%~$Qh&cN_fV{tz;Mj&dlbM95nCBp&F!8ybJpSMqRD^3)CpBGl)gq4O8h8t%-qd& z+Gl4JHrE5fn2)X{LlTVyQ!Ngl(CHkh&}xjn-y*W5iS`~A=Fc>IqTn&3f^<3kqtp1F zp)BYA%{|sR+8;PfQaw7|U%$_l&ESqrq1iFqU92MRFpKwinA^!v*HwyC{fP9JrkPYyr&s1ZI<(e@0(bzq(ABf^p zJ*jm4>7&8K!1^cJVRA9yw@coIdqoXt!v5+HVAe%Cz8$4)di*&?MqX*okrfaG0)mD! zHYxUK-4u&Xwn(9&!`dabzUn0Fj05SJIUPz#`HlyrQ_obe$Fsf#1|MPqiW9u?8_oJ6 zMH+SXr-P!ZGC7a>{;#y6N6mLh8}$B~B`RfP0d$m>0g`R^FcTQ7%wxAW`A!`$ue+;* zj%ifJ=e&H)B3t2;1j0Rp`fL~Jhh`BDBth`pLh~MIh4cMiLN0gR%jWGAT~wQ3W$_+p z<-CqjNRMeh)Ldd(ZN|;R?LgrlFH9GuEZ#Vxf*77#gY0l>#6h_lNF9C~FS;L`p-gnw zvg_@#wO|}KrkQZ?W?mf)NvG(zgIXx1=-F>d0c*ParZ2>@Z#T*EbT<1hU}wI7>j1#_ ze3;iDpX_go#!^t)zFrn9!FU9de%9@jS=x1}AE7XsCz3?_F1e=g)U)<9aG=_O@Moy~ zRHmNE9n3s%_o=^0@~)=>qzMyxeSwz+Q9^DO^PH}Pw?b~PJ?!RejlN&KqnL}kV2>SH z&z}2K{1M#Y4;smm$(|$1Lgn#fReh2$h!<(}J z<_~*Yt*|I4ag*=y@CEtp()X?!B&}or#)jC=Uj3(|Met!H0U~U%Sw^`P)n^z_utji* zoqYtaBLUGkMmcz@v&Y@vbG!W6ITqwouQRy87sjAJzqovX){W9%jy(EB}}a>}n@>_RlwGy_Dlp;A*CQ-j&^5b92& zqVK_GI|L$}9x$;PfA@v5Toy@u$UZG%Ze=BnoKxve#hBOQKOp8j8&8O+(GgPqgcv43 z#42`$ekR`tqyQUBN}-ysvizNG-odkDL4&CD(DqS~-~(?Zc@zisGOQ5#+cc5+cw&_^Q?AC9onk$x`=YR`zKP!? z`hkTk|c2}SyZ!kU6wChYxNp~%O#mJ<9Y`zzb zzc1B=2exUg8$N9J39rd1@5cb!RA__!C7X_~QUf1`_tP39A=v4P?4Zsm2E`<)o1bB~0gzZiGtq>iaTr7_48Sl&5%f;HxA+n%KWWVBBmz$~@z3a``Sz#6J(V zM>ozR9jdE5v(ZTE2`G$6he-DPSVPbPay6e8(39_0#gyO_>Y~yFW$dP-bSROfkt8bI+DC$$^jCCRM*CI|R zi@`<+qN?79bP`Ef}@huuB3o^c>c%RDkkva;4XL6>oHWlkTTt`%CB` zXx+XN%tarq3Cd6c_EQa>Z0vI01oCNWL?v+NOb{qN{V5v79ev%I(1R^UKuE$b5m_?W zx?%)3y~y4IVd{j7QrW$!$B8eIL!<6Pq_yw;lB*5-|^GrcBg zn_+~ZuzI8R{-nzNK_`Ch5pbigLyDdpgZ|-a{X9Id!5Ac33o50wt(f5MCH1s4#e`HqH#$z=V?(cWKOd z!;aaEBx3-WOXlfX$?&N*&}A z{jG8V{OpszLBX#UggE8tkF}(dXAT~oTF9yv)D9|kBCQfRNDZmMYUu+9V#&kKg&1jDsyvg zQKHZtUpxu7AUq%m8YX!5xnx6BApg3fS(d4M@{H27fnR~FnUs}qy^`p^`arG-*~mcx z&@NWJy(Molk_ts%eib&gys=@`vFj5~Pa`N52+o*U2s&(5^vE+zGI*z0XVb;x`f#J{ zn5M!UviYR0+_?KB#CSym%W0eb8O{=L;Z35olW4l*1TlMePS3Ib=`Z2GvN zliH{iG+ZU1wqVIAH<>+5#pvo_ibm%(1>dG8XN1IS*WddV#X>}Nm#q!y$mWf4LyQnu zYJ;WmAatB3`O6QvOhu-Ir@#5H1;W3(Zw~d!OLXFS>!-+MmNM~swa7=ZSWM#%=V*?4 z&=sufAqTc(P*qxy@tXv-5X7Lotx@~d4>cGo)X5HHwkpDP80wduQUfJ|bU~I9p@K^I zxSUtyGttCJeo%h@1m+mMH?LDQ-qQB}Kgod8FUeMb+6rY7K2egzE&cAvA}wCC}B4?b`~8?=ztMbR>mQ}LB-53-^l%yeE8 zXu-F{78(Zdau=#ySu3_?8KPk`SIc5TL_8&rT469RJUp2z#aP@F2Dg?+`K%*ioxSeU zS1llcCD0nO@j8XbK~ZYbeQ4tE>9*__Qa;=H?gtYG8+WE>?1|p-oBg6oyDNEq_9bw0 z+^Bl|vL4V~-P?YC^86O42U+|KVY=g96HNFehbtx-%I=LLb-@~n?mfksWX}sI80Tt& zjs3_`dmZN&ZZ&Av*>tu|2aBFojhOJ0m5tkIa9o_oqMt9D`gL)QuSvetK6&s+Zr$Oq zIFxdx-@6(J0w^&ZRCJz-twh|7Bbv0?&xNSZcfAt{AA@p?0~TWX=p&!a^4gca-7k z(v+WRn98ryd3NRAP3^t8CYt^Q2H*Q*c+}9^s{H7+azyEWw*f{$ZPE3q4BU7ivlh76 zjAfp5;N_Y}bYL;cIJ!wQgta(G~{T-t9x8p*Hkv@AjJp%o5pTUrLq0<=FddI0Klf^4@D5|>YZzIZb|1OPu*<5~p;IZBdBEQ5j1FDn<#Mrnu$Q!iaXD!>^!+W zu5L(9Mqtj)2y5RAgB?)aalC;wWx>X%>-W)SbqXmY$y?xuU+VuGJrPOtTE~Db;?Y=5aSw8yz#(dECY6qMlDWVqhirrw0*^BGPgx z{#VX}V|+a=`?g_U=Rv$0(Do1OxFwW(u}=Y!O?bZ6h| zbtHK@DYw0PerA6t=?^l>tZmi$rs9vr4@h+BMpC_k;)2Fqyn%Ef44lm83Y96trr`ag|YE=+#u;K!pSF$F$_L@v#<`>P_hyYZv_99N4%I=>O zb8MU90Kd8rdaj>ts-1^5{eO>z(EArd7l5Iv@An#j0c9hc?oUmb#FPrIJrNAY9G-pi zieXu_cmtIzdH79+gEiDs^&U-B&G3f3Wh){xm)e|iwDZlV=u1il3->*ULyDUz*77@;v!bgl~JSN*-;%Rcztyf+n*o#i%hV3GEy#zR_Y*HcN8g_ z?j_5&a{C_XWia0Z&!4QBq#o7U9v}Cyx&293f};sqwkegCl6O2?wS8xwEN+^F>6#Wx zLIaBK0)M;! zQVfXlCr{tPzfb{2`2SaH{Q@xTHE;a{etlyQ?Bz$71QLn8Jzzl4sJ1przuxzKzX;M~ z!g*9VcSh|@C8SkBL#^#2H!>9Ir&$cT-edGvelk`^LyvHl>d8d?k@`{YLE*1ynh^D& z3@ie!4HZ7YXDv;*&D2lUNva)*Hu56@`7KVxCW9{<*nY}SL!ER-;3i;|DJb9}ypfvH zbmTX7Zt>V44T4jnPS6RIZYO|b8l znv9I|DN_K^(cA;e6J{R0So{kOurHQw0E><4&^JKqW8iAU4ubVxQU#)uCOo0kScV9^ zi&w3f@eCd?co!s_M*c@X1r~R2WqEX--#pqj@{za<2>7jj&;_1$6)%O$s9G3{>sdSa zj*-*d-t|arsbJ>OfI99)s|lbzEa$zBnLR?Y$a-tuFP0WMu!ElWa7E`z!=u0Ap?GcP zXqB;G)L>k7Lp%}=;A6Qo1(%X`nY@=Ia^phl%n8BpuZR)q)~hSV5`gRzJ+W@e5Gweb zfjXh-YUtiQr~74M38n;4kzJ-<=gcz)C3EuP9B_^Qx$`h=KF@gI_W`+6K4AvKi1hU8 zCi_2t0H`m39smGHFohb(=QQB9e{7T<^YQZ2^$BT9cLkx=mIO7Ci;Fx44YxHoY*KVj zhv$_N223ODePq+5%NC@2^l3wrf(fwoRtXouJ#C{p)?>EM@a499Tnxkj#}#_A+vs_bj&$_zp%7yB)Q2)lBt2AeT{h)1h&r%R`3AN*NIx%j`W-Ieb*k9- zWk*87Cm5sAi_hxDY+D@4?z-=WVdWY0__U~2hxP8s_;EY%_Boq zvr`THu<+%BG^0VHs8|@-am`1#h8tEZ-nDcy=fg#Ca~B!zh^UkF#}`AzW>G?nC1n?bwf(6~T4kI-`nN6C_)&N-$2Y zX#rdL(3z(0_MHiJ*EH=wg1>n@(I?-pwL~6&AdS8g&=pboik~-kF|Y*he%34Svfs#1 z`Je9`LY_A?bcJR93qrWBk?sYI^!;kk5b&Qj+@2VSrTkZ!zvO0tERY~#dS^}N@!h@p zes%V{EA1N#y}z_h?}Yib-UU^`;}{Jj60x(_2rKO{2%5MCS4K-%W?vT!u`o$hpPxU#imNVSR`0{n{A>RQrTQ!!{NLC z#25fEP7$+olDWr5GE{YMbQ@lb0dI?7gZO*-5gzh(fz(X+-@$BGA2NljP!TCQ?f-=c z6cD|B4t;K8&U=bK`z23egk_VSE|y)P760U27NkXAJ{b{#$+GWF@Gtt1z6$RH6s~35 z76m9As$RJV^hv$F;f1M%6md3&FfHFUx8`PtIc^yp%QS<-)Lwh^{X3AHXV~LxfFyfH z13IVMk_GdhP3D_XJ@H93wRaYMEl$1x?c94Ymw1VLa82%z<>$ec-3s{GX$X~vSSvrzK%rO<~7y zw$J=|`n-CN20`C)u?Dk~lx|K1q4i61@#~$>OG8#+hkD)R4q^NUn6Dg=LTYnlybr8h zxAe`IO-bADOiszZBfAFlxT2TKQS@`U^4_Bs+R&t)r}3Q}<{)xUjKB#NEziIyp37hW z+Iqv4OKco^UD9w*uO#`4P`sRu8S}vd6_vB`F{PLLhJvxy1S3|IkCE}%92J@EY7_G1 z!)HRcgq7QW8db54hCc{0MCxVd_DIHaL(+!C4rd!$@{ndD8CaE>)fxX=^bNrLs(S!X zH_^6}8sK&|#&NJ@&+1r-?0Jko?AJdE=H;`2k(=s>j+R291)&1NrV?itkxXB{8560O z#w0rcsjXL6BIGIcs4zQ$u-Y8sE(ot_>~JMNSW~3-+U;H;?_t?*+hEjf%lP9Mn*$xk z9T<`-9C%84_2Xpo1PI{UUp}wu(q|6?BZMQ}ddN!Dzz)%VW9q$|bYyAhPkR+Ug2K64 z?xfrRQWsb8Ato73_R{-VtGcIR{_-kQ%SKvo$G83ZSfF7O6KwZPh31lNfNJP`>1gmC zFGZavplIu^;f8={C%0>Jy)#mGU;poSj{8;pAfWnj%STO+&-pj+52nXP>@v#E z>=1m!*&uKPm@*_10zfMqpmTzh#lTv#ddEL8?Qio8x{JNu`YzvYMUp_!sl!MFohEOO z%QpxiG@8k$%W3E$cO|nPO$>9p4b=qT(8;ZU>-uG2p~nqOjW_ z%T7%HDIhWl{lGa*V$%1ba6lWY_v{wbir{Zp`u7nw{hiiu^9lwphVQJ%7l&^n$CI+YEvs4#L!&PJ<_Y zzGIh-B|8?^i%r`5n(S{;ecN6_wTSq^xl9>ESQK!N;~{V06dbUQa=rYz-kUB-)tJ@0 ze@#+E-q;9(91e*EiarS@Xns5che{v*W<v1aEgL8v&eE+G^@EyqZnYw=E_?RG}KrY^nEls~8p_4vy6IshG`oA_sgkQrw1Q_lX zGTLUqjyNFdIvL%1xG-xw?_T~yZ_@Uzve-bI2AUS+)Ohh)KQD6w!Z{?rZr?%n;7DOo za4ii(&-gMD(={*92W$7g^($ClDb3?y)(T;yL+x6-z%LthaMq;A&18jhs;nXmk0b-{ za}f6?AE=K+LyMesB9w?&B$5tn?kAODm-|V(#UTN-=DaJa%0)ymfC}iA5 zH`bxSM|hoMu7*o7{MjICdW%V0OsHucQOXiv1fH`tr5u-<+CLyNj=l??zpy&|$|oY_ zk&<>4n|e{-n5&|=5%;M;3ux(_H&8rx{g-hhU*!)2%2&ByC;;AngECWc%9EL7h7~u5 znU@x$F2u`m+h6Z#Oc5_;DXm5l-GD6=k)-a_W@`d>2)yyAN<4BR9=L!1h|hOf#<e zh%eE<8w4}L?a#QI+F?NCg^(heqR4=ty!?(OeUMSMd`434lB)Si2(kuVn;M})FQ{AGXOflZ%BI-j6`YkZDM zQrh2DCyd9CR#t85mT7;bE!c4S{mF7$(}=UwA;qKzw4G6(Gk=cx6+t@B%DD8A|+0o2bd0z>(~Q%SdRxzMUA;@rvzcnSTNN+wCGGjBV#JCeD& z=PH4|9VA#@x3Cnyhu1KcBH@qB=D0Mprc$yfLYkz*s(3mvWaylgKdJ|kun<<)4JD%tfjg<3C2J4 zFZ5`>7)AjMF;8cGfa51nbpjprfes(gTF+{coRL>Y`D~r8Ad@@YQM_~F!P?W|;D^9d zOH!}eZ(nV5ckg0_Py>0CPkzZ4r?=AfXzu!Lw3%(p}%g|hL6gMc!+y!mx4=?6es?Y z_mFB@+LOUe9-I}_v~f;dWniNql?JF%{_MgrZlk zcBo?#1U50V%Z?cB70pLujj9qt>#*t*eBr>Mz98e)3W?c)?w&)hiO~2z2Ox$ohcSQy zs8p09V3#nI_8WM?A~QbPSu5B1;2(^lqMzPAm6V6DXrLT=nzVEo8t_;v4fMWABO^re zCth{;z3nRWnr%C>M3mN-BJKUv8S&F(jm8G^bkKouCJ_NGV=l~#L`Ra(uFFRbXkY?_ z(tg3<$;FELWbD}4cZ|PXYz?TB?gc=CYoVydJg-V4Hj{y#7KOg(|F8@44#+3av&{8rW^mQF`-G^r zP%$v)^}!PNO?7a;4n5sQShrzh)dk^IpxwTR&2B%l?33%11}__nwZdvmWTQ}z^YV3i*2v|MCy=)Y88{{ol*0Fdv)|NY-sT&v0`4VUe~AG|7J0WlnZX$ZXk>e?W17xN%GF6;7p|3yS& z;0m;fin=Fu*kgbT}J#14E9ziS|FG&fUYgDPt;ru=!1SIZZjMt3OaZN%K;tP~+*AHE=? z;+$DmcZQ8~VY2ycqSgYO{LgGyflYvaq&5Ug&Jw424S4a-A2ate*O)a+_oYk2=38+L z_W$yL`wL(a05B{fwFUTBgivJRV1$s_MWMQovj4;H*HhF$Y{x*Ib58h_SkR+D;W=z_a?!lnwzAj(A|y>q!w0Rxyi{bX-mj;8j- z+|+Ao(c^je$F|p5eN_7NXs~>(4-AGC@xK!K8CdeS9wpbp4unE67Hlc)8OjXcy=*Zq zaNX*#i?a#Ed3JyM<95Zc*`k*lW+|2pwT2qDd^fm>Q(P|n7X$oX!+`7Z|&dtZOef5fvB!WMV2tBbc)>Ev5;e;9mHoz z#yC?=9?tUain9%Moj~3YuIF|pab0)a?d?7)8lZeQkzCj;>m&S)|8-W*hzx>P>z1lcQ46V1mypo8cMNpPZ&6eqk2LSIQ6+@d9|}?0 zhOdgGI&*Fql%vz0a?54mr)l$>HV_3aHUkK9xgy;mY_5bxj8@vR0ysTCcbk8vHZbSTPqAEQn1arPG5iLs z@PlKnxwh&o7D;nnW-W|0X+|AVY(9eo{-G?I(<5~I`Lsv7;Q_j%=k72Gw6X--D7$}*;67J;^c*xs!%W_?>qHV#4+GamI|8G@YC*fm-c#(b9C`) zreNfC5pCInh@93nZvUQJz7_+_; z{BZ@2ZCl0zAdla(i}6t$$=6_x8eRBMl}0O8M@kpp>+^#^SQDC)+cy-*6u_FTOrepC z`HoWDj2bOWk5TACqGK}yfToK+y>yOz`DQ|s!|-CpTur_A?&~J(?k8}bwrdse&1FTS z^Z3lJl*?I7Bd;Yw(^(cqeoyj9w##0mZ<^8>EPkHD{K}w233KY;LMqKr;dU2Pz`l5P z&DutJziKpkai!S4y}4%jeQP9?BS$~*z^&DQN_^J~BkS?pKp6AFQ->hjD~0m9HCnd+ zA%@k{7kgy|HcTMu&mS{^y=M3O-KPsEEY-=ocNQAu*6!K*v3H%NzAcJO>%f~DmVqS| zh2lYOKH}t3hrQzIZ|+t5uxr+pRO>6dW^$^r1g9pVeDdTofie+H2~pJFGH~^a?|S%9 z20Yz*FYJhitvG1AJgl!XoH`AsOE;lGV+5I4oGV80Lbjv=2mcaK>TAen07LG~cL4|T zY4_$y;-d38n9Cu)=5_DnX(=|rNfDWea6L_NSx8i1Xm4tA4SMa){q1L|cX6zX>6%o2mS+OsNY~y#@6x=^ibZR&9 z&_HJJ|-84ze#itR#;|8xVMhh6b1JyRlDe-ukPazU`dI!binn zBUNeVg^}ag#@$d$;|v)DTtnN!$f8#fkG1F+3UiFS7~AlkQMbc{5fByBKBEmA{7BjU z7CjRNc`D0uubX+jK%u7(fEcv&v$Wz)Bo{9+g8nvVRUU zSOnO)nbp#1982lsFaIbXds?XU2qxff0og1(Fddwp4Y&FGt6k6mZJaIGn)?^nlUPW| zh+F?LgQ6d2m$Wzk*fCLh2fJ5pru1Ia*q$U|F4K8qGrX`cI${C}C3MmZn_7jbq4}1r zhK>{j(4au^irNHkksX!in>uT9=YXs0G?zd1#{EP$4(8mqclm(7YY_1h9Q||iiHBck9Tw>H?P6U7D`5qyrFAui zjUWFl%-OR5gerYCw~4YuTrHD3xOnGb^Pp;@CvDh2DGK!BE$5t1V|oRLPl~b?v{uC5 zI65s^?WL6#ZO{(0d;I*_96NOmWJ75UK_EAo^sc=0Cq(t`pOJFW1i7SR3B*h|`B-=_ z;s?)obLeue9o%HMzaQx>&7(nrKK?dcF=BI17^M{8i{qgtwy(2;P`Gz{iJlfc&i_(b z^^0R3z`?Y{5%<6T_P^p@@q|n@QjwP_cBeuF4c?qO_vH0KRo!OSF-3ntN6r@$!lb7e z%p)qx!kDSrROa>* z-zR$|1#{pL-vo(q-_I@>Kje?(^%>OW-T(2*H~HrrG%nVhtNeE>`fc?Xp6Ze;Sc9$V zP^PR&)DZB!xN;e0+*aAXBh-(@i4fzztHt_i45k&{^J1Ind(J-wZ2z#c@>!o*mWX6! z>vHef;rt~DUHg6;l&lDTAJTq)&|{eQ60vB7?@Eb4FV{546l>*h%w7NKzxddHzZ4%Loo(-J3!B93&Mx6AJk^>ZuM!L6x3XG4T9 zjA3+G7vR1_Zz`9OMS(t1$M0tg0P`Sh+;e4$l z+&zA}>*?0NG+9g!LG|OMqTEHD>sH*X%2p2%5{Esk-XDo|mFaG|Xn%iBG*#NGFoN`H zUh;aWD~+EC9vkKygRfG^vV9tXrO>;It7K-@Bez-hEW`1eK&a{K@iX5rQUqS}fO2dI zkG<_1@Y{9M5rn*IA{lHhxqF0L+NUcom6%W+2kk%3-pgmJEWgo@2saKaKl1bKL=2V; z$TxtsQQ#=b{(I9(v*9F>hY%7VOWCIsrcbmt`_1`RIHPdL+#p%gWXb}l#_E6lVs*dd z7Xk8QLZtp6pR#~{k>&e%yWeS~jNTmJc6LV#i9d1~7N3Igb#4k`D@}#3nviW>@wM2z zVr@3TxWHrPMBJv(#Mf~HKUa%zE<-c=EK9rcZ}S4we{`K!z!ZyZ!SZ`s0?rE*ikM)4 zUeTWO)Bi{i-AWCXL^>0^r}VCx4el6GWwJGk*?fHPP8w&chYlS3mcyUu)1>)byCjE& zhL=(?9SvF+CK}RXr$=hYM-`b1OTs(KF$0ou+85fzEinmls6p zea}Ia(HEEf_jK*N$SY`2o2hy8(E8Mtihsr~f3gYk<&i);v6fkcJ>oY5Dy(tf5Evu> z+M<(Zws4$yvp0OrR&Rzw@`osmwzFhrUSJB8YVsV&8c)(Ys5g=gz0C8*F1_Sx%z*N7 zK5ULW@5WR5!lX{GoyJUc1wxN^UOV|6u)B!lf8XPK4ZfI{0L1Yd{dbHTxE~_bFtHP0_;jKhZx3;l_Z}RU92olzxRxkUnbDD%%$HW}u zjR=t9SKm9Qo0gkX*`&0*@q296rP4aH+7|kQ<~A2XVB=O6m>^OJx8+EtA}WEj@#3!q z6a8i^l);Gw^1TO#30`YE)eX!4*F7Y_j4=g;#}7oU^nF^^Jxc0I$6Rf z3_MIrkSnX9{2?v`p~KGV2&*fMvKn2tvvxu)Gyi^F`Ke9X^45T!ncq-?a!Bxo7Ml!J z%u8c-)^3O;4PxggJut)RXhb8Zq%4G5FCW^p#fG6@g3QyU@5}YlNsqoNI`@-YaM&Pn z@5z;rzy7`CUI>Tbw`jjIvRE?TMn<4GxVlHvk~~MgjT4b~;`iltZuY^z(pFP#@*e|q zccp3$xHv(a7u6NUdjAs7;tOI008yf9jRk;M)dov?T*U32Jt|u##PF%_40^e2)Cyp5 z$d(*i(z$*g4PS3DVeA7dsT^B9uUWfA)^K(ihQXN047|s18WlEK?2U61k=M4 zApZkCjD{?~X|O>1mO=LIxxz)tj?NZ{SQ~YKTg`6D1R}arV2LdKhsJ@5?EG8dvd#`H z6!ASoe+cFWMmec4FP7g4aZU5LZX8w^%;NRs?RLR{jmU#n7%d|nBKwo}*Gsmx$4BaT|VWXM) zB>yyy@e^q^!6isQu_ zCDs6Gh%#Alas=(E4|cW%jHDS@%v4r%{XltqaVp}>+e@&hUE?vOD*a#;(pKX8-Q&5Y zLrB}pBoIUK*<9qp(nRIo9=hIdUp8w18~5KNe}JC=2i{GaSn`VMrZ>_yQB}OL;KF^g zgZO~za=37=>hIAOozFnpscVHGb6LPy{qQUr{7x1H4^k1>K@ zZjEIdJ`L<#<#T_U&@p==Ujv=mydevm$O2nS7_eQ|11@XG<7XOE0q*wa@L897eO^kJ z(>@~%{a&mQ2HnEnteVaG8@I#r-@M7`%VZs3@?gPy4gB&35z1u>=0F;~uIpK>*@%5w zSE9!(1Da5BZH$@2pk}t60DMGoymG!Lt)>+LrQ5Wxy1*^}<8oAi;0ZDdP0CWqzf8wt z!{|`vdG6MMZ5?63<57qWHVP)_eP9S;pT*b~5W{EvbGP({{SRnAx0F2~Shp3`%)|H1c;wZFCI9J*j? zgB)r$D$|`j`kP%O`Q~^}Bahn3^Z-h0uc7GoH`Dgc$e;q|fg>c^Iq;bu!$;h5^ma-? zFWZ0|#!5IjxRcBnOYO($#a!DV$@)J#arvUM2B4Bbl>`N-f1_`|VlukPeF5Rz1X+xM zTYWAT)B-CnZhhRoJAH1YMINrvF4*N6ihlvRZTR`T6)Aa8wBZ$77;m;1IiCXZ@~rvj zGMd#YWvrs?jeoA<@x@~uz=LTk{TXoZB0H}sXn*b)`SySbcQyq- zl(Vj8=}VQJyHT?;m(cN)s&o1_)2DjcKro&PN6*`)>pQ3JbnHO!P9@4T13!7eVT%x9 z_i36nS7#VNbw+UV=UyeuafRshWuYCE9Nhgyhx%M^x;>$A2_LpQAk`3`2+d9SP=HKE zxB^Qb`}+{`5m~9$;mSSQ(^2&8nk%+gD^r1DCq8T1pn4c!3b9*)Ur_4x@H(ED!o{^A zkv^GIoX1#@5?5Ebt>Yk_!Zs-cx?I%~LQXwjD!||Rf%K8%pT?>t1n9DPq(fEH!zN;C z9%Va1WL=Yuug6p?d4-(0HFNrsR5m|uKE+@q70a64Q19OD(um!jacL z{#C1+`ez&dUl29{5QN%ZF#xGD`gQ~C1A*6KS7azB8-nB&t|w5vsi6t+ad1Bwss{)( z$c3=lu)i2tO2qzfne#*4PVwU}WZd6t2$tiyckv9h7)N3hzkj>*4y3-emUGTtwRZ1Y z&os&|?C1-jZFMAW-Lsw8wVtZWJ*|)Cr=g|N9##u91E*rakWnY_pDSjo@#Ms(?6pO4 zM4rtlaBpli8iQNXrD$Y}bTgk9W0y=?gg3p44j=kqxL_i@oFKcp3I-m%9@Uv<6L?e? zPp?-!WaW|TRQr_jUG+X-3C%k<@gXTa(+s#QljPnj{*hZz=yToT2)u_-l->8APXvEq z*aBccl#}uR`HX+_u2yi?8R4nb4U&$$zxZiTcMw^U_qVp2Z@w*7l5NpS$Xs~3!w`J; z$nQ3oSyE3%{^A{~C$nM=8v@}QS{hWh_0W8K0TSgvB{PVUNuai>jD2KrNIXM=fJfT_ zj8XDo&cycOrX2=<>0fc**pk5w!bJ0z>NBgB17)!(S{vA67DecZ@zcHms5CkG*kl9n z9dfr4MPQN42shW23m+ElhUyglaj33ft05qsV`Nai;pm2jb+zQyM<~+tR}%lj6?usQ z+9=-Yk+)tjrnC4_m^`FenI(C=Loxbe;!QPkOg}=FLBLf%9JC z`oSi0oh$|3Z8*=c`EOw*7q}Tihh{I{;PJhq^|o>GgyuCC94{wTGO5m2@5lWf<2*z; zM$`lr?b0rYCNlWxmcTAqhfAd4erWcPy6VvdtSZ?TH`XJX-B z!P*7{i#sMK2e4d`Li6@A$!obMSp_hR&p5>Uosps@t@mP*PbR!IKTui8W*`3@`=}{&i?9^%;+R~T zNiE$=@z|o`O#!0Dtsbsu*#-{(&#a=pH17a3PcdV0|3`D?wi_u##P9a(4Jajhv?jJD zb!PPgR?I)yOP3GYj6*uO*<$pG-^VCkiEfU4R}kQrn}uU>X~Py=FStH)X!D@F>{=5f z>aYID?ywDNh=eO$6>>9F9zd&O1DlQrpZ7;f(rwdYjSQeEi%5sn%T(gM*ER1&fZ4)a zKzcL(c*c|ekbIqp^#clRLwk}Xfk<7WBWSP7n_AG~DEePV{EQdK1#8W`A) z31Dj<3bG_3c}DTmAw3;4E#RG1eViS}^HS%a5lVjJPC4U~f6rD8wcv1ixd(2L=&bhL zil$allD;by>_LANL1%d06M<`K7hj`E^(O5ygzWo0;sifb7no_`b`G8k*BibOkICLh zvqdjg-qOIbK4v=Wp6SD;7-rlDh|xy8`Xn1HAMG7rkh>%fYbt{ykfYhx4?K%WM<8<@x!S^2EuEEIdCHwafPfPlx5vwl!yxLRKkT*P>sgQdy>p*cissuNvg3LXBr_q*SYn%N0C#QZ%-UmpA2%#Yc`5b)nE>%bx zb$`GamEAO9cqXRLLkrY`LJeC>e0aqBwy+$GfcL#FAIV!s0w=2;N7mmPCTB_XdbNb| zUUWh<3(NoGemdubU_2%c>2VG*0@Caf=&_~)G*MOBpwwfW>=A>*nReTQ&Oe_}&jg~J_Q(pFWQ?lL1K^a1^mG+NZO{Tgli&I5W9oT-n~e;uB1B4%$NqV~x1DUd z!sC;NKmrN$)z^LrQh9eJ-l*NSd7|duw1%m7Yv|y8%SK-%7UCEzsJRC7C`YwzTf`V1 zSmMpIXeaRNv;y zDpj$=?V-RaPWd|>aHui-Fx+>KJ6^=y@BCHm?l==2rXpz$^W8Ug%JUr|!lb>2izyus zdxV&Z2xc@`*zaNfyn?LBL#Hz8p}pOSCo`r?lEaOWy6aqgJ1S5Lf6O3uHxqg-ZnM&irx{DD9klSEy8C0U@DwaB z(fW{pLYB@JL8x6I$Dz_B1*&KzTQ667vMt$LN^j`|g=p-H@qqoQK7B27IE7WJ&LrlJ zgzKn^pod_L#Neg|vH&sLeAEA0{<6QKa|noz;`vV;;Kerz(C#oYFGm}ftVzdTaLMj@ z`PyDf)BcPJM%6vAGJDG}@1}p6E-J#eg*T~IEnem_df6l!`sZHPmXBYFeacz=3?$bl zf`p|hlhP6f=w6r{osjEkQU{j7GQhG{17Egv_4Ve>prgwj1<1y49~?Z(uC7}{B!eKt zVROUuoVBR)(XTIh=S_N}G=0vpY_&*?BPCdGKw5EkN`Ej_M)ya1Tm5+J#|PhEXB`mf zZNc7F7RaMPp?B6z90SG1rXq?nzLfXRYFL}D6${4tHP-Ten3pP5`d`Np%>Pn+1W@d4 zdkY6>`Ct=GXOOcKV?#Zx9+A>wWI&;6F{ct6S0Kw5u4C|z0~0gy z4%e;_wug*&+@wAriTS&4$bQ{Ay{^>W1m4`?tW{h+ZMCYtjmV6?crU}5rFa#K=IYTH z|ED>VKM!%9rJUjn{Qcsb5YQ5KmaSVvE+X_obR?%ngl0S8G@LjOCqx8(k(=6Olr^exx;4YYdb zW_m!if)Jz4Oi-%ng6Lc3X{@I~eiR`aQYU_(>-{ z*#$@C!>nobRWR3)SrllT&!RXDhkqYcWnZdK0IKsr@E`y+(GW4<+=X7l&_)-?nX`;P z9+ec>j;f-U-J1W>fJDDg|DtB~o$qQHPBn7gd)T3E9}(*kR`;W3dv)i}G-l-$4hOrg z`r%U1l}hoD_v=jnFqgGkGpL(%5_r%=!r~3K=dgZhHo=m=z$6EV@f;EU_j&U;VPk<%LD_w zyB0ldG%Lw8G5-x@x_=*%^v%r2Mg~VT?A1&QT^(1@bR|NLCUaFtOSuyJZ>y+IMZ=%v zdO`93KCi02)Sm*>3)2e$&ODZY6o)iH6Ik;0eZ1Sjcy)M+De4ZHO&t&3?*eZuZ-IsP zlQ^RD8iSboZ*^VaT}SP_;3s4A6R#g`7zfdYKv*Yc>st_kJM8Ba@dhqEN{75Uf1wGR zQZnWw{CiqduEYbO(h)Pzx-H}DY@xhpW~esu9ZratfenthSs2`Z1b(X!{-!a$XkIzC zzAy%TQcB8a011o6c-1@H73$g7{OF5%QW60eu;h~For`GN%yQhnWRk9_{3E}dTNg5z zIozc^J(y;e`?GjI^xfhxoK};?YM|oT3G>#wN!$b#mI2hsuNQ-(mkVo-=umW;U`P#g zrZ}c)Ifu7fa1`|nPOfVi--1@ueQL9xeJs(pGS98ZXAb?3X`dEX#wXBmS$s)J1mPvJ zv?L7s!U1vBsY6S?@AihV2q}~$oW!RXNORAbi@DX~EGn&9Qw%cE)I=Hr)>Qhg8?xki zRxz8k&ar!e@Q3Nqe7cPE+&@FA`-;LDAPS(1fa!!6Z-7+DYBsyh^CnD#kWvp}sxs{) zF{a}h6j`>>716k3U`A%)bG!x(XWmKkCJFHlFLk^u?v&ndKiYJym0^7gP~#cwSmAmM zvY0Mctx=f!8irD0)OHNbhkZ{3nq2R;4E`f4)KPN`Tb1ow#)h089(2#)PT&UtjP^LJ z?+;Nj5<_RU`5U7JFDea^@Dv}s{r!fQq zBu&4p!>M=vNE-Z?1W?l#iE{u6bP^QY|MX_Q!~*l;hA&dyD3m1#MRuRVWb!#T;oADq zZ*zmq_>TA3u^GHq#5X&nB&_MR%M}UTeY~-MK(Dpt zy)aJRU!{}7t|NeZoFuSpB*b^f^y6_QWq<4XVxnjE0kj+Uba~F1u=F7olQrAFOQc)P zD`GNXpy6uO^)O2)xK&ekLDdHFV81A`x4FTrvG%TN`Q^F}508nB7OGUVIq80TEa#F0 z_Ma)Weqp!(U~puar}__ux1h6)9qHETOXEuGAf&Lr>l6E~u`~#!6HKu{oUQe7p0bk6 z>Nnlrs~(G#!XN7N8!YU}Jd->=T-F0eohhkM5qxz&g=GfigpnqH(Szhgr~d}>)A>Et zPl!=>PR17ITW|SGlBka8iPnt&gH7iEKN3BWK6 zNJ;@Pz_!`sH;~>)0Gag@Q_0Ul^Et)qv;zi{%&lUF7(ewA?#hY&l!w?_EWjaO`*038 z`Ex=tqqG_&Hu!ODBc?LS1|#E6Nc_ygp%r4tZ2lL)$?4|q3WHB8Dxj#aC#0D~rpeS# zps!?E(FwFJ%;+aWMG%VEo{iE)V+x9eJr-dz6|DRVKM}{iF%zK+74LUnr3KmznZP#> z0TG;nNT_Lf4<6l3AWgTje)5Rwf-c6MMm`mxMw*ZX_Up3tZ-rNQgGb9_4Sswcy|J6{ zT1Z^@AbvKY= z>{~ltf63`WO-gU&CL@$?34*+sG1?!G4p+&lxAK>MdbT4z zLxqEWlzL@AidJN4OM_Hjy9xau#$`)6CB!oOe!DiRutF!u*!R!+2EL+v4Tv(W7hN9U zC4lyJ60R9xrAHYkk!McHAkJ~t+YC;R=STLkv)}rh@x<$}1#+0Hl6chnn)py-=XxDw zlzzyqJ!5ear!G0AbGyg3W@in92T>{lUpU(Xakj^uf1Je+cOtJ66rpw)sU-pu`V_}=gUZ|~2+YiJ|=A{U$ z>rN&FtIIIwawoKaKORaS(FO8y{+IKacyv0@D81|2b+JoXWoaMU0Vl;Vt6HH=yJR>~ zX5C$g%Wi--#8`?n6QKvc@}oCKonPOnt#qW_J!aUfIx65bp}wTy+ZW|nX5AN#yq>zN}5gy{8ayt zN4pa-uD?j1@{w%gFv_)Gl2XeZc3mm-EiX4Qb*+s8mL84?2k0V`h`;U|IF7c$g?4S- z_^;iRW+_MD--FTBSod1}g)vKElCooA`s!Gv9Ak|$@@J9jlt9nJ;L`y}RzE=EqYd!( z8ZnnoMA1A0b9x|K(urez8BjOa%>>sphK8S5*rTi#a!SKDzQ=(JgAosdY7%co%(d9) zxXyVq5S3UOK|qb#YQ#sltEtF(hkn$>VOYZpk*j-8M%a4iAx)U{v5wv#jIWK5&T##& zMsw;*{vAL*!KZ2opg|K#m6`=6{1m&H$riG&%bf6Xi!)m(^yj6q-gs{Q(_heAO*)qm ze&jL>l?#yxm!7Fgvcou5e{7aNBjvR)VLuSEO1QtTik3kjP4~_7(4#{(4Bz(QhK#p> zx!WcxFO*CcsWuQ*crJ1fHCyM|0d)m-_{1LnST-ve~d-R%y6e2%^OsV+Bw5;S%k z4+{7*-a)>-yh+W@$MOuO&&Bj>Z~yugXjC{o<3fEuVrkg2(a@`?bt{XpbXtzMGTLxr zIfo|Ib$@XwGgM;hSlG?3UA*f;HJ>Rxjr=ilHtu}8dJbVXoYb>}^Ap1x%q+M)zvnvR zJ#Op@17H5vvPLY95aemlQ-3NjTy=cTWR5?U?4-?+rl56MTz3O9%AXX&DIp0?q|w%+ zbAd`A>hV_z>c#7B^fZ0n@>^W0fO%S$?9P~*d-$4vjZ3q; ze7=SnxJmBVT&U}i^xV++AWj+`&F0S4+Jn`eN^^9gNJJb3GN%PGR!a+jc23km+a*r0 zALHog16Kk;>)ImVTc+H~AFy_aS%@~|)q%e0eUjbJCV9s47@`shNg$JS; zj{bO@=$_#o)9=)Q8$__rW#3JF5>0;y-^r?7enhy8j? z5_HA4x(EK2ZoMIPn!4e}0+p4pDdV^#xxaAcAcb?EcbSPpoW~nDtp%;sw!$U!V+ie( z!Sa3eqAE`ffgN>B=r^Bs_Sog?Dy8VxGdFV!M$^l6`x+%vJkFxEdL4>8Ej|b>KhGh+ zY)o)+4>f8hl@m&*H)IFyxqJ%eLtApf`w`p5qIwE;%ef$HBo;nk`9J>{K33B_Men{s z$U;8*(C}@DQmo~0E*x2}fjOdcu0d|z$=p}jZgwWB=VA&-K<1_M%$pZA0Np!POcy5) z%#Z)j0j?!oE%e|XFNX2BZ2g-^2BnV?@{qqlZe{}5UgXKz{GII!HZ4OE%G2V zm1l_ldW}=A{9j(kfAtIPBLHpK#{w|m0v(t_&)Hw}B@T=iKWhb6^4vu4-dAxWBDB*n zzBu~mZmHC=hBr@U56{YiyRj|jHgO{h*J}){cklIB5im8AX=rGm=8u=%i4=JM-yo5# z{o*pt*)XMtd!@X>piJQwtr1bS4*b=AMpInz^}I)qOmAf=0}f6F+`1?yhy`3DELoRu z!gWZmtJ{3kzfGQb4&6T1++wIT-z7+4KTSqk1KQCgy_?7K5{%9wDQQ;STvNqXJButfzNaNjM09$^Am7n|JO7GH@+l40VG$zH6Q@5ys;zR zbIp>(liR6e=f9DkR4kj1WFg5h#$pj&>}^0WnJ}`q9Bl=giK)gO+&H1d;ODGd3BsbY zyT0x^*)RU4!{FTV*EPni0GlpNprMZqa=SU!M(%l#;N11g(6!pF+h~Ynu85O`iOPY) zJ%3CKus~{eV`e^wuiWs(NjF$UVt{I{fHN`?jO_XgV?;Zuu!#*GaxVhClZ>Q?UZU$7 z@Lc9hjJi>y_jV0@*4LIEw21*ZE>S;T9Pk8bg0GGDIns^}DHt6o(<+f%q2vlt;HK9f zng!Ac8~)EJcfLeF14QSr#Jd7=VRX--Aoxmxi?{dgNznD%VwJti$6QQI4C75WK__RC$^7%Mw`IPUj2U?I>}>^u;-J<6K1z$fP%?(yyO zF|#G5SD3ZMQ~AR{jNy=*rhB3V94nlf(u_?~ZiTL0mZ&rH*{Yt(cLw=&n^PdHzLrv1 zDwEcAl7vjzo}Ex$_V(-RMZB)(m;AQ=yh5S;H?N^*|4Z`=K=aIqR5ReMc)lSIyh_Zw z<9NG1>_f&nq>nRpa5_GaMf;ojAoE1K-jjsg!UwZ~mjdWE8K#;;i~@%OsKj}h*a|)C zZzL2=66ISAvH!LK>cq!yz7oYDXNcIU4=)BB2L_dIUKd>2*y1@75>W<}Y(kBC2J1v} z19|*1Clu|3$oN?;LEAMTH!&Kt?=w^hAvdh+x`9g?;e;z?w)@b|5L#n4J)q~T1y*(E zu&_6|@{#-{Ldp@d%M2)%^!p$=y|iifr&HbnZef2a^-u--{l-oeeRINSKk;#nL&(_9 z)}-<|{(TG`f9ZY&=pH1?HURmoe)Dd8AbrOWsZYq@+?4n+Ffx$XFE{eyivp4a?P*hz zH(%YsfFdy8Ka>tskQ*$YSnJR<7FK-Z3QdPsC4*2EVnB)p`B$tACCj+g6wvnCx7!~e zf)wLli46yJ73S7q8LFo?QBgVugI1 z{(PX6AEocDx{ptA_m6tHjm0d=>)xpy{NG#Ug5_G|80~LwB4KPvbFRUE#B)_N2_NAy z%f!dq4;fU4apA_0Aj0ts^UfdMus*9W_UhwJ3TqZDg`zFRy(8D??`W+4)BWf_t2+ORPKdu4jYY@CM&g@sP-oELs*gX zJKRk;NaXiDjfz=2?ji(Md!R-jI@})Z!POVa+U`V}ZCb5BX_&>Gq4Z7CACO%zrS^6%$3Dh)aV%!c76!Z7TX>4v%wps}ZK1$*%#H4s$+(?th-_YZ;97Jw0`uOgAkBi$-D&1i4B3P*0~5m^wTt zc+M<<3^8s^5xf>ixdNF-%<%r(pW1Wxh2aB$!Hxh<3HS)GL>BYGn2H@L(Qxz@oc1y; za8Mc4TP*e#jXT~5a(A0A0;c8l9Fh5_(xBYV&1rGb&$NIrD+=jl7E%J_|AwaLq72S=^cJ}PzCms*Ey|frQ zNF-hp1)#p!FE6C+kj96Qs0KbwD1DJ(2ofWsa;;-l}aJHOpd#!s~*9q+Ohlp5Dq{B?)+%xQAUk;Jz5#vTp={^g)9`PjP$1djA5kuiMeN<7#?8hHUwT;6x zE=lGIeY$D84IXVh8}S=`M!HiWjh|rdt(GtSv7`sLImCKsTP#1ROGmWYiDCH#;x|^` zUB#FpSS-1X+#a8`EL_2X#f05=Kd-@lZN(uQvf!P|SI02B+t8&SV}R+NbaE7c?{D&8 zp~lVVrNNBtT4z}q^wE%+ z$S+)w&zd(cOheYxg*O!)=gWfd`-5)jiOgoHqA{d%T1`seDL;Dxo`~yLCwLi7e-GqN zqMN-6=|r1r!QR%<&??xI!$0cX}Q+uu_n1$84jasaDO7i6D z#-;{7f%GLX^Ih7{eJla^PJYBM7(M(vX0 zn>@V{86S*bpfk-+U+~I?;ci^i#$ck7&A(*g{_VL87PA&COjklcsWEeIhb zK;;j_9Mr6Z1y!Ke-)Oe21-4d`RI&os=%WhLJ2Qu;kqth_91^g#4Vk&FVl#QWBohhX z`ie3k>54ptZWFW&8N6o>eSzIV>VKvNg!uml8(=^v<5qC)0Le58myGd*_V2E69b2Eo z&Ot0%?8^45t5|#yZ1FmQc=FaDuXP>5#1Zw8Dzl3PvQsGrOfPoi zaP$?>w~#jS8l>PR7J@&oU+d~|TtfZ|{6x&k(G;GO-H}38A-%SjtO%kk?6vS8k7^ZD ztj)YEuY`134Q{rId&7O+mnz-JmSA)0%zaeNijP5=<$KsxP5*9ehJR~`isQ98iTvnd zsMr!b>M;xtm^XRQTk3TS_MqP`CJ2ta+R26Vrbqs(0;EtkLyS=oU6tPj%3M(}Q{q)X zOL0!Nbg;Lo9(EK8}rNKz&-o?|@%wGj|j`3IEbsiv*NLfkfy z;gz8zDqnhZ&rhJjj`!doEQ8up=+MNJjh)YD&Mq!xD8o)A{;+51N6fF=e^v(hg#Z+Q z;7P<*2GGmHCaGB4!aK(EeT7c9$qan|F?g&&AI`KNGCA#dL%NUh-N`J{`|ZRqh~|%? z?VaT?#e{WcR+KRTM0#vd`0Jqt&o+XSoC;z!oNGYSI52nNj}H4dV2ENaYi9 z#WJMd2xk)-pLDrTxlunVOwG(**=EvBMj*rlwZk;eVk|E6j;RI@vveTD@3-|{UJbR! zgK>H5;S#3jvgQTJE@9G6K`Pd?g7C8f8Ii_)UDlcX6UtkjqA-Hz21Bg+rbz?pQjdAP zygreJDf$4*w3X}CU zTzw3WfaQdp>hS*n*#Ycbg%^Uieaf4nL1!6Vo6=`+Ocpr~^|59w$*0c|zfTRm*%yac z#~5&_abL+<%Zum%!Hq9lg!-qu7fN%czMMfPe_Hyx)mxo%YmdT$p z3NYN+rO%H<^#fdXt8*t_I+`(Rv)B=tS=)x=rL`9Cig$ma;k6*j!iRCO4hL3bvawEK zT?ZkJ=QnILUotaxrMLSSmn z7Q0U>^OPaQC70ovVBzK;B6=r9%1YvQefU5t{Y3sLy0#uWyyk7qP?cd!<9g^VSpqc+ zqg|v({w>jA@F8rbX?g^0Vo_&hNk@7jniu^PiMPLDIR~&|m!pZr-UcXHAi`91;;Gws zW8?E>q&IFqCrs_jXvdC?w2w^(!oN!qn=`rkyg93h)I~;h42{!+(-Is^Au9H}%bHaQ zYA_A|w54~A;`Q%DcND~qh0`;V41;l%X~qS@Wo+t8%>6ZfIe!6?uemjyyQu61UQAZ} zv5C8>Mj#=2Z)ZXG2XoOhw@QjR^xblD`zC^Np7ud#r|VC>O79z_lE{G5Ux9fObOj3I z?Uxl-V@0)Cz+KA^JeTkCV0d*HpM{hdBi|qp-pTSG&$)DX3*$Kbp7n?9K@4Ku89uF` z>O$Tnu-3FTF)P>emim*R63dcoBpM_(l;L;fkxqmcXi0_CC?I~X%`qYzSJ3doy|X|rQfoT-x|lFFJL*_@|mcTodlx_EoB~?AD4_xsOf!s;SJ+V zvqsC*&HVHs!`<$G61%*aW(5X@(XiEbtV7-tnQGG~{L^r)eblQ#+<8$~mO=JDH~e>s zx~}sJIo?{x6Z8*;a?`wNtlwl7Iz^*2W@!!kC)+_@<%TPj1u}Cy!?{ulh&Zx zvuwx1EUS&&r6Af+Airrpi;ZyV&9V;n9u%zcd-J9)ib1ti{K<4Hk4^HZrYQ^D$F3&K z0PSA=Dh+IN>zM5v%fQiuSrNw@5Lfw7pSh5he9Vj2spNEC%vayOAHTWAC;SY%&4_U) zCiZmP_HKuyQIvSy1K+&kCmaxcx9$>xVLC6g9h5ACaX${4^puZM0+tDAY&3p;)0&#` z@AWfynCD;4!j5#mJShD^5*{<%sqckGtEM-yqerP zNn$pQm*?7xMIo!k%-8DTCIc<+ZSi%#^6g=R+%S%z9M&tsD&GkqD=!aVY~pixtI1*E z+vFlkT8A*hWEQMC>X#x5e)yB~&;RhhXg~vK#XyiBYno#4Rd?Al0N-UWQG?fbCQma7%6E`Df`Vc)1K)P-uKz zlnV;*3T8FEMfi;c303P5OShN3zGqEgx_w+ZzcaFb&@!t~rdtK2l9pgwXn8r(3k1LU zIif)=PZo}}U(!ehnoEHLy~F6OnN9?MBdVNtg7LHNX=Z-NZ{Gb*3gHN1-*5wuVBF+? ztw^{*;$GD((;3?~ch_)7%5g`t{6Bvn`a%K&KvKMbLJ0CX4rsVQ0jEH(uw@33%*Oo% zS8m4#a=tAh)>RyqEIi{&>imE&#h`%)Yc#|NX3NyYSC*JdlZw|m`!}>Q{{>2^5EZ+- z-XCvtYZ{9SYMaWIf^%c_%-00sxKrkP3Gv86n3PzPr3bSgWnITwH}CxF&=esMR#^6# zgFF2Il6z^CWA=u(2~DE-bfd@}U}WnY8&q}d=h|Pty-;x?rKr;k5oG5<85~28hbW{i z4aJPRq&cqE^sa+OlHwWvpm#+}AI|C{>tY%3;x1iFMeQn$Xv_ST3{Rz55Hnbj4q4$|C}j*k1%Ww!MynI5W(w44X4Q4e83 zV9ZglVm>t*YE5eO_t+w33@sx!?nO-z-F5(@P0h_!fPnnTzTylEi1Q-GT{&R!JtIZ% zqPENd8r=OEshw!;I?@T74RIzJLZfQMCt`>iDz(AcRKWmAv2mh-w#yN9uD08sgO%1- zA7|~}@OJ52XFXg3cLjr5rn+*u(d)LK@nC>a2EItqtmzwx+^ULq47vl0`c4s)Z)D$X zilaC&4Cf52tu`_BjQX&Owvqk}-|m;7M0S^N-y;A$)27XmUdnMH1PI|GWO!L!6cXT~ zD&ARd@BhH^?1(O?S!})zvZgN|om&erO#ErBs!i4F`-?AC9hTXf;|5IlMmq=Pyo`Y0 zJGUr`=&6CELF;J3*>10&oBV_nK)cz}s)+{?{Gk5d2IR@*@<+!Et}~ zrFQv$2C+Js#}>yE7y@D3xtHinefOy(t>QV{&G;dn$Sf_2Lzl-icCpt5Ygh`E4OgpH zihlCTHo|`XT#G%__SIvdw>F2)^{M$mb;F*#89bv1R=F22GseoW&DmCv!PZ9xBf>9e z)8e#d%yIdU4eQJyBhtfK($;XL}%vizQ;=w_D;lO~YG=BgFQw-W#XQiViyZbpQR%rYYI$v)QSuWhwI0RoRWus5N{QWFfvZ?=jDQU%YJs+NW zMX`;j&Sbw91-O8r26Vc|Yt69MG)tkRB}poiHDl6;*$#zp`E@d6&e_n6+4qD0YVTqE(vASoP6S_40yqyb>QC0< zuNO1S0& z(9}1db(EsoY1AuLVqg&`e-1h78gfFp+4CE2L)RUGWYUQa(YsH3NS2keTF5;9vjEmF4u}8_ls0VsfSyw*UD#kiMWa3|j1PsR zM6@;OB1QA@VskF+;gI*Y%^F?OWT;x}2~kc6rpzXvp~+n-GB2RBi@MbgzyV_bx|iNf zOs!i3ZjPLzNL`~hBfZjPYJM)FurPb4%-+iOgJ3lg4dT@dmB&1H=$g&o%Yrs;5mG@kR0T-u51?EW6k z_nDW@s}KIg9g~az9OF+q4F_|t^*95AEV`nPdD%Y*GLj31Qk!jCv!1uG@G2u`8DzT6 zCD}E9Gb2G#ub(TlroHZ?Sl;*>B!P(9uUtLsiQv1y@~fC|>2?@aJ`(cIm0EG4o{lea zCqvYW4e@m{rvLc@?-vJT0EftyBnZIOFH{sV>l&H{UBV37Dsw0O%+AJ%Q4(`4ULbzi zHk))8`V#ebwNjN1FXy8UP=E48Gn#f;>OIagI17KFxSr+%UWCW|fpW7s%w}wW^{l)Wv$(9EiX8=v-0%5ZCygB!t~zJE!|T@yD#(d?h_n;gdcYG`3uOIg0yf&k0B$v9wF% zh6ykeCp<#zhA1SSFD0%gNFIfTs*`B)q?KI5h(8b=_%H)cnAZG7WbtoqH*G6xNNM3@ zG37SU3_WY>n&Z6{ZSc^j4wsWgR%~=(<{y!$q*AS6SITz0)ZPC=ot|~o-731U3ubu9 zdMY>&Wnqs{4}pm!e*{)hQ`24;8wRg*JnvnJ+OB}oea6hZfK@bWDI0C2u=u>^sp=t5 zF5N1cbxYf+rC&PuQ=1715pmJf@#|5|HJ1%C{SJY-#T#`LkE9F`__v3{U*wAfDu9G9 z{sIVKKr6Y=eF5>EZQ=UNpt9%jqHK=9@OYphmlNLOC>uuooQa&rF&$qOYTrAz3<76$ zmI3X__wzZl`ew-PtPJA#J+{7AZ*T9Mk^NbJQesM1cOIH|_rR5~+K4yR*$FnTngfU{3E(-}LHfT<=6SiiR`RUjjC5WoJa@bEdxol(< zb6;VwEjd^BfD5%)Pq&&u)IK**Vqi0qF8?O+jiFxf)2JQ>tf4< z*KeZ4X_iTKq3kS{(s=hbiU(BpROlqyM{%+>%2p=m4`rq}O7SkF zzn=m^AuMKK&+|SInyLscHMxX$zW0-V9YPCzARV8dVW3TKmkHWg1c67_kjxhcbN~l&VVYKu&(=3Dx-G;{w_n7CgOEKR z);*C)!YN1L^~~@(9^X6fKFrvrIl^zk3Ie+N6eF#+#q(FrU#ax2d^rgMn zoZ^zjr~S;pO0+;QWXRT38=L!_8x;p`aa5$BU02CDRjT3OFjHaexrRm+m56Z&S3j_` zN~f3A3IM%-M@NcX7-|1QZLn{*yCXeth<}-94MJ3z!FtiP)`c8CUi`1M#CBXpG0tAl%bThK^|6!uH*c4vc#x9@A)2l5 zupbT|rni%AyaW=UWNXk zLV?>A8ZO$tFyP1_4;u~Xy(1+uN7McfJ7{b)YHZuKZ8o-T+iq;z zHX7S%Y+DWf-`{ife)1CbnBC9L&d!|YcM+jd)zYQs=ST3!{%CtkT#xB~|F(MgpWjHw zmUog{ALa(vIAAmW)Oas*tX^DfSp*WJ)Ld0RyHZiR@8Ufys4JCk4<7G?Tx^JegN8_u zXpbW8qGG?#U*U-UDnYqwGTh*99^4f&fdaQx3LL)u;k!9gY~!8gLGS;uJixd<<cWT;`l;B#3HoAsPqk4%t`iH@v|CUsXDf3XJzEQPL_s=MrU=PK*CjbmZ{QA{ku@*N(278hGb#%S zxTSzvM>*sCpm?-@zE$eAeUu4Vl#8NmZ`+qS%o^aN`q>(4MMw)VqM#zQ{L56;uS>@O zTsmbO%n_jGGrbl{;?)WVPj!xfrgeu8#_D^>rbc&KF54AR;QX^m*I3db-RlbjFJGyo zHEJ;}iD;P~Hdcy7Hj)Nxy^+f`T|C-f?*-=EKT11QoJ*s5n*@iMTI$O0*JKsmm_=0wYxWc5KD*Lfo3!(z3Re*Y z^MhhKOISuH55g@LLburoX)vg>YMH-%YiPosO~)+FsI0i6wI}Dry^070Z9!yEf08iL zGV;FcCsRPrSuTh06*@E;OtzP(4Qi0>B^zkF`d9NANb?H-699m-$ZHG$U`1NWq0W|b(ig%9;P{6{{UHK_|DGHmoVz8J;9B9;|**U z*^s`9;3sK^s8mBPS`|fl76&}_^fQzdcx>g*G!(bpv8#s=-rt|af93Q~86#lp5yP)` zk3oKxE)8z4HC)=}lEAUb;hwmvN&sIBy67`&hV6!y2t%u<>7lha^Ag7ghw2FbPHyqr+Cu5n_*<2H6i1>NHM5jN1MIaV z{-h*mHc-Vwb8?W+)8xP8(fIZKQ0g3Rlq=M7f17=hkR(t z%)@9+!=}y`KaHFY-H8tPz<;|*-B;{r{ZwU|CR!D+^*3(_E&)k8g=o9KollmbT)u~m zheld0HIET4jpgj`-r`QX(30*@HmJCMw??tMG(kqSKX&v+BT>21=v)Td zKONh&D51&!)P%?jYkIquLCiC68?^sR^Z`6P1nFtToJ5THsuselT}6wn8p-zjII_|Z zE>*`s!e*skWTMQt;2YDqC5QNTQCFgQQDe1446Z^xYoRN<(T&BY{QH-q$>qYjV$@p! z=+&!#5upF2@B^SQBoL+nD66*Op$FOh;NAI|L@g+7w%FPh;T=N4ONr+O{nJ}MH6ph#5qDqIK;R;&OR1S-g66(VR zaNr6m;bsoE%?uyOy)TDx4@5&97NjkzDf{s30}7B*DhiXY*1>oasL=6lf^+{ABU##9 zt+6c+5iBK_TiJ4t$ovq$c>rLq`XR|#@LALY5}p0!!B31mY19iOoAFwZzkL_~0>StT z0viBvSNIYExE)sVgdDC_y!0Z;^U|Y9DH2r0yb!?|mv4N;?4pl4r)%`QHT7!Y@rV#l zw~E+#CO91S*tq5S=bJs%5jhAW_bfQe*$X?D$4d=h&p8ViSgAPFVAix-bb53EB79rF|Z3?-RiJ3angx`3>=_HMm$G z+?G?{C16%Al@|m3+aI1jSH)h=Ie)hn`91zQ^jdv6-~t?chd6=(pJ0o}bQ75)P!Ow4 z4c}{(={zPjhAWRw#E#@&T;kg-@g&8&7XOkc@iN!}3ujZz+85M}+oLITVgF!=kbkZX zDhbgXn$F*|;THO=io8-(4z`ul8O)qiL3F&IYH$s+soL3V?!A%+&uY8g%G!3cS1yce zeyE0*+o`NEG@uzN#ilNLAEitexqci2o^jp+tO{VSBT&Z?pN*nA(h2Mzm5Uoc4!7Uu zE-kF_E|d44%VpSIT^C%rvz|y9naX43bzjU}t=hW0d5;TDP@Yv|wm6JS9Lgb2A7FWA z@4H_FeFU}Cb$?EqvU!26mbd?t@U#1pzynB($}fxpKfI9!tZ6VIDiu`lNHd9I(=lw%Ls8MEt$mL(%aq;t;#7?~KU)rN@7|n6(D3PiY#`N|{r54>PsC(u@DbceOZMOHBq=xctshUj}E=Q`t!3Rxu z+?-h9Vo7z0Kgeppfo}!_mXDxIN^cU^miy*qod$>ww@$_C_s@^qXMeA;_^bqSV~3=B zEFSPor^G;&w|#j+m&&<8 zKS?F^6V}r~c(~NAq>wzK-b(4Gm~tXrkG1hZ?NT`NcBfT>Myf9+siqenPC~enG|rhF zV+PuZ+zea?OKkX%CtI)|IhENa@ah2*W=hy|;-ip$uNhqNU46{KzVsz=0R|eU1E~u< zEPUv|&AmtTYJ_~gEEfb_@~bjZw7}MC9-{EE58YU@OoEVU)<&{3>y=r(v*$ZZXk%#p z3bS;Ze)|HGyk_(vQ2@@7Q%{s(PS$scWoz#%iP{i7l_KzjJ7Eb7n2F=?CFaYo%2Yy_nrZmRD^J@63g&?5uVYj z%D#4JcyCubS8bgUyPLd#&NSI4hX*TOjtuLRCa^Y7BQfewN2-d$pHQK|KTStxH)*nH z(0GHgqG7e?H34loh{riANt4%5Qad z&bQ_VS6~s+Xmh1iisHmqbOp3&69+DPW!C%afZMqeiI9?zLHCfVD4^v#L^;7g*>zti zVISW64$&N%V8xvl#1mJzWu)@}v!soNMMP?v)bFm3x{Y&E*c8!! zTTpvFzebV}Fp}5D#&duLIG_Y$xwVwRWLW0237HP3Hi*YW_7-rl&9hcgW+rmc+9vNH zpQRjs-_a`5m~O{izcgibD4t=yZQ=+rI*s)F-B3w3J5z|t${C&qV(&)SxX*KQGqfD7 zJv4Ysq*Z13MRlss#J3;dsqnKxP_c=LFe)GH+xF76X9{lGU=<`xOwYKf{?Fw z20%Cr!Tm7@$;P2Rj-E6_#GfvtHmB-kg^hC+5cA*{F|n7&fV?Ui2!b*l96P(Ld-p_B ztaNBU435qxso4wdp^3=}pwx}d$F#rQzG_3xK5kd1btm@4`dHHb+f(o7`(;1`Fz6Dh zE&|MrA$5kei%y|Z)Fu90sy_UkSHR{TGF_?6ebysUK~Wx8j}6s$c&VF9DM41G={{gt zS)K0>%i&M>Zj`0$x_`NJa2a2-%+EYp>&db?PO6&t4P~TfKL+S6xWn8;$AWPfTZMNs zcrtCrC%UIIM?WyH?)d;Bl{e(4exE{kuqe6XD56oO@A02F+9Ph)Zrb^Y)#aX>abO3W zn(oW#ti^bdfaY#OS;a@D^Xqj~#El~bX$2bn4I0+K>(1yL4iDM$MHa2pH@6)Q8|2tH zd#)fZSJKkaT(Nj1JHz&9!Hg+U{nW**!habT_ys`xe~&Xk+$Z4mN9zPVzz<51HD+eX zGnb%>s3=F#OsR#(?7xm$w&Eact!HZo_KYb(wP3(|EZiotE-&YkfZ#(36Rhfurqwe1q@X6YEEik66fe1n&z9EK&=n-WH!k za%>1en-i3Q@|Gu1w8LCV&IN0I(Sx918&_H0-)BH8Fs(%)9n~?Szad4c)uXwoC(K!> zcjgfUE>YBO2?P{;U9+ckN=I0nr-l+|uB2gTn6oraC5<^uC{-pwGkcxx71x5|N5PwI z1RKsQ1STFMv!O6<6OyZ)mA(H3Tj-Yr2|%Lg6g~t{grDYaIf9$uav7U=ai-S;_9E#c zA`HIN;CD+QasK|UO)0qUBbsJ3IQ(0S!ucP}$OSpGtDrxb2yxA*`iEQVorKoBXO(Bz z80H_7h+_;}4+oB)XO!P~6!6QcysJj47S^OuTcSS}(e@~oq6T;Mv!;zZT=R*x(Q?=$ zc`If`uVv5_7+jft1!~jiCi{l_^E;z0a^h{D@RSFd2!KORwqhyVU$ebS4L`J)>uaaq z10fwq*!ZWst|%Oddd}Awiw>g7$sHflaUffbLmTB`#0ZVgc<4Cf*z_o45l@`9@vcnB zClIW^qg~rC)y3Md*W{@G@3cw87Y8YTW4cqH0_3y*%@ffnVGaiWiKC^rk-^o3SPy6m z1PL`(0$-JmI{E7sCJ%j!Pun_Z3+=@{<9(hy8RcNpFz6Jhp%7}7js5)mfc zPermzC-1rMomTSFw-GxmR}hRw;d6z4Ehyky3SI?;|9;4*9nmIDOr?-wdNLd%ob@y|I$H`x#-!5@I=_dc7g3TfU@SP{q-E9 zH`eeU%n`X_<#^8g;{h3$SIPQ~mClS^c`RynS%k{n&>Re=RL+yLqRDc$6%%i#2}VIq z@~P^4&> zBhE8;0FG8?Tuf-OM1{cN8V| z$Pld&(%8mGFM6U8#JtkZ3AX*XzbvH1a1SD(yApG*3}tbJFuZ*Z1~QJu@uv}GFS~-u zNlJ8bF*LA0JsW8M%$>y;1T9#o<|%Fmf~=TOfYfr{1emVo`t_MEp(1#Fz&8Xya;ckoMoy z6=lyzsUbe^?3ov1k7`?5?6FW_I>*%oE0UY>qMo%t{c%srt-eJ}h;q~-cDRu+`@4^6 zsL8iLi)mpf{mtCnnt>!>yl!XbT7oPNynnvx^31%79TvK!KSeawN)A2eTOp&^I-^D`7_isP${mR{e>q?tmW};O>qh+2chwXLgZf{Etbv+#j-J0*pzNLKBVEH!PxWjEb8q&(z5zd=?!2 zhB)t;Y%@qh?aA870rPYHONo*z_5CODw;IbZ9n67LjXSnkBJs4fx&+lz(2?p!tY)TU zAeVoo8c%tPJf1$N{(e3z(t{PQWzP0vD3Ge?gPphT?dmqzyL<4Qx0*o9AEO)Sjj3q! zWBf|3e%Q?U=rWPMD8p?*7jU|8wZ_TF$hj^>ne)%&R*R5#!Z*&AHCKr@$z5xf1D#lOBFC;$-H>mOi% zk5iiaAOU#|Z$`$~u}GhRq$-i38!jY8jBt=SoJ40tJkzFNAB~d&sLLo`Tayx?G1*=` z54zY1V2$2v&L3@%dwkIv$X%fw4YgTm7uVnwkZ`&<{&Fc6N=hK*KZy_Q%Ba2BS5~UrNWQa=V%&CqAaDQ7d?PX7~?gS2XK2H)f`uzyd_|qP&A-g0NGnV3>KDws7N`Vmy z4aul5955WXMP5$)q$q(7C^-TI z>);g2pD{-ngx>E3?_Ld-v~=(9=L3Yhkz6;EIxo$8{h zL#`2$kBPLQ_$AdV0o{mgRQKlVvT?;by%mEij2W9WeXAQZqw{O7GL{@e4RkW8GlDd< z1kZJoT!>ui?K>Y`QWQzlk(ZzU!Yb=afeN5-k!k`Bn4>`Iieel0p_?k7a6Vq&=nXfh z+cOS*!?-K$w2tkE)JIczOzB|~*_O30iQ>ww9E37lD-~%jZe z2^h5;M=X)YTfq|NB|_K#Q$T`7*XPaw^u55d5LVb58hpGw0yS@B%@$hqKymIBSVH<&b=xN@yvovVlPX zdE_}o;46pZV##DwG!l}r(H$Sn2Z8Ns2s{*08WgzX?X0Pie1x;lvoD;O92a@YURvg^{_8N24zsf(f8MH2xtYnZvKWQ@EpqfWcF}IhVkFy*j>vd?22m znktpze0lt2GC$q%hUQ1O%OZc-DG?trmLaqAYo%Y!L zy`^6wv;dLOV1Giugqqek(aNGZU~APTmEHP^!IjQX_`<`qt++$k5Jt|cF6UWL3wLc# z^P|JIyPrKqJ6GGBKz;cek#%(q9jt~!ESA8mX|x9vpt4w8)Wh*s6UkVTx#!=X@ReR& z4h_oh`bFskojk?dRa{kZJmf<4$^E@yTP;H{8`7#56PJmIO^fWzs|4L2Y3vJFTN+2a z{4qHTw@sH-)cOR*afar{`~_a~@|x=(!|>qjztWwUw3zzb`4o+Ys2)>p%5;M$K>@AA z6k5Wo zi-QipQF+i$0qAPQB3u!KG?X1>n|c)YGwg#;@qZ@3PH9;+H3vDJdqS-~cRqx2tJh5fwDawkjbS1m%&)gS)R z`pM~tsANSGflk1vV7gN6g$q+f$eH#qJup?y_Z8+Ts*W?7k$Cug_%kY#?WK6gyHbyvpo`?{1^-1m~XISQmtn)U1>x1FG&6Ga^M|D`* z;AFOlwF48i3gDmbW2%}U*eeBBaP0hiz|Vwu&mg(6v3XJVJf2SI0N0o7{}*JnUl{ZN z3}+@@s{h;Q0ONN5<4vnwXvuSZ{x!d}#CNSV#qF}0%2l8{rM07ianP|yZJzcsUW0}5 z2wDT^4tYjHCSe>TH@!nx@>>aFs%a>AO^jcx9}X&l;X|QpI(=deNS@(9B+V5=X|dt% z5?;e&neTOwUd6_gGRzKA3(Sg4qCJQH?Vz%YH$9#;L!|?=9D|jTP$=is5$lkS0G%ms zVZ58(d=dHrmpB7zWj`;``6!Bf^>iOz>tNJ`INBB484iuWK2YVQH43`=Fq8Y}Km%V^ zUhrqdd!pC9#qd=(P}JJ{`%&|>>)u2+aPdYX!3@kD{y4$E)M)&2U;sFf9aJp?UTrk@ z0p46Xn7!RTiX{8B1G4Xi<~X)Bc4Y_*(gI8`cZuIPU*62ukY$d&^_pk2+yAiL3f-f? znL2FNi+ePBLfovcM%3r5Z740;V0An@CuFcD5%7z+?8VYWO{gxk0&j@ki!~)AoWS+E z>HxjLZAWR9)H=c~my|_32*>#kG_EIuqB|Ga> z(7KgFm$Zrw?!Jc$MQ!X>;KQ^(_)!r0o&GVhwJe0{M~B51Y#tQcIgmZ!b4f(=KZ)L!F9}9~MAa>?4&d=14LgP(k)A8b zy%ts>GXSba`to)pfV%s+hygC|1kVIzA%^o7|FdYf&ce-5L1i#Y%Gp|%_8F# zLYBe_yfU630H{`Be)% zKfBeY>ST<|krNqvY;vpQ>>mRccow}lAdWCpw{*HzRUUWUp~KnJN^#Q*;#{$U9B#wQ z3Zd>WICp-_881#csOYq}joDZL9?9ce!)5)g8UfbZQ8%Zv^>`t4g->~M1+Yn|#K}Qb zy>?*$Q+v5T9ciun#>T6f$cV^B8h6LX40W}CxanEr+Thb@S=%gU99x zLH~OR^4`x7*WuAAXYh!BG12qozzlE*WCwQwoRTN4C`G4z4g%F925IktF(Fy3A>i9) z??;Bbo6@rbG`A>T56a!Pr|f+VK@*|5iQ7ms^XjEJ9iq43tl1as>&*)K<$jC27J$`= zI;)^A#heUPQc&?@9fzyOy%4>RrGCu}KD$Q%gabwQTXnmy!i|0jIUlS4=qsdD%a3T} zn0_hE?{=m#S~z-*%4H5vnds_o8RQO`$;Q8 z=Sk7YdhQC#W(9VN)p+r@Gt?dyZ9@#FKbie>yT81KVa zXzI{&*)-wKoe30yB6sW|X%(o7PV?)oQ1ZZFjnpulwSrc=mdQe=afRjTBp-$#$a)|u z_GR1>D{=<(7j0ED&t(;i(QRCdcSp>NibwM>jtE>w>R61wDcy(4?&X@vs$r^qS|X(h zl{2A9FR(M-%RH%E##zT~mQAXf(mei1MmbhpET~gY_ee2~+e)^cs))POF+_7L^AlV4 zbxyrd`#%-_sV^0FfXWeK6$IdKCT&n-iNWOCnJ7$%k1?A354AEI5nuiKv-t@Sk5^xf zTE1T$0;jn#1_X-KhWNqOTR+ALtUv0Yso8-)?wsFgjMV=2t@K2$D+TIf4dGHxj4mmc zeHIoF_4GNxL!`sVf;l&+865g+IQYhbHk;da+%mmg=u+@GQ^A^HId4aum0dt#kD2V_ z_REHB_4!N~7l>f^6#GfT2JBDT`tY!}nM}W+d%c zm~&gp%9Vq2*A~H4)aPC>WP8q^G|U<97+CWsr$o93GN}d|%UT`{YIc68 z>R3Wao$$+UQ8_tLx$#`O(XH5_xg;Ek4f{Qlv;LMXJDd#dfaCQGhtUwxV^3{})YIbp z>>tcSifh*MK{LM$7%Rs{hZ97CfZorAk*XLYL5*_qh_#>LuWEl`+IFc^)C0K$eI8Qy zZU|CQM>D!XLJft9Bnvs@C83gI2I+NI`sfGJ+Z~VF1`g;6{o`PUB=Xr8o-Sip5d892 zzFqOZ>Bi!h1}8vc_K|)Ju+#<_-xHYZ&S-W`m5RJ8B# zcBmGcwL4`1FJo3<7f#)cq;HBa8P*?tZlmm~pj$rh0;3{Wvyx|;2l>IIP6JN6N+*I? zxvi!zf;IMf1h->a)B}WkPPdyuao+o#2s1bV1LausW7fg8_}tXs60Zg0fIl|RRjrUn zrt^Ekf()TmH18`UWgfK{NXicEOuW;$Lu!QByCeGj12O12KHhl7Ax5Zmg|Ld;#r@X0J&+$bg1rc8-%0LYH9Y;rmuad)tm|;@Nc5==SzVbpzxYYbqJVn zl>^Gjr|JQH8$xQQ5l4TrsUg6>#MViIa#RLsml2ZN6db`{8zT#KdE$JG8D4NwlK%$s z=Bv}OiGidRcs$gbucMqln7;tucR1h1bXAT?z5cklY*UJg?BI^-bB3P=Im+I%wPlpS zAW>D2%*y(vvJOexLJlO4y{ifKQuUJ(xe*F8E_QF>0@ZSAVN7J_yM}|VIQsV(!dY(g}U0lz6YT( zAf778`}na$h6+v+<;(t99qXIH)`jEmG{Q?M|2Y8dd^zv{9HOVrN&x?aNxe}|6yyo- zoDwl!VY%cS(*hZfxZ(63l9#W^tDrTpb`<*^?YQ;`U6uYKQ|y=uzvCBB`jVUgLU0^nvZ%+ zg?ly5;Y#x#PZ~bP>QY68d48;Ba&|*mItUfn)oll(I^TIMClwH{bKyXDf$EE8z(H+D zq)|vDR7+!~2IZ|+{48Mn7Y+Mg2D|_Rm{Uz#fB^{N>vEOStRcd9=EV_5X0ZI5;^)dL z;^#(_~;RZqn?7kZ8Mrz7l8&{o#F0lepY<=q@_i~Cs zJ!V+vQ&~?>@1WXSJzDSF2q? zlTvk9tFCB4#*?)$l;DC&Pc6-Ha7NcqUm|NX~ckoK+8y+<`oasHoeY~;l~f3yzIc=`O!zH z`2QLB9e)|{0Sp{6u7&{dc(JKt^@LZycM$;R=OO02B-`inJAg`?JB*WK@6cJ853F$& zat(Z3rzD<~w}!;$WZU()hJ;HWcw|!k83T1TBzuy-`q*BMjqklZY>OfaH)|xIoBq32 zcf?J_>y9N4XEFjesP9kBsC2cyDSMi0g=^iRi#5w z*98LV-mU{78o9COt&79vUAG&kg47Zg!tA&o4R81cJws5ANz>$C}+XoSYgP@9d zru?3eB3?Rf_0p?)R#W|U(!p=YK=0g3g(BsIv}dh;U0!4nBhkUL9J@F2OYdx|xK+xy zcc{HQDv%#%!A$EFla$hV0RgtT{PlS=F7vo6GYumgjW^axzF5&+X%`s5VtRRY~)4#|N2$meMtxcBy=9>xB+Jwfo2POeFY?A zA%!E18eh>^ug(uxQ|Lr{@iFmFsMI$2DAJu423h%I5+P6^C*x-sdfm8WoH{U6(*%!X zU!PlJY-OV+B^4FtmtF*wGNJHC;Qp#>MH%B3)GXHMAgZ51Xa^rjUe;@Vn~c}BxH^_v zm_F?E-;w_94Hh^usKl1Z4YThXls8LBxzL&eh5rI!T5w1%HA5p?DnR%e+&I8pf00%6 znQ0P<-EM~v*31VusV8)5luw*H1@6>y-k!J>E$!FK(6}{RiKT86qllqcYUz)=>>;2( z^F>e)PYx!ASs%)MFR{+1ppigv3Grr8OtiVn!|Vvj#TF84*Y^dH7FC*>GRNq3@&=<=bo zB1`-J#4iXaCF2AU2F0p~tKLmv&@U3#IG6gU{)W+m-HKMneb+kAnP2mnG~GMb5M6=- z$%xbD>2hh%4dTj_8+^;V@oxh0_e(+;AOWu8LLnz;AF8o!Hqq~Fz4t!*aeQEvZZT@opmr;ADyJV zs>uPl>iFQPPT;O+Inw1Ivp-^XrcYk}~J`Hx-U+7g=IlQ?4vk@?MZDZyO*r7s(r?cUAqp zJT;e!^nbI5&o2ZK0Kz%eAr#=vSw6{@P%!P=Vef?{gjIV+sIm?oWsefdVk@dLtgS&{ zlONVxau(-wfoNzkuY^fb7%rwFYkUpEs|Az;EyHQ+!KOhwKp-=EL)$SGHKK_2zMW^h z^Ry+CZ-PuO@q>pG?TP{DHL{@W_vi%4^~)J-XtNdHC=7;br%)ohLDaF9BPC1{YEAaN zJw_#_5SbGNIw>RQ2Khn`ZAx(Zpm9VdAWw%ZEvI>{P3zf3pV=#Q(nr znkc})$V1B+u%oQ%OmTBGd^yZ>x!!>TGh5O#d#4-MYwhU6lNGtJNY^(lh70Bjjp7_o z{0~?Zf}r>VC{ygc^C7q2=1RwhPm(ck9LDs8DBdR1W&+}-Uukv&8ui420aBv%w0X6N z4kdPGXR8cstKklH%OE*ar|#zu29?E_xw23yU(16Q!W~$GXE~@vc|_>yWiH6 z@_8e@ksOXda6uhII(|LYzL(mB9hv=b<2S%SOrie=;0OK%AO-*+gs=F&t{vsohmoP6 zRY*upy20hl=w!x!?kUV~jc4(u+G?~aMsJk1b6sFRlRy6+t;KD}6bsX6&`*oRh5H#F zP)_C$-P-Mfxu1&s?iIWDUBNq{!Q#|;XcFL&SbzGla#UEAdd zW5U&SX{Ts+!MSA!m)u+qX4O|wptU4V8BY%FfZq>6@!~|(H}!gdJ2{2X{>|++vEde< ziBy=V3IyKoccJ}rcIil%>x_pxxkVo~Cvd5W3*DOp5AC%rBQ(+;8gl>gnAwaIoFE_w z>oE_+aCjIxfaKQ-uW@T_cc@DDKL&p2F9vY{gH-4-51{Qd5Z2nE53|t{6tffkD3D~q z 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

_$71dTVA9oszkdmUI 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 UEI-$x+NoN^tzjb8ogKZ}P}i=8}yUzKjjt_hwr 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

M|#^oE0Zc8WcBE~Ls0gT z26)*-l{mWg&m3Xx{<8 zUG+p!nPgc8={VjCZQi)xeEq9YDLnA$cE85(m>g}4OzxKjpPbi`0`%~IC_*$;ZUE4! z`?srOt}n%G1E#6Y;@<)~5MMF;zt;*?D-E^?!N)BhSHp59vVj+IOjc17Lj&fzs343N zsoGy7M^A;ky_BxF6tH40JA??HJ`1m`mYZb~7~ysLe`>n&aH!fo{LIE+n6YnznPeHt zQfMTMv1F?(#gJtvp$IJ~(y{M*N+?SU*^;TD;3e(6U(a=& z>s;r)pYuDbKQspaHF;TxiXrH&h*0As$M4}L zg}f^ynBAhb7fhV?6XP4d8Z{eTZ+=*Ju{^**Xg}Td2BdMk(Ff7{<%|Sd_3!ImEXYn# zJ#c+jmi}?T+00+-vB~u;-NUb(c^Gj;hqGOGuEpj3)4N~Z|H8z36s1UJOyBtgoO)f{ z+W3`)-q!fGl`kUtJ-vE~rxaCyi z-PTtUN&J?mZd%xE$>=SQo2&h9jCJxsxV4P5BT$3@HGU7=sE8K;<^Rk{`2X1yXIPcp6elO-P?Rhk-F)@jm&rfPilp*V(54m- z#uH?$LcGzr30<1OW<-n3`li~on8G7@VEUTFtc}3Uzghb8wjpLrGau7;4BKaCYD*!< zVmb9eYw^ZU=Gr0-Xsfb8)~1CtDRW7VCOKb-ETpXj;<75wMOKAn{+wI=5g(}*4O|1> z@sgM|Zl1$h?fnrR<;i%Fnf(veZ(Vd|8=)Knb}PERzbm=4Hr1t>y`{FU!{p##z4Env z15>+PX!7@J6g?4hVK8@_0#r>LJ z^{$~LAE~2^67COeJz70Z`!rbxSAbG^p9bs{?3pF1Z6MnIf=n|VdI+sns}LGm-oy;TJo3r zSd^dcEQ3?2b>iV=9>GQbYKDc;??nQ8_4WZ2`uO{|Q{vp#s%XCj>n{~n;$M~;R-de# zmeg2f+d`DHnttJJ%~(N@K7feU_x`oUp}n>}IsjoRIxYNG8pMw4M37uggxo$C4Sqbg zy?8Ho*~m`9bwj>nPO;rKx<={5cRglbmzT~5^^ zZml46H3Xpjj5r2+nHsb?z;7{=QEd4#F$*A^#!O6s#IKu2a;k}Cl}vggQn>hfKsDMf zFWvi@EJ2fLOG8a2_H5=Qm#6|n=1`>_d0vH!C@FwcQ8sckAaRO4;kA1e!xTv_9(XJ} z+1ROdfOued-voWVq;9;gMb7E_e3|fPCeGr%qTKd}^CaZZFY`H-`CzV}If-KP)e~tD zdqma`Rce9oEIIt)i|v9Fi5^tw*`pts-C^Jt`pSX^kPkm(-2td0yh?2b|BQ11IAOJ0 zVJ)@*@y&#~4M)_qgVW)*_T(JR=Y-oCrfek?C5GNnIuu;mQ~5>3MK+cF{8tT4^OC)>2i>S}#6)b1lX zgYe!J`N?H(lje%8qXh?&52g=@D)K7TGRGO_><|o`hWGoT3k}qxg~tzA6Urf_TzV@q zanq4+ezsDGSi2*Mq~*du$F~BN^a3AY%M$_&`X$gq!rX_jv(Q04-f0>Dc(Zm>YEs)E z1Mo8Qb})gVX1z*B?enyeJd>IFdaI-@>E*pTtc~PXGU^O~bCvY}9!9tJMGjtFF3i8PQAOp`246FbT^V}qI(TjXN2;CHkO5gY^o zDJh_&>%2$kBch)ZXnb<{lQZiL(iXqr%zvA2=bffdL{BOe`h9!g00|ir0751L9-}4~ zQP*R42-lt;fc*$gu!|0YMM4Sr>(V*?&z-VD!IS6S$OL?unU@cHlD+oE4?pZveCD;x z_nJn=KQ~6!o8EaCjAaun7|chwXyUyzLTK2oC&KPJ zm+?1VCJKt#ug^PG)-}CmeY)m>UsZgDXywy-NvvFt>U&(;Ifp89J+?Qcb;avq5-_Av zfVzME^JsKa_?4P5p-4>iBokmO&bVA?_e3PU@{tv&*$+~t=U$*(?bBv0kPEX_`A>%Zrxj)LDJ9ZBu=k^6!_TXIuSwq>?>{oD zuokH{CU+=ilHsz=^zS4GQ@Wh*UfTO1WXwvf;yVzeFC^L+aQN5ll)F2qFXB5tI(VH5 zv7H%SR1D#sxY##5$dWuAd<+bE<#VN*flQiR?KPAiV)^%M|32{B@P7%tz8*`2EgYVF zy3VIo0(|oKmreB#yzGDB$wQ$$ZeHdr_RHX{Y}T)^qU>8~(EPQ*-X71Z2UMA4J?-}o zUR|#bi-@&!^pEh8jZgO1oao4QNgO;DYQx>VZ5Qx{=L^iDAG0nc!rcr$obEkF zHbtAQSrrHe|*tT3y|iV`SjGKSuYRBdIQ9AtT@A=DTox zVJHT#bo&UCUx};i(o@!x<*XwT(vG%`(_`Frbppc?$Lb!s9Oj>q|GZ92d|Lq~fFtdE z&#qHU?919$3a5e~NeWrubV6>>*Q-wRNwMeC+GzwFyhq0h6^;Slh8d~1z17;1<&F`v#3Pd!BevD-J>8;guddF%0_QKy0?VPv z%5ee{2Xrd+n@AtkTGps63sv;)_;2gox?BbqL~NVn&zC%j<88Ai;6>EJ`Uj!Vcm@37 zE2D-aQSRegkR5We1Ze1#@0O+MXUl*)9zHMdIk@dps@7Y$XnR!l+<;V9;3WwB4@1sr AF#rGn diff --git a/examples/examples/test-data/output_vp9.ivf b/examples/examples/test-data/output_vp9.ivf deleted file mode 100644 index 03607b5263b281ef74e4c871d086deaee19d2cf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224430 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%0Dc*q5{6) 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 z%0D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�R+%smzJ^H!rB<6qg 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

b`6+~^w(T6~5==AIPUk3e=r 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 diff --git a/examples/src/lib.rs b/examples/src/lib.rs deleted file mode 100644 index 31e1bb209..000000000 --- a/examples/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/ice/.gitignore b/ice/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/ice/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/ice/CHANGELOG.md b/ice/CHANGELOG.md deleted file mode 100644 index d42dfb896..000000000 --- a/ice/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -# webrtc-ice changelog - -## Unreleased - -### Breaking changes - -* remove non used `MulticastDnsMode::Unspecified` variant [#404](https://github.com/webrtc-rs/webrtc/pull/404): - -## v0.9.0 - -* Increased minimum support rust version to `1.60.0`. - -### Breaking changes - -* Make functions non-async [#338](https://github.com/webrtc-rs/webrtc/pull/338): - - `Agent`: - - `get_bytes_received`; - - `get_bytes_sent`; - - `on_connection_state_change`; - - `on_selected_candidate_pair_change`; - - `on_candidate`; - - `add_remote_candidate`; - - `gather_candidates`. - - `unmarshal_candidate`; - - `CandidateHostConfig::new_candidate_host`; - - `CandidatePeerReflexiveConfig::new_candidate_peer_reflexive`; - - `CandidateRelayConfig::new_candidate_relay`; - - `CandidateServerReflexiveConfig::new_candidate_server_reflexive`; - - `Candidate`: - - `addr`; - - `set_ip`. - -## v0.8.2 - -* Add IP filter to ICE `AgentConfig` [#306](https://github.com/webrtc-rs/webrtc/pull/306) and [#318](https://github.com/webrtc-rs/webrtc/pull/318). -* Add `rust-version` at 1.57.0 to `Cargo.toml`. This was already the minimum version so does not constitute a change. - -## v0.8.1 - -This release was released in error and contains no changes from 0.8.0. - -## v0.8.0 - -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). -* Increased serde's minimum version to 1.0.102 [#243 Fixes for cargo minimal-versions](https://github.com/webrtc-rs/webrtc/pull/243) contributed by [algesten](https://github.com/algesten) - -## Prior to 0.8.0 - -Before 0.8.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/ice/releases). diff --git a/ice/Cargo.toml b/ice/Cargo.toml deleted file mode 100644 index 84916213b..000000000 --- a/ice/Cargo.toml +++ /dev/null @@ -1,57 +0,0 @@ -[package] -name = "webrtc-ice" -version = "0.11.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of ICE" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-ice" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/ice" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn", "vnet", "sync"] } -turn = { version = "0.8.0", path = "../turn" } -stun = { version = "0.6.0", path = "../stun" } -mdns = { version = "0.7.0", path = "../mdns", package = "webrtc-mdns" } - -arc-swap = "1" -async-trait = "0.1" -crc = "3" -log = "0.4" -rand = "0.8" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -thiserror = "1" -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -url = "2" -uuid = { version = "1", features = ["v4"] } -waitgroup = "0.1" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -regex = "1.9.5" -env_logger = "0.10" -chrono = "0.4.28" -ipnet = "2" -clap = "3" -lazy_static = "1" -hyper = { version = "0.14.27", features = ["full"] } -sha1 = "0.10" - -[[example]] -name = "ping_pong" -path = "examples/ping_pong.rs" -bench = false diff --git a/ice/LICENSE-APACHE b/ice/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/ice/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/ice/LICENSE-MIT b/ice/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/ice/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/ice/README.md b/ice/README.md deleted file mode 100644 index 73786a11b..000000000 --- a/ice/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of ICE. Rewrite Pion ICE in Rust -

diff --git a/ice/codecov.yml b/ice/codecov.yml deleted file mode 100644 index 99d83b7f7..000000000 --- a/ice/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 4bd5cec1-2807-4cd6-8430-d5f3efe32ce0 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/ice/doc/webrtc.rs.png b/ice/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/ice/examples/ping_pong.rs b/ice/examples/ping_pong.rs deleted file mode 100644 index 9620383b1..000000000 --- a/ice/examples/ping_pong.rs +++ /dev/null @@ -1,424 +0,0 @@ -use std::io; -use std::sync::Arc; -use std::time::Duration; - -use clap::{App, AppSettings, Arg}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Client, Method, Request, Response, Server, StatusCode}; -use ice::agent::agent_config::AgentConfig; -use ice::agent::Agent; -use ice::candidate::candidate_base::*; -use ice::candidate::*; -use ice::network_type::*; -use ice::state::*; -use ice::udp_network::UDPNetwork; -use ice::Error; -use rand::{thread_rng, Rng}; -use tokio::net::UdpSocket; -use tokio::sync::{mpsc, watch, Mutex}; -use util::Conn; -use webrtc_ice as ice; - -#[macro_use] -extern crate lazy_static; - -type SenderType = Arc>>; -type ReceiverType = Arc>>; - -lazy_static! { - // ErrUnknownType indicates an error with Unknown info. - static ref REMOTE_AUTH_CHANNEL: (SenderType, ReceiverType ) = { - let (tx, rx) = mpsc::channel::(3); - (Arc::new(Mutex::new(tx)), Arc::new(Mutex::new(rx))) - }; - - static ref REMOTE_CAND_CHANNEL: (SenderType, ReceiverType) = { - let (tx, rx) = mpsc::channel::(10); - (Arc::new(Mutex::new(tx)), Arc::new(Mutex::new(rx))) - }; -} - -// HTTP Listener to get ICE Credentials/Candidate from remote Peer -async fn remote_handler(req: Request) -> Result, hyper::Error> { - //println!("received {:?}", req); - match (req.method(), req.uri().path()) { - (&Method::POST, "/remoteAuth") => { - let full_body = - match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let tx = REMOTE_AUTH_CHANNEL.0.lock().await; - //println!("body: {:?}", full_body); - let _ = tx.send(full_body).await; - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - - (&Method::POST, "/remoteCandidate") => { - let full_body = - match std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) { - Ok(s) => s.to_owned(), - Err(err) => panic!("{}", err), - }; - let tx = REMOTE_CAND_CHANNEL.0.lock().await; - //println!("body: {:?}", full_body); - let _ = tx.send(full_body).await; - - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::OK; - Ok(response) - } - - // Return the 404 Not Found for other routes. - _ => { - let mut not_found = Response::default(); - *not_found.status_mut() = StatusCode::NOT_FOUND; - Ok(not_found) - } - } -} - -// Controlled Agent: -// cargo run --color=always --package webrtc-ice --example ping_pong -// Controlling Agent: -// cargo run --color=always --package webrtc-ice --example ping_pong -- --controlling - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::init(); - // .format(|buf, record| { - // writeln!( - // buf, - // "{}:{} [{}] {} - {}", - // record.file().unwrap_or("unknown"), - // record.line().unwrap_or(0), - // record.level(), - // chrono::Local::now().format("%H:%M:%S.%6f"), - // record.args() - // ) - // }) - // .filter(None, log::LevelFilter::Trace) - // .init(); - - let mut app = App::new("ICE Demo") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of ICE") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("use-mux") - .takes_value(false) - .long("use-mux") - .short('m') - .help("Use a muxed UDP connection over a single listening port"), - ) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("controlling") - .takes_value(false) - .long("controlling") - .help("is ICE Agent controlling"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let is_controlling = matches.is_present("controlling"); - let use_mux = matches.is_present("use-mux"); - - let (local_http_port, remote_http_port) = if is_controlling { - (9000, 9001) - } else { - (9001, 9000) - }; - - let (weak_conn, weak_agent) = { - let (done_tx, done_rx) = watch::channel(()); - - println!("Listening on http://localhost:{local_http_port}"); - let mut done_http_server = done_rx.clone(); - tokio::spawn(async move { - let addr = ([0, 0, 0, 0], local_http_port).into(); - let service = - make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(remote_handler)) }); - let server = Server::bind(&addr).serve(service); - tokio::select! { - _ = done_http_server.changed() => { - println!("receive cancel http server!"); - } - result = server => { - // Run this server for... forever! - if let Err(e) = result { - eprintln!("server error: {e}"); - } - println!("exit http server!"); - } - }; - }); - - if is_controlling { - println!("Local Agent is controlling"); - } else { - println!("Local Agent is controlled"); - }; - println!("Press 'Enter' when both processes have started"); - let mut input = String::new(); - let _ = io::stdin().read_line(&mut input)?; - - let udp_network = if use_mux { - use ice::udp_mux::*; - let port = if is_controlling { 4000 } else { 4001 }; - - let udp_socket = UdpSocket::bind(("0.0.0.0", port)).await?; - let udp_mux = UDPMuxDefault::new(UDPMuxParams::new(udp_socket)); - - UDPNetwork::Muxed(udp_mux) - } else { - UDPNetwork::Ephemeral(Default::default()) - }; - - let ice_agent = Arc::new( - Agent::new(AgentConfig { - network_types: vec![NetworkType::Udp4], - udp_network, - ..Default::default() - }) - .await?, - ); - - let client = Arc::new(Client::new()); - - // When we have gathered a new ICE Candidate send it to the remote peer - let client2 = Arc::clone(&client); - ice_agent.on_candidate(Box::new( - move |c: Option>| { - let client3 = Arc::clone(&client2); - Box::pin(async move { - if let Some(c) = c { - println!("posting remoteCandidate with {}", c.marshal()); - - let req = match Request::builder() - .method(Method::POST) - .uri(format!( - "http://localhost:{remote_http_port}/remoteCandidate" - )) - .body(Body::from(c.marshal())) - { - Ok(req) => req, - Err(err) => { - println!("{err}"); - return; - } - }; - let resp = match client3.request(req).await { - Ok(resp) => resp, - Err(err) => { - println!("{err}"); - return; - } - }; - println!("Response from remoteCandidate: {}", resp.status()); - } - }) - }, - )); - - let (ice_done_tx, mut ice_done_rx) = mpsc::channel::<()>(1); - // When ICE Connection state has change print to stdout - ice_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - println!("ICE Connection State has changed: {c}"); - if c == ConnectionState::Failed { - let _ = ice_done_tx.try_send(()); - } - Box::pin(async move {}) - })); - - // Get the local auth details and send to remote peer - let (local_ufrag, local_pwd) = ice_agent.get_local_user_credentials().await; - - println!("posting remoteAuth with {local_ufrag}:{local_pwd}"); - let req = match Request::builder() - .method(Method::POST) - .uri(format!("http://localhost:{remote_http_port}/remoteAuth")) - .body(Body::from(format!("{local_ufrag}:{local_pwd}"))) - { - Ok(req) => req, - Err(err) => return Err(Error::Other(format!("{err}"))), - }; - let resp = match client.request(req).await { - Ok(resp) => resp, - Err(err) => return Err(Error::Other(format!("{err}"))), - }; - println!("Response from remoteAuth: {}", resp.status()); - - let (remote_ufrag, remote_pwd) = { - let mut rx = REMOTE_AUTH_CHANNEL.1.lock().await; - if let Some(s) = rx.recv().await { - println!("received: {s}"); - let fields: Vec = s.split(':').map(|s| s.to_string()).collect(); - (fields[0].clone(), fields[1].clone()) - } else { - panic!("rx.recv() empty"); - } - }; - println!("remote_ufrag: {remote_ufrag}, remote_pwd: {remote_pwd}"); - - let ice_agent2 = Arc::clone(&ice_agent); - let mut done_cand = done_rx.clone(); - tokio::spawn(async move { - let mut rx = REMOTE_CAND_CHANNEL.1.lock().await; - loop { - tokio::select! { - _ = done_cand.changed() => { - println!("receive cancel remote cand!"); - break; - } - result = rx.recv() => { - if let Some(s) = result { - if let Ok(c) = unmarshal_candidate(&s) { - println!("add_remote_candidate: {c}"); - let c: Arc = Arc::new(c); - let _ = ice_agent2.add_remote_candidate(&c); - }else{ - println!("unmarshal_candidate error!"); - break; - } - }else{ - println!("REMOTE_CAND_CHANNEL done!"); - break; - } - } - }; - } - }); - - ice_agent.gather_candidates()?; - println!("Connecting..."); - - let (_cancel_tx, cancel_rx) = mpsc::channel(1); - // Start the ICE Agent. One side must be controlled, and the other must be controlling - let conn: Arc = if is_controlling { - ice_agent.dial(cancel_rx, remote_ufrag, remote_pwd).await? - } else { - ice_agent - .accept(cancel_rx, remote_ufrag, remote_pwd) - .await? - }; - - let weak_conn = Arc::downgrade(&conn); - - // Send messages in a loop to the remote peer - let conn_tx = Arc::clone(&conn); - let mut done_send = done_rx.clone(); - tokio::spawn(async move { - const RANDOM_STRING: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - loop { - tokio::time::sleep(Duration::from_secs(3)).await; - - let val: String = (0..15) - .map(|_| { - let idx = thread_rng().gen_range(0..RANDOM_STRING.len()); - RANDOM_STRING[idx] as char - }) - .collect(); - - tokio::select! { - _ = done_send.changed() => { - println!("receive cancel ice send!"); - break; - } - result = conn_tx.send(val.as_bytes()) => { - if let Err(err) = result { - eprintln!("conn_tx send error: {err}"); - break; - }else{ - println!("Sent: '{val}'"); - } - } - }; - } - }); - - let mut done_recv = done_rx.clone(); - tokio::spawn(async move { - // Receive messages in a loop from the remote peer - let mut buf = vec![0u8; 1500]; - loop { - tokio::select! { - _ = done_recv.changed() => { - println!("receive cancel ice recv!"); - break; - } - result = conn.recv(&mut buf) => { - match result { - Ok(n) => { - println!("Received: '{}'", std::str::from_utf8(&buf[..n]).unwrap()); - } - Err(err) => { - eprintln!("conn_tx send error: {err}"); - break; - } - }; - } - }; - } - }); - - println!("Press ctrl-c to stop"); - /*let d = if is_controlling { - Duration::from_secs(500) - } else { - Duration::from_secs(5) - }; - let timeout = tokio::time::sleep(d); - tokio::pin!(timeout);*/ - - tokio::select! { - /*_ = timeout.as_mut() => { - println!("received timeout signal!"); - let _ = done_tx.send(()); - }*/ - _ = ice_done_rx.recv() => { - println!("ice_done_rx"); - let _ = done_tx.send(()); - } - _ = tokio::signal::ctrl_c() => { - println!(); - let _ = done_tx.send(()); - } - }; - - let _ = ice_agent.close().await; - - (weak_conn, Arc::downgrade(&ice_agent)) - }; - - let mut int = tokio::time::interval(Duration::from_secs(1)); - loop { - int.tick().await; - println!( - "weak_conn: weak count = {}, strong count = {}, weak_agent: weak count = {}, strong count = {}", - weak_conn.weak_count(), - weak_conn.strong_count(), - weak_agent.weak_count(), - weak_agent.strong_count(), - ); - if weak_conn.strong_count() == 0 && weak_agent.strong_count() == 0 { - break; - } - } - - Ok(()) -} diff --git a/ice/src/agent/agent_config.rs b/ice/src/agent/agent_config.rs deleted file mode 100644 index 9c1c6cd4d..000000000 --- a/ice/src/agent/agent_config.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::net::IpAddr; -use std::time::Duration; - -use util::vnet::net::*; - -use super::*; -use crate::error::*; -use crate::mdns::*; -use crate::network_type::*; -use crate::udp_network::UDPNetwork; -use crate::url::*; - -/// The interval at which the agent performs candidate checks in the connecting phase. -pub(crate) const DEFAULT_CHECK_INTERVAL: Duration = Duration::from_millis(200); - -/// The interval used to keep candidates alive. -pub(crate) const DEFAULT_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(2); - -/// The default time till an Agent transitions disconnected. -pub(crate) const DEFAULT_DISCONNECTED_TIMEOUT: Duration = Duration::from_secs(5); - -/// The default time till an Agent transitions to failed after disconnected. -pub(crate) const DEFAULT_FAILED_TIMEOUT: Duration = Duration::from_secs(25); - -/// Wait time before nominating a host candidate. -pub(crate) const DEFAULT_HOST_ACCEPTANCE_MIN_WAIT: Duration = Duration::from_secs(0); - -/// Wait time before nominating a srflx candidate. -pub(crate) const DEFAULT_SRFLX_ACCEPTANCE_MIN_WAIT: Duration = Duration::from_millis(500); - -/// Wait time before nominating a prflx candidate. -pub(crate) const DEFAULT_PRFLX_ACCEPTANCE_MIN_WAIT: Duration = Duration::from_millis(1000); - -/// Wait time before nominating a relay candidate. -pub(crate) const DEFAULT_RELAY_ACCEPTANCE_MIN_WAIT: Duration = Duration::from_millis(2000); - -/// Max binding request before considering a pair failed. -pub(crate) const DEFAULT_MAX_BINDING_REQUESTS: u16 = 7; - -/// The number of bytes that can be buffered before we start to error. -pub(crate) const MAX_BUFFER_SIZE: usize = 1000 * 1000; // 1MB - -/// Wait time before binding requests can be deleted. -pub(crate) const MAX_BINDING_REQUEST_TIMEOUT: Duration = Duration::from_millis(4000); - -pub(crate) fn default_candidate_types() -> Vec { - vec![ - CandidateType::Host, - CandidateType::ServerReflexive, - CandidateType::Relay, - ] -} - -pub type InterfaceFilterFn = Box bool) + Send + Sync>; -pub type IpFilterFn = Box bool) + Send + Sync>; - -/// Collects the arguments to `ice::Agent` construction into a single structure, for -/// future-proofness of the interface. -#[derive(Default)] -pub struct AgentConfig { - pub urls: Vec, - - /// Controls how the UDP network stack works. - /// See [`UDPNetwork`] - pub udp_network: UDPNetwork, - - /// It is used to perform connectivity checks. The values MUST be unguessable, with at least - /// 128 bits of random number generator output used to generate the password, and at least 24 - /// bits of output to generate the username fragment. - pub local_ufrag: String, - /// It is used to perform connectivity checks. The values MUST be unguessable, with at least - /// 128 bits of random number generator output used to generate the password, and at least 24 - /// bits of output to generate the username fragment. - pub local_pwd: String, - - /// Controls mDNS behavior for the ICE agent. - pub multicast_dns_mode: MulticastDnsMode, - - /// Controls the hostname for this agent. If none is specified a random one will be generated. - pub multicast_dns_host_name: String, - - /// Control mDNS destination address - pub multicast_dns_dest_addr: String, - - /// Defaults to 5 seconds when this property is nil. - /// If the duration is 0, the ICE Agent will never go to disconnected. - pub disconnected_timeout: Option, - - /// Defaults to 25 seconds when this property is nil. - /// If the duration is 0, we will never go to failed. - pub failed_timeout: Option, - - /// Determines how often should we send ICE keepalives (should be less then connectiontimeout - /// above) when this is nil, it defaults to 10 seconds. - /// A keepalive interval of 0 means we never send keepalive packets - pub keepalive_interval: Option, - - /// An optional configuration for disabling or enabling support for specific network types. - pub network_types: Vec, - - /// An optional configuration for disabling or enabling support for specific candidate types. - pub candidate_types: Vec, - - //LoggerFactory logging.LoggerFactory - /// Controls how often our internal task loop runs when in the connecting state. - /// Only useful for testing. - pub check_interval: Duration, - - /// The max amount of binding requests the agent will send over a candidate pair for validation - /// or nomination, if after max_binding_requests the candidate is yet to answer a binding - /// request or a nomination we set the pair as failed. - pub max_binding_requests: Option, - - pub is_controlling: bool, - - /// lite agents do not perform connectivity check and only provide host candidates. - pub lite: bool, - - /// It is used along with nat1to1ips to specify which candidate type the 1:1 NAT IP addresses - /// should be mapped to. If unspecified or CandidateTypeHost, nat1to1ips are used to replace - /// host candidate IPs. If CandidateTypeServerReflexive, it will insert a srflx candidate (as - /// if it was derived from a STUN server) with its port number being the one for the actual host - /// candidate. Other values will result in an error. - pub nat_1to1_ip_candidate_type: CandidateType, - - /// Contains a list of public IP addresses that are to be used as a host candidate or srflx - /// candidate. This is used typically for servers that are behind 1:1 D-NAT (e.g. AWS EC2 - /// instances) and to eliminate the need of server reflexisive candidate gathering. - pub nat_1to1_ips: Vec, - - /// Specify a minimum wait time before selecting host candidates. - pub host_acceptance_min_wait: Option, - /// Specify a minimum wait time before selecting srflx candidates. - pub srflx_acceptance_min_wait: Option, - /// Specify a minimum wait time before selecting prflx candidates. - pub prflx_acceptance_min_wait: Option, - /// Specify a minimum wait time before selecting relay candidates. - pub relay_acceptance_min_wait: Option, - - /// Net is the our abstracted network interface for internal development purpose only - /// (see (github.com/pion/transport/vnet)[github.com/pion/transport/vnet]). - pub net: Option>, - - /// A function that you can use in order to whitelist or blacklist the interfaces which are - /// used to gather ICE candidates. - pub interface_filter: Arc>, - - /// A function that you can use in order to whitelist or blacklist - /// the ips which are used to gather ICE candidates. - pub ip_filter: Arc>, - - /// Controls if self-signed certificates are accepted when connecting to TURN servers via TLS or - /// DTLS. - pub insecure_skip_verify: bool, -} - -impl AgentConfig { - /// Populates an agent and falls back to defaults if fields are unset. - pub(crate) fn init_with_defaults(&self, a: &mut AgentInternal) { - if let Some(max_binding_requests) = self.max_binding_requests { - a.max_binding_requests = max_binding_requests; - } else { - a.max_binding_requests = DEFAULT_MAX_BINDING_REQUESTS; - } - - if let Some(host_acceptance_min_wait) = self.host_acceptance_min_wait { - a.host_acceptance_min_wait = host_acceptance_min_wait; - } else { - a.host_acceptance_min_wait = DEFAULT_HOST_ACCEPTANCE_MIN_WAIT; - } - - if let Some(srflx_acceptance_min_wait) = self.srflx_acceptance_min_wait { - a.srflx_acceptance_min_wait = srflx_acceptance_min_wait; - } else { - a.srflx_acceptance_min_wait = DEFAULT_SRFLX_ACCEPTANCE_MIN_WAIT; - } - - if let Some(prflx_acceptance_min_wait) = self.prflx_acceptance_min_wait { - a.prflx_acceptance_min_wait = prflx_acceptance_min_wait; - } else { - a.prflx_acceptance_min_wait = DEFAULT_PRFLX_ACCEPTANCE_MIN_WAIT; - } - - if let Some(relay_acceptance_min_wait) = self.relay_acceptance_min_wait { - a.relay_acceptance_min_wait = relay_acceptance_min_wait; - } else { - a.relay_acceptance_min_wait = DEFAULT_RELAY_ACCEPTANCE_MIN_WAIT; - } - - if let Some(disconnected_timeout) = self.disconnected_timeout { - a.disconnected_timeout = disconnected_timeout; - } else { - a.disconnected_timeout = DEFAULT_DISCONNECTED_TIMEOUT; - } - - if let Some(failed_timeout) = self.failed_timeout { - a.failed_timeout = failed_timeout; - } else { - a.failed_timeout = DEFAULT_FAILED_TIMEOUT; - } - - if let Some(keepalive_interval) = self.keepalive_interval { - a.keepalive_interval = keepalive_interval; - } else { - a.keepalive_interval = DEFAULT_KEEPALIVE_INTERVAL; - } - - if self.check_interval == Duration::from_secs(0) { - a.check_interval = DEFAULT_CHECK_INTERVAL; - } else { - a.check_interval = self.check_interval; - } - } - - pub(crate) fn init_ext_ip_mapping( - &self, - mdns_mode: MulticastDnsMode, - candidate_types: &[CandidateType], - ) -> Result> { - if let Some(ext_ip_mapper) = - ExternalIpMapper::new(self.nat_1to1_ip_candidate_type, &self.nat_1to1_ips)? - { - if ext_ip_mapper.candidate_type == CandidateType::Host { - if mdns_mode == MulticastDnsMode::QueryAndGather { - return Err(Error::ErrMulticastDnsWithNat1to1IpMapping); - } - let mut candi_host_enabled = false; - for candi_type in candidate_types { - if *candi_type == CandidateType::Host { - candi_host_enabled = true; - break; - } - } - if !candi_host_enabled { - return Err(Error::ErrIneffectiveNat1to1IpMappingHost); - } - } else if ext_ip_mapper.candidate_type == CandidateType::ServerReflexive { - let mut candi_srflx_enabled = false; - for candi_type in candidate_types { - if *candi_type == CandidateType::ServerReflexive { - candi_srflx_enabled = true; - break; - } - } - if !candi_srflx_enabled { - return Err(Error::ErrIneffectiveNat1to1IpMappingSrflx); - } - } - - Ok(Some(ext_ip_mapper)) - } else { - Ok(None) - } - } -} diff --git a/ice/src/agent/agent_gather.rs b/ice/src/agent/agent_gather.rs deleted file mode 100644 index 311d1cf14..000000000 --- a/ice/src/agent/agent_gather.rs +++ /dev/null @@ -1,892 +0,0 @@ -use std::net::{Ipv4Addr, Ipv6Addr}; -use std::str::FromStr; -use std::sync::Arc; - -use util::vnet::net::*; -use util::Conn; -use waitgroup::WaitGroup; - -use super::*; -use crate::candidate::candidate_base::CandidateBaseConfig; -use crate::candidate::candidate_host::CandidateHostConfig; -use crate::candidate::candidate_relay::CandidateRelayConfig; -use crate::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; -use crate::candidate::*; -use crate::error::*; -use crate::network_type::*; -use crate::udp_network::UDPNetwork; -use crate::url::{ProtoType, SchemeType, Url}; -use crate::util::*; - -const STUN_GATHER_TIMEOUT: Duration = Duration::from_secs(5); - -pub(crate) struct GatherCandidatesInternalParams { - pub(crate) udp_network: UDPNetwork, - pub(crate) candidate_types: Vec, - pub(crate) urls: Vec, - pub(crate) network_types: Vec, - pub(crate) mdns_mode: MulticastDnsMode, - pub(crate) mdns_name: String, - pub(crate) net: Arc, - pub(crate) interface_filter: Arc>, - pub(crate) ip_filter: Arc>, - pub(crate) ext_ip_mapper: Arc>, - pub(crate) agent_internal: Arc, - pub(crate) gathering_state: Arc, - pub(crate) chan_candidate_tx: ChanCandidateTx, -} - -struct GatherCandidatesLocalParams { - udp_network: UDPNetwork, - network_types: Vec, - mdns_mode: MulticastDnsMode, - mdns_name: String, - interface_filter: Arc>, - ip_filter: Arc>, - ext_ip_mapper: Arc>, - net: Arc, - agent_internal: Arc, -} - -struct GatherCandidatesLocalUDPMuxParams { - network_types: Vec, - interface_filter: Arc>, - ip_filter: Arc>, - ext_ip_mapper: Arc>, - net: Arc, - agent_internal: Arc, - udp_mux: Arc, -} - -struct GatherCandidatesSrflxMappedParasm { - network_types: Vec, - port_max: u16, - port_min: u16, - ext_ip_mapper: Arc>, - net: Arc, - agent_internal: Arc, -} - -struct GatherCandidatesSrflxParams { - urls: Vec, - network_types: Vec, - port_max: u16, - port_min: u16, - net: Arc, - agent_internal: Arc, -} - -impl Agent { - pub(crate) async fn gather_candidates_internal(params: GatherCandidatesInternalParams) { - Self::set_gathering_state( - ¶ms.chan_candidate_tx, - ¶ms.gathering_state, - GatheringState::Gathering, - ) - .await; - - let wg = WaitGroup::new(); - - for t in ¶ms.candidate_types { - match t { - CandidateType::Host => { - let local_params = GatherCandidatesLocalParams { - udp_network: params.udp_network.clone(), - network_types: params.network_types.clone(), - mdns_mode: params.mdns_mode, - mdns_name: params.mdns_name.clone(), - interface_filter: Arc::clone(¶ms.interface_filter), - ip_filter: Arc::clone(¶ms.ip_filter), - ext_ip_mapper: Arc::clone(¶ms.ext_ip_mapper), - net: Arc::clone(¶ms.net), - agent_internal: Arc::clone(¶ms.agent_internal), - }; - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - Self::gather_candidates_local(local_params).await; - }); - } - CandidateType::ServerReflexive => { - let ephemeral_config = match ¶ms.udp_network { - UDPNetwork::Ephemeral(e) => e, - // No server reflexive for muxxed connections - UDPNetwork::Muxed(_) => continue, - }; - - let srflx_params = GatherCandidatesSrflxParams { - urls: params.urls.clone(), - network_types: params.network_types.clone(), - port_max: ephemeral_config.port_max(), - port_min: ephemeral_config.port_min(), - net: Arc::clone(¶ms.net), - agent_internal: Arc::clone(¶ms.agent_internal), - }; - let w1 = wg.worker(); - tokio::spawn(async move { - let _d = w1; - - Self::gather_candidates_srflx(srflx_params).await; - }); - if let Some(ext_ip_mapper) = &*params.ext_ip_mapper { - if ext_ip_mapper.candidate_type == CandidateType::ServerReflexive { - let srflx_mapped_params = GatherCandidatesSrflxMappedParasm { - network_types: params.network_types.clone(), - port_max: ephemeral_config.port_max(), - port_min: ephemeral_config.port_min(), - ext_ip_mapper: Arc::clone(¶ms.ext_ip_mapper), - net: Arc::clone(¶ms.net), - agent_internal: Arc::clone(¶ms.agent_internal), - }; - let w2 = wg.worker(); - tokio::spawn(async move { - let _d = w2; - - Self::gather_candidates_srflx_mapped(srflx_mapped_params).await; - }); - } - } - } - CandidateType::Relay => { - let urls = params.urls.clone(); - let net = Arc::clone(¶ms.net); - let agent_internal = Arc::clone(¶ms.agent_internal); - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - Self::gather_candidates_relay(urls, net, agent_internal).await; - }); - } - _ => {} - } - } - - // Block until all STUN and TURN URLs have been gathered (or timed out) - wg.wait().await; - - Self::set_gathering_state( - ¶ms.chan_candidate_tx, - ¶ms.gathering_state, - GatheringState::Complete, - ) - .await; - } - - async fn set_gathering_state( - chan_candidate_tx: &ChanCandidateTx, - gathering_state: &Arc, - new_state: GatheringState, - ) { - if GatheringState::from(gathering_state.load(Ordering::SeqCst)) != new_state - && new_state == GatheringState::Complete - { - let cand_tx = chan_candidate_tx.lock().await; - if let Some(tx) = &*cand_tx { - let _ = tx.send(None).await; - } - } - - gathering_state.store(new_state as u8, Ordering::SeqCst); - } - - async fn gather_candidates_local(params: GatherCandidatesLocalParams) { - let GatherCandidatesLocalParams { - udp_network, - network_types, - mdns_mode, - mdns_name, - interface_filter, - ip_filter, - ext_ip_mapper, - net, - agent_internal, - } = params; - - // If we wanna use UDP mux, do so - // FIXME: We still need to support TCP in combination with this option - if let UDPNetwork::Muxed(udp_mux) = udp_network { - let result = Self::gather_candidates_local_udp_mux(GatherCandidatesLocalUDPMuxParams { - network_types, - interface_filter, - ip_filter, - ext_ip_mapper, - net, - agent_internal, - udp_mux, - }) - .await; - - if let Err(err) = result { - log::error!("Failed to gather local candidates using UDP mux: {}", err); - } - - return; - } - - let ips = local_interfaces(&net, &interface_filter, &ip_filter, &network_types).await; - for ip in ips { - let mut mapped_ip = ip; - - if mdns_mode != MulticastDnsMode::QueryAndGather && ext_ip_mapper.is_some() { - if let Some(ext_ip_mapper2) = ext_ip_mapper.as_ref() { - if ext_ip_mapper2.candidate_type == CandidateType::Host { - if let Ok(mi) = ext_ip_mapper2.find_external_ip(&ip.to_string()) { - mapped_ip = mi; - } else { - log::warn!( - "[{}]: 1:1 NAT mapping is enabled but no external IP is found for {}", - agent_internal.get_name(), - ip - ); - } - } - } - } - - let address = if mdns_mode == MulticastDnsMode::QueryAndGather { - mdns_name.clone() - } else { - mapped_ip.to_string() - }; - - //TODO: for network in networks - let network = UDP.to_owned(); - if let UDPNetwork::Ephemeral(ephemeral_config) = &udp_network { - /*TODO:switch network { - case tcp: - // Handle ICE TCP passive mode - - a.log.Debugf("GetConn by ufrag: %s\n", a.localUfrag) - conn, err = a.tcpMux.GetConnByUfrag(a.localUfrag) - if err != nil { - if !errors.Is(err, ErrTCPMuxNotInitialized) { - a.log.Warnf("error getting tcp conn by ufrag: %s %s %s\n", network, ip, a.localUfrag) - } - continue - } - port = conn.LocalAddr().(*net.TCPAddr).Port - tcpType = TCPTypePassive - // is there a way to verify that the listen address is even - // accessible from the current interface. - case udp:*/ - - let conn: Arc = match listen_udp_in_port_range( - &net, - ephemeral_config.port_max(), - ephemeral_config.port_min(), - SocketAddr::new(ip, 0), - ) - .await - { - Ok(conn) => conn, - Err(err) => { - log::warn!( - "[{}]: could not listen {} {}: {}", - agent_internal.get_name(), - network, - ip, - err - ); - continue; - } - }; - - let port = match conn.local_addr() { - Ok(addr) => addr.port(), - Err(err) => { - log::warn!( - "[{}]: could not get local addr: {}", - agent_internal.get_name(), - err - ); - continue; - } - }; - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: network.clone(), - address, - port, - component: COMPONENT_RTP, - conn: Some(conn), - ..CandidateBaseConfig::default() - }, - ..CandidateHostConfig::default() - }; - - let candidate: Arc = - match host_config.new_candidate_host() { - Ok(candidate) => { - if mdns_mode == MulticastDnsMode::QueryAndGather { - if let Err(err) = candidate.set_ip(&ip) { - log::warn!( - "[{}]: Failed to create host candidate: {} {} {}: {:?}", - agent_internal.get_name(), - network, - mapped_ip, - port, - err - ); - continue; - } - } - Arc::new(candidate) - } - Err(err) => { - log::warn!( - "[{}]: Failed to create host candidate: {} {} {}: {}", - agent_internal.get_name(), - network, - mapped_ip, - port, - err - ); - continue; - } - }; - - { - if let Err(err) = agent_internal.add_candidate(&candidate).await { - if let Err(close_err) = candidate.close().await { - log::warn!( - "[{}]: Failed to close candidate: {}", - agent_internal.get_name(), - close_err - ); - } - log::warn!( - "[{}]: Failed to append to localCandidates and run onCandidateHdlr: {}", - agent_internal.get_name(), - err - ); - } - } - } - } - } - - async fn gather_candidates_local_udp_mux( - params: GatherCandidatesLocalUDPMuxParams, - ) -> Result<()> { - let GatherCandidatesLocalUDPMuxParams { - network_types, - interface_filter, - ip_filter, - ext_ip_mapper, - net, - agent_internal, - udp_mux, - } = params; - - // Filter out non UDP network types - let relevant_network_types: Vec<_> = - network_types.into_iter().filter(|n| n.is_udp()).collect(); - - let udp_mux = Arc::clone(&udp_mux); - - let local_ips = - local_interfaces(&net, &interface_filter, &ip_filter, &relevant_network_types).await; - - let candidate_ips: Vec = ext_ip_mapper - .as_ref() // Arc - .as_ref() // Option - .and_then(|mapper| { - if mapper.candidate_type != CandidateType::Host { - return None; - } - - Some( - local_ips - .iter() - .filter_map(|ip| match mapper.find_external_ip(&ip.to_string()) { - Ok(ip) => Some(ip), - Err(err) => { - log::warn!( - "1:1 NAT mapping is enabled but not external IP is found for {}: {}", - ip, - err - ); - None - } - }) - .collect(), - ) - }) - .unwrap_or_else(|| local_ips.iter().copied().collect()); - - if candidate_ips.is_empty() { - return Err(Error::ErrCandidateIpNotFound); - } - - let ufrag = { - let ufrag_pwd = agent_internal.ufrag_pwd.lock().await; - - ufrag_pwd.local_ufrag.clone() - }; - - let conn = udp_mux.get_conn(&ufrag).await?; - let port = conn.local_addr()?.port(); - - for candidate_ip in candidate_ips { - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: UDP.to_owned(), - address: candidate_ip.to_string(), - port, - conn: Some(conn.clone()), - component: COMPONENT_RTP, - ..Default::default() - }, - tcp_type: TcpType::Unspecified, - }; - - let candidate: Arc = - Arc::new(host_config.new_candidate_host()?); - - agent_internal.add_candidate(&candidate).await?; - } - - Ok(()) - } - - async fn gather_candidates_srflx_mapped(params: GatherCandidatesSrflxMappedParasm) { - let GatherCandidatesSrflxMappedParasm { - network_types, - port_max, - port_min, - ext_ip_mapper, - net, - agent_internal, - } = params; - - let wg = WaitGroup::new(); - - for network_type in network_types { - if network_type.is_tcp() { - continue; - } - - let network = network_type.to_string(); - let net2 = Arc::clone(&net); - let agent_internal2 = Arc::clone(&agent_internal); - let ext_ip_mapper2 = Arc::clone(&ext_ip_mapper); - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - let conn: Arc = match listen_udp_in_port_range( - &net2, - port_max, - port_min, - if network_type.is_ipv4() { - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0) - } else { - SocketAddr::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 0) - }, - ) - .await - { - Ok(conn) => conn, - Err(err) => { - log::warn!( - "[{}]: Failed to listen {}: {}", - agent_internal2.get_name(), - network, - err - ); - return Ok(()); - } - }; - - let laddr = conn.local_addr()?; - let mapped_ip = { - if let Some(ext_ip_mapper3) = &*ext_ip_mapper2 { - match ext_ip_mapper3.find_external_ip(&laddr.ip().to_string()) { - Ok(ip) => ip, - Err(err) => { - log::warn!( - "[{}]: 1:1 NAT mapping is enabled but no external IP is found for {}: {}", - agent_internal2.get_name(), - laddr, - err - ); - return Ok(()); - } - } - } else { - log::error!( - "[{}]: ext_ip_mapper is None in gather_candidates_srflx_mapped", - agent_internal2.get_name(), - ); - return Ok(()); - } - }; - - let srflx_config = CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: network.clone(), - address: mapped_ip.to_string(), - port: laddr.port(), - component: COMPONENT_RTP, - conn: Some(conn), - ..CandidateBaseConfig::default() - }, - rel_addr: laddr.ip().to_string(), - rel_port: laddr.port(), - }; - - let candidate: Arc = - match srflx_config.new_candidate_server_reflexive() { - Ok(candidate) => Arc::new(candidate), - Err(err) => { - log::warn!( - "[{}]: Failed to create server reflexive candidate: {} {} {}: {}", - agent_internal2.get_name(), - network, - mapped_ip, - laddr.port(), - err - ); - return Ok(()); - } - }; - - { - if let Err(err) = agent_internal2.add_candidate(&candidate).await { - if let Err(close_err) = candidate.close().await { - log::warn!( - "[{}]: Failed to close candidate: {}", - agent_internal2.get_name(), - close_err - ); - } - log::warn!( - "[{}]: Failed to append to localCandidates and run onCandidateHdlr: {}", - agent_internal2.get_name(), - err - ); - } - } - - Result::<()>::Ok(()) - }); - } - - wg.wait().await; - } - - async fn gather_candidates_srflx(params: GatherCandidatesSrflxParams) { - let GatherCandidatesSrflxParams { - urls, - network_types, - port_max, - port_min, - net, - agent_internal, - } = params; - - let wg = WaitGroup::new(); - for network_type in network_types { - if network_type.is_tcp() { - continue; - } - - for url in &urls { - let network = network_type.to_string(); - let is_ipv4 = network_type.is_ipv4(); - let url = url.clone(); - let net2 = Arc::clone(&net); - let agent_internal2 = Arc::clone(&agent_internal); - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - let host_port = format!("{}:{}", url.host, url.port); - let server_addr = match net2.resolve_addr(is_ipv4, &host_port).await { - Ok(addr) => addr, - Err(err) => { - log::warn!( - "[{}]: failed to resolve stun host: {}: {}", - agent_internal2.get_name(), - host_port, - err - ); - return Ok(()); - } - }; - - let conn: Arc = match listen_udp_in_port_range( - &net2, - port_max, - port_min, - if is_ipv4 { - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0) - } else { - SocketAddr::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 0) - }, - ) - .await - { - Ok(conn) => conn, - Err(err) => { - log::warn!( - "[{}]: Failed to listen for {}: {}", - agent_internal2.get_name(), - server_addr, - err - ); - return Ok(()); - } - }; - - let xoraddr = - match get_xormapped_addr(&conn, server_addr, STUN_GATHER_TIMEOUT).await { - Ok(xoraddr) => xoraddr, - Err(err) => { - log::warn!( - "[{}]: could not get server reflexive address {} {}: {}", - agent_internal2.get_name(), - network, - url, - err - ); - return Ok(()); - } - }; - - let (ip, port) = (xoraddr.ip, xoraddr.port); - - let laddr = conn.local_addr()?; - let srflx_config = CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: network.clone(), - address: ip.to_string(), - port, - component: COMPONENT_RTP, - conn: Some(conn), - ..CandidateBaseConfig::default() - }, - rel_addr: laddr.ip().to_string(), - rel_port: laddr.port(), - }; - - let candidate: Arc = - match srflx_config.new_candidate_server_reflexive() { - Ok(candidate) => Arc::new(candidate), - Err(err) => { - log::warn!( - "[{}]: Failed to create server reflexive candidate: {} {} {}: {:?}", - agent_internal2.get_name(), - network, - ip, - port, - err - ); - return Ok(()); - } - }; - - { - if let Err(err) = agent_internal2.add_candidate(&candidate).await { - if let Err(close_err) = candidate.close().await { - log::warn!( - "[{}]: Failed to close candidate: {}", - agent_internal2.get_name(), - close_err - ); - } - log::warn!( - "[{}]: Failed to append to localCandidates and run onCandidateHdlr: {}", - agent_internal2.get_name(), - err - ); - } - } - - Result::<()>::Ok(()) - }); - } - } - - wg.wait().await; - } - - pub(crate) async fn gather_candidates_relay( - urls: Vec, - net: Arc, - agent_internal: Arc, - ) { - let wg = WaitGroup::new(); - - for url in urls { - if url.scheme != SchemeType::Turn && url.scheme != SchemeType::Turns { - continue; - } - if url.username.is_empty() { - log::error!( - "[{}]:Failed to gather relay candidates: {:?}", - agent_internal.get_name(), - Error::ErrUsernameEmpty - ); - return; - } - if url.password.is_empty() { - log::error!( - "[{}]: Failed to gather relay candidates: {:?}", - agent_internal.get_name(), - Error::ErrPasswordEmpty - ); - return; - } - - let network = NetworkType::Udp4.to_string(); - let net2 = Arc::clone(&net); - let agent_internal2 = Arc::clone(&agent_internal); - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - let turn_server_addr = format!("{}:{}", url.host, url.port); - - let (loc_conn, rel_addr, rel_port) = - if url.proto == ProtoType::Udp && url.scheme == SchemeType::Turn { - let loc_conn = match net2.bind(SocketAddr::from_str("0.0.0.0:0")?).await { - Ok(c) => c, - Err(err) => { - log::warn!( - "[{}]: Failed to listen due to error: {}", - agent_internal2.get_name(), - err - ); - return Ok(()); - } - }; - - let local_addr = loc_conn.local_addr()?; - let rel_addr = local_addr.ip().to_string(); - let rel_port = local_addr.port(); - (loc_conn, rel_addr, rel_port) - /*TODO: case url.proto == ProtoType::UDP && url.scheme == SchemeType::TURNS{ - case a.proxyDialer != nil && url.Proto == ProtoTypeTCP && (url.Scheme == SchemeTypeTURN || url.Scheme == SchemeTypeTURNS): - case url.Proto == ProtoTypeTCP && url.Scheme == SchemeTypeTURN: - case url.Proto == ProtoTypeTCP && url.Scheme == SchemeTypeTURNS:*/ - } else { - log::warn!( - "[{}]: Unable to handle URL in gather_candidates_relay {}", - agent_internal2.get_name(), - url - ); - return Ok(()); - }; - - let cfg = turn::client::ClientConfig { - stun_serv_addr: String::new(), - turn_serv_addr: turn_server_addr.clone(), - username: url.username, - password: url.password, - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn: loc_conn, - vnet: Some(Arc::clone(&net2)), - }; - let client = match turn::client::Client::new(cfg).await { - Ok(client) => Arc::new(client), - Err(err) => { - log::warn!( - "[{}]: Failed to build new turn.Client {} {}\n", - agent_internal2.get_name(), - turn_server_addr, - err - ); - return Ok(()); - } - }; - if let Err(err) = client.listen().await { - let _ = client.close().await; - log::warn!( - "[{}]: Failed to listen on turn.Client {} {}", - agent_internal2.get_name(), - turn_server_addr, - err - ); - return Ok(()); - } - - let relay_conn: Arc = match client.allocate().await { - Ok(conn) => Arc::new(conn), - Err(err) => { - let _ = client.close().await; - log::warn!( - "[{}]: Failed to allocate on turn.Client {} {}", - agent_internal2.get_name(), - turn_server_addr, - err - ); - return Ok(()); - } - }; - - let raddr = relay_conn.local_addr()?; - let relay_config = CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: network.clone(), - address: raddr.ip().to_string(), - port: raddr.port(), - component: COMPONENT_RTP, - conn: Some(Arc::clone(&relay_conn)), - ..CandidateBaseConfig::default() - }, - rel_addr, - rel_port, - relay_client: Some(Arc::clone(&client)), - }; - - let candidate: Arc = - match relay_config.new_candidate_relay() { - Ok(candidate) => Arc::new(candidate), - Err(err) => { - let _ = relay_conn.close().await; - let _ = client.close().await; - log::warn!( - "[{}]: Failed to create relay candidate: {} {}: {}", - agent_internal2.get_name(), - network, - raddr, - err - ); - return Ok(()); - } - }; - - { - if let Err(err) = agent_internal2.add_candidate(&candidate).await { - if let Err(close_err) = candidate.close().await { - log::warn!( - "[{}]: Failed to close candidate: {}", - agent_internal2.get_name(), - close_err - ); - } - log::warn!( - "[{}]: Failed to append to localCandidates and run onCandidateHdlr: {}", - agent_internal2.get_name(), - err - ); - } - } - - Result::<()>::Ok(()) - }); - } - - wg.wait().await; - } -} diff --git a/ice/src/agent/agent_gather_test.rs b/ice/src/agent/agent_gather_test.rs deleted file mode 100644 index ed90f097a..000000000 --- a/ice/src/agent/agent_gather_test.rs +++ /dev/null @@ -1,490 +0,0 @@ -use std::str::FromStr; - -use ipnet::IpNet; -use tokio::net::UdpSocket; -use util::vnet::*; - -use super::agent_vnet_test::*; -use super::*; -use crate::udp_mux::{UDPMuxDefault, UDPMuxParams}; -use crate::util::*; - -#[tokio::test] -async fn test_vnet_gather_no_local_ip_address() -> Result<()> { - let vnet = Arc::new(net::Net::new(Some(net::NetConfig::default()))); - - let a = Agent::new(AgentConfig { - net: Some(Arc::clone(&vnet)), - ..Default::default() - }) - .await?; - - let local_ips = local_interfaces( - &vnet, - &a.interface_filter, - &a.ip_filter, - &[NetworkType::Udp4], - ) - .await; - assert!(local_ips.is_empty(), "should return no local IP"); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_dynamic_ip_address() -> Result<()> { - let cider = "1.2.3.0/24"; - let ipnet = IpNet::from_str(cider).map_err(|e| Error::Other(e.to_string()))?; - - let r = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: cider.to_owned(), - ..Default::default() - })?)); - let nw = Arc::new(net::Net::new(Some(net::NetConfig::default()))); - connect_net2router(&nw, &r).await?; - - let a = Agent::new(AgentConfig { - net: Some(Arc::clone(&nw)), - ..Default::default() - }) - .await?; - - let local_ips = - local_interfaces(&nw, &a.interface_filter, &a.ip_filter, &[NetworkType::Udp4]).await; - assert!(!local_ips.is_empty(), "should have one local IP"); - - for ip in &local_ips { - if ip.is_loopback() { - panic!("should not return loopback IP"); - } - if !ipnet.contains(ip) { - panic!("{ip} should be contained in the CIDR {ipnet}"); - } - } - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_listen_udp() -> Result<()> { - let cider = "1.2.3.0/24"; - let r = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: cider.to_owned(), - ..Default::default() - })?)); - let nw = Arc::new(net::Net::new(Some(net::NetConfig::default()))); - connect_net2router(&nw, &r).await?; - - let a = Agent::new(AgentConfig { - net: Some(Arc::clone(&nw)), - ..Default::default() - }) - .await?; - - let local_ips = - local_interfaces(&nw, &a.interface_filter, &a.ip_filter, &[NetworkType::Udp4]).await; - assert!(!local_ips.is_empty(), "should have one local IP"); - - for ip in local_ips { - let _ = listen_udp_in_port_range(&nw, 0, 0, SocketAddr::new(ip, 0)).await?; - - let result = listen_udp_in_port_range(&nw, 4999, 5000, SocketAddr::new(ip, 0)).await; - assert!( - result.is_err(), - "listenUDP with invalid port range did not return ErrPort" - ); - - let conn = listen_udp_in_port_range(&nw, 5000, 5000, SocketAddr::new(ip, 0)).await?; - let port = conn.local_addr()?.port(); - assert_eq!( - port, 5000, - "listenUDP with port restriction of 5000 listened on incorrect port ({port})" - ); - } - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_with_nat_1to1_as_host_candidates() -> Result<()> { - let external_ip0 = "1.2.3.4"; - let external_ip1 = "1.2.3.5"; - let local_ip0 = "10.0.0.1"; - let local_ip1 = "10.0.0.2"; - let map0 = format!("{external_ip0}/{local_ip0}"); - let map1 = format!("{external_ip1}/{local_ip1}"); - - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - - let lan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "10.0.0.0/24".to_owned(), - static_ips: vec![map0.clone(), map1.clone()], - nat_type: Some(nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }), - ..Default::default() - })?)); - - connect_router2router(&lan, &wan).await?; - - let nw = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec![local_ip0.to_owned(), local_ip1.to_owned()], - ..Default::default() - }))); - - connect_net2router(&nw, &lan).await?; - - let a = Agent::new(AgentConfig { - network_types: vec![NetworkType::Udp4], - nat_1to1_ips: vec![map0.clone(), map1.clone()], - net: Some(Arc::clone(&nw)), - ..Default::default() - }) - .await?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - a.on_candidate(Box::new( - move |c: Option>| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if c.is_none() { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }, - )); - - a.gather_candidates()?; - - log::debug!("wait for gathering is done..."); - let _ = done_rx.recv().await; - log::debug!("gathering is done"); - - let candidates = a.get_local_candidates().await?; - assert_eq!(candidates.len(), 2, "There must be two candidates"); - - let mut laddrs = vec![]; - for candi in &candidates { - if let Some(conn) = candi.get_conn() { - let laddr = conn.local_addr()?; - assert_eq!( - candi.port(), - laddr.port(), - "Unexpected candidate port: {}", - candi.port() - ); - laddrs.push(laddr); - } - } - - if candidates[0].address() == external_ip0 { - assert_eq!( - candidates[1].address(), - external_ip1, - "Unexpected candidate IP: {}", - candidates[1].address() - ); - assert_eq!( - laddrs[0].ip().to_string(), - local_ip0, - "Unexpected listen IP: {}", - laddrs[0].ip() - ); - assert_eq!( - laddrs[1].ip().to_string(), - local_ip1, - "Unexpected listen IP: {}", - laddrs[1].ip() - ); - } else if candidates[0].address() == external_ip1 { - assert_eq!( - candidates[1].address(), - external_ip0, - "Unexpected candidate IP: {}", - candidates[1].address() - ); - assert_eq!( - laddrs[0].ip().to_string(), - local_ip1, - "Unexpected listen IP: {}", - laddrs[0].ip(), - ); - assert_eq!( - laddrs[1].ip().to_string(), - local_ip0, - "Unexpected listen IP: {}", - laddrs[1].ip(), - ) - } - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_with_nat_1to1_as_srflx_candidates() -> Result<()> { - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - - let lan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "10.0.0.0/24".to_owned(), - static_ips: vec!["1.2.3.4/10.0.0.1".to_owned()], - nat_type: Some(nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }), - ..Default::default() - })?)); - - connect_router2router(&lan, &wan).await?; - - let nw = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["10.0.0.1".to_owned()], - ..Default::default() - }))); - - connect_net2router(&nw, &lan).await?; - - let a = Agent::new(AgentConfig { - network_types: vec![NetworkType::Udp4], - nat_1to1_ips: vec!["1.2.3.4".to_owned()], - nat_1to1_ip_candidate_type: CandidateType::ServerReflexive, - net: Some(nw), - ..Default::default() - }) - .await?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - a.on_candidate(Box::new( - move |c: Option>| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if c.is_none() { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }, - )); - - a.gather_candidates()?; - - log::debug!("wait for gathering is done..."); - let _ = done_rx.recv().await; - log::debug!("gathering is done"); - - let candidates = a.get_local_candidates().await?; - assert_eq!(candidates.len(), 2, "There must be two candidates"); - - let mut candi_host = None; - let mut candi_srflx = None; - - for candidate in candidates { - match candidate.candidate_type() { - CandidateType::Host => { - candi_host = Some(candidate); - } - CandidateType::ServerReflexive => { - candi_srflx = Some(candidate); - } - _ => { - panic!("Unexpected candidate type"); - } - } - } - - assert!(candi_host.is_some(), "should not be nil"); - assert_eq!("10.0.0.1", candi_host.unwrap().address(), "should match"); - assert!(candi_srflx.is_some(), "should not be nil"); - assert_eq!("1.2.3.4", candi_srflx.unwrap().address(), "should match"); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_with_interface_filter() -> Result<()> { - let r = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - let nw = Arc::new(net::Net::new(Some(net::NetConfig::default()))); - connect_net2router(&nw, &r).await?; - - //"InterfaceFilter should exclude the interface" - { - let a = Agent::new(AgentConfig { - net: Some(Arc::clone(&nw)), - interface_filter: Arc::new(Some(Box::new(|_: &str| -> bool { - //assert_eq!("eth0", interface_name); - false - }))), - ..Default::default() - }) - .await?; - - let local_ips = - local_interfaces(&nw, &a.interface_filter, &a.ip_filter, &[NetworkType::Udp4]).await; - assert!( - local_ips.is_empty(), - "InterfaceFilter should have excluded everything" - ); - - a.close().await?; - } - - //"InterfaceFilter should not exclude the interface" - { - let a = Agent::new(AgentConfig { - net: Some(Arc::clone(&nw)), - interface_filter: Arc::new(Some(Box::new(|interface_name: &str| -> bool { - "eth0" == interface_name - }))), - ..Default::default() - }) - .await?; - - let local_ips = - local_interfaces(&nw, &a.interface_filter, &a.ip_filter, &[NetworkType::Udp4]).await; - assert_eq!( - local_ips.len(), - 1, - "InterfaceFilter should not have excluded everything" - ); - - a.close().await?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_turn_connection_leak() -> Result<()> { - let turn_server_url = Url { - scheme: SchemeType::Turn, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - username: "user".to_owned(), - password: "pass".to_owned(), - proto: ProtoType::Udp, - }; - - // buildVNet with a Symmetric NATs for both LANs - let nat_type = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - filtering_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - ..Default::default() - }; - - let v = build_vnet(nat_type, nat_type).await?; - - let cfg0 = AgentConfig { - urls: vec![turn_server_url.clone()], - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - nat_1to1_ips: vec![VNET_GLOBAL_IPA.to_owned()], - net: Some(Arc::clone(&v.net0)), - ..Default::default() - }; - - let a_agent = Agent::new(cfg0).await?; - - { - let agent_internal = Arc::clone(&a_agent.internal); - Agent::gather_candidates_relay( - vec![turn_server_url.clone()], - Arc::clone(&v.net0), - agent_internal, - ) - .await; - } - - // Assert relay conn leak on close. - a_agent.close().await?; - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_vnet_gather_muxed_udp() -> Result<()> { - let udp_socket = UdpSocket::bind("0.0.0.0:0").await?; - let udp_mux = UDPMuxDefault::new(UDPMuxParams::new(udp_socket)); - - let lan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "10.0.0.0/24".to_owned(), - nat_type: Some(nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }), - ..Default::default() - })?)); - - let nw = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["10.0.0.1".to_owned()], - ..Default::default() - }))); - - connect_net2router(&nw, &lan).await?; - - let a = Agent::new(AgentConfig { - network_types: vec![NetworkType::Udp4], - nat_1to1_ips: vec!["1.2.3.4".to_owned()], - net: Some(nw), - udp_network: UDPNetwork::Muxed(udp_mux), - ..Default::default() - }) - .await?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - a.on_candidate(Box::new( - move |c: Option>| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if c.is_none() { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }, - )); - - a.gather_candidates()?; - - log::debug!("wait for gathering is done..."); - let _ = done_rx.recv().await; - log::debug!("gathering is done"); - - let candidates = a.get_local_candidates().await?; - assert_eq!(candidates.len(), 1, "There must be a single candidate"); - - let candi = &candidates[0]; - let laddr = candi.get_conn().unwrap().local_addr()?; - assert_eq!(candi.address(), "1.2.3.4"); - assert_eq!( - candi.port(), - laddr.port(), - "Unexpected candidate port: {}", - candi.port() - ); - - Ok(()) -} diff --git a/ice/src/agent/agent_internal.rs b/ice/src/agent/agent_internal.rs deleted file mode 100644 index dc943b5b4..000000000 --- a/ice/src/agent/agent_internal.rs +++ /dev/null @@ -1,1198 +0,0 @@ -use portable_atomic::{AtomicBool, AtomicU64}; - -use arc_swap::ArcSwapOption; -use util::sync::Mutex as SyncMutex; - -use super::agent_transport::*; -use super::*; -use crate::candidate::candidate_base::CandidateBaseConfig; -use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; -use crate::util::*; - -pub type ChanCandidateTx = - Arc>>>>>; - -#[derive(Default)] -pub(crate) struct UfragPwd { - pub(crate) local_ufrag: String, - pub(crate) local_pwd: String, - pub(crate) remote_ufrag: String, - pub(crate) remote_pwd: String, -} - -pub struct AgentInternal { - // State owned by the taskLoop - pub(crate) on_connected_tx: Mutex>>, - pub(crate) on_connected_rx: Mutex>>, - - // State for closing - pub(crate) done_tx: Mutex>>, - // force candidate to be contacted immediately (instead of waiting for task ticker) - pub(crate) force_candidate_contact_tx: mpsc::Sender, - pub(crate) done_and_force_candidate_contact_rx: - Mutex, mpsc::Receiver)>>, - - pub(crate) chan_candidate_tx: ChanCandidateTx, - pub(crate) chan_candidate_pair_tx: Mutex>>, - pub(crate) chan_state_tx: Mutex>>, - - pub(crate) on_connection_state_change_hdlr: ArcSwapOption>, - pub(crate) on_selected_candidate_pair_change_hdlr: - ArcSwapOption>, - pub(crate) on_candidate_hdlr: ArcSwapOption>, - - pub(crate) tie_breaker: AtomicU64, - pub(crate) is_controlling: AtomicBool, - pub(crate) lite: AtomicBool, - - pub(crate) start_time: SyncMutex, - pub(crate) nominated_pair: Mutex>>, - - pub(crate) connection_state: AtomicU8, //ConnectionState, - - pub(crate) started_ch_tx: Mutex>>, - - pub(crate) ufrag_pwd: Mutex, - - pub(crate) local_candidates: Mutex>>>, - pub(crate) remote_candidates: - Mutex>>>, - - // LRU of outbound Binding request Transaction IDs - pub(crate) pending_binding_requests: Mutex>, - - pub(crate) agent_conn: Arc, - - // the following variables won't be changed after init_with_defaults() - pub(crate) insecure_skip_verify: bool, - pub(crate) max_binding_requests: u16, - pub(crate) host_acceptance_min_wait: Duration, - pub(crate) srflx_acceptance_min_wait: Duration, - pub(crate) prflx_acceptance_min_wait: Duration, - pub(crate) relay_acceptance_min_wait: Duration, - // How long connectivity checks can fail before the ICE Agent - // goes to disconnected - pub(crate) disconnected_timeout: Duration, - // How long connectivity checks can fail before the ICE Agent - // goes to failed - pub(crate) failed_timeout: Duration, - // How often should we send keepalive packets? - // 0 means never - pub(crate) keepalive_interval: Duration, - // How often should we run our internal taskLoop to check for state changes when connecting - pub(crate) check_interval: Duration, -} - -impl AgentInternal { - pub(super) fn new(config: &AgentConfig) -> (Self, ChanReceivers) { - let (chan_state_tx, chan_state_rx) = mpsc::channel(1); - let (chan_candidate_tx, chan_candidate_rx) = mpsc::channel(1); - let (chan_candidate_pair_tx, chan_candidate_pair_rx) = mpsc::channel(1); - let (on_connected_tx, on_connected_rx) = mpsc::channel(1); - let (done_tx, done_rx) = mpsc::channel(1); - let (force_candidate_contact_tx, force_candidate_contact_rx) = mpsc::channel(1); - let (started_ch_tx, _) = broadcast::channel(1); - - let ai = AgentInternal { - on_connected_tx: Mutex::new(Some(on_connected_tx)), - on_connected_rx: Mutex::new(Some(on_connected_rx)), - - done_tx: Mutex::new(Some(done_tx)), - force_candidate_contact_tx, - done_and_force_candidate_contact_rx: Mutex::new(Some(( - done_rx, - force_candidate_contact_rx, - ))), - - chan_candidate_tx: Arc::new(Mutex::new(Some(chan_candidate_tx))), - chan_candidate_pair_tx: Mutex::new(Some(chan_candidate_pair_tx)), - chan_state_tx: Mutex::new(Some(chan_state_tx)), - - on_connection_state_change_hdlr: ArcSwapOption::empty(), - on_selected_candidate_pair_change_hdlr: ArcSwapOption::empty(), - on_candidate_hdlr: ArcSwapOption::empty(), - - tie_breaker: AtomicU64::new(rand::random::()), - is_controlling: AtomicBool::new(config.is_controlling), - lite: AtomicBool::new(config.lite), - - start_time: SyncMutex::new(Instant::now()), - nominated_pair: Mutex::new(None), - - connection_state: AtomicU8::new(ConnectionState::New as u8), - - insecure_skip_verify: config.insecure_skip_verify, - - started_ch_tx: Mutex::new(Some(started_ch_tx)), - - //won't change after init_with_defaults() - max_binding_requests: 0, - host_acceptance_min_wait: Duration::from_secs(0), - srflx_acceptance_min_wait: Duration::from_secs(0), - prflx_acceptance_min_wait: Duration::from_secs(0), - relay_acceptance_min_wait: Duration::from_secs(0), - - // How long connectivity checks can fail before the ICE Agent - // goes to disconnected - disconnected_timeout: Duration::from_secs(0), - - // How long connectivity checks can fail before the ICE Agent - // goes to failed - failed_timeout: Duration::from_secs(0), - - // How often should we send keepalive packets? - // 0 means never - keepalive_interval: Duration::from_secs(0), - - // How often should we run our internal taskLoop to check for state changes when connecting - check_interval: Duration::from_secs(0), - - ufrag_pwd: Mutex::new(UfragPwd::default()), - - local_candidates: Mutex::new(HashMap::new()), - remote_candidates: Mutex::new(HashMap::new()), - - // LRU of outbound Binding request Transaction IDs - pending_binding_requests: Mutex::new(vec![]), - - // AgentConn - agent_conn: Arc::new(AgentConn::new()), - }; - - let chan_receivers = ChanReceivers { - chan_state_rx, - chan_candidate_rx, - chan_candidate_pair_rx, - }; - (ai, chan_receivers) - } - pub(crate) async fn start_connectivity_checks( - self: &Arc, - is_controlling: bool, - remote_ufrag: String, - remote_pwd: String, - ) -> Result<()> { - { - let started_ch_tx = self.started_ch_tx.lock().await; - if started_ch_tx.is_none() { - return Err(Error::ErrMultipleStart); - } - } - - log::debug!( - "Started agent: isControlling? {}, remoteUfrag: {}, remotePwd: {}", - is_controlling, - remote_ufrag, - remote_pwd - ); - self.set_remote_credentials(remote_ufrag, remote_pwd) - .await?; - self.is_controlling.store(is_controlling, Ordering::SeqCst); - self.start().await; - { - let mut started_ch_tx = self.started_ch_tx.lock().await; - started_ch_tx.take(); - } - - self.update_connection_state(ConnectionState::Checking) - .await; - - self.request_connectivity_check(); - - self.connectivity_checks().await; - - Ok(()) - } - - async fn contact( - &self, - last_connection_state: &mut ConnectionState, - checking_duration: &mut Instant, - ) { - if self.connection_state.load(Ordering::SeqCst) == ConnectionState::Failed as u8 { - // The connection is currently failed so don't send any checks - // In the future it may be restarted though - *last_connection_state = self.connection_state.load(Ordering::SeqCst).into(); - return; - } - if self.connection_state.load(Ordering::SeqCst) == ConnectionState::Checking as u8 { - // We have just entered checking for the first time so update our checking timer - if *last_connection_state as u8 != self.connection_state.load(Ordering::SeqCst) { - *checking_duration = Instant::now(); - } - - // We have been in checking longer then Disconnect+Failed timeout, set the connection to Failed - if Instant::now() - .checked_duration_since(*checking_duration) - .unwrap_or_else(|| Duration::from_secs(0)) - > self.disconnected_timeout + self.failed_timeout - { - self.update_connection_state(ConnectionState::Failed).await; - *last_connection_state = self.connection_state.load(Ordering::SeqCst).into(); - return; - } - } - - self.contact_candidates().await; - - *last_connection_state = self.connection_state.load(Ordering::SeqCst).into(); - } - - async fn connectivity_checks(self: &Arc) { - const ZERO_DURATION: Duration = Duration::from_secs(0); - let mut last_connection_state = ConnectionState::Unspecified; - let mut checking_duration = Instant::now(); - let (check_interval, keepalive_interval, disconnected_timeout, failed_timeout) = ( - self.check_interval, - self.keepalive_interval, - self.disconnected_timeout, - self.failed_timeout, - ); - - let done_and_force_candidate_contact_rx = { - let mut done_and_force_candidate_contact_rx = - self.done_and_force_candidate_contact_rx.lock().await; - done_and_force_candidate_contact_rx.take() - }; - - if let Some((mut done_rx, mut force_candidate_contact_rx)) = - done_and_force_candidate_contact_rx - { - let ai = Arc::clone(self); - tokio::spawn(async move { - loop { - let mut interval = DEFAULT_CHECK_INTERVAL; - - let mut update_interval = |x: Duration| { - if x != ZERO_DURATION && (interval == ZERO_DURATION || interval > x) { - interval = x; - } - }; - - match last_connection_state { - ConnectionState::New | ConnectionState::Checking => { - // While connecting, check candidates more frequently - update_interval(check_interval); - } - ConnectionState::Connected | ConnectionState::Disconnected => { - update_interval(keepalive_interval); - } - _ => {} - }; - // Ensure we run our task loop as quickly as the minimum of our various configured timeouts - update_interval(disconnected_timeout); - update_interval(failed_timeout); - - let t = tokio::time::sleep(interval); - tokio::pin!(t); - - tokio::select! { - _ = t.as_mut() => { - ai.contact(&mut last_connection_state, &mut checking_duration).await; - }, - _ = force_candidate_contact_rx.recv() => { - ai.contact(&mut last_connection_state, &mut checking_duration).await; - }, - _ = done_rx.recv() => { - return; - } - } - } - }); - } - } - - pub(crate) async fn update_connection_state(&self, new_state: ConnectionState) { - if self.connection_state.load(Ordering::SeqCst) != new_state as u8 { - // Connection has gone to failed, release all gathered candidates - if new_state == ConnectionState::Failed { - self.delete_all_candidates().await; - } - - log::info!( - "[{}]: Setting new connection state: {}", - self.get_name(), - new_state - ); - self.connection_state - .store(new_state as u8, Ordering::SeqCst); - - // Call handler after finishing current task since we may be holding the agent lock - // and the handler may also require it - { - let chan_state_tx = self.chan_state_tx.lock().await; - if let Some(tx) = &*chan_state_tx { - let _ = tx.send(new_state).await; - } - } - } - } - - pub(crate) async fn set_selected_pair(&self, p: Option>) { - log::trace!( - "[{}]: Set selected candidate pair: {:?}", - self.get_name(), - p - ); - - if let Some(p) = p { - p.nominated.store(true, Ordering::SeqCst); - self.agent_conn.selected_pair.store(Some(p)); - - self.update_connection_state(ConnectionState::Connected) - .await; - - // Notify when the selected pair changes - { - let chan_candidate_pair_tx = self.chan_candidate_pair_tx.lock().await; - if let Some(tx) = &*chan_candidate_pair_tx { - let _ = tx.send(()).await; - } - } - - // Signal connected - { - let mut on_connected_tx = self.on_connected_tx.lock().await; - on_connected_tx.take(); - } - } else { - self.agent_conn.selected_pair.store(None); - } - } - - pub(crate) async fn ping_all_candidates(&self) { - log::trace!("[{}]: pinging all candidates", self.get_name(),); - - let mut pairs: Vec<( - Arc, - Arc, - )> = vec![]; - - { - let mut checklist = self.agent_conn.checklist.lock().await; - if checklist.is_empty() { - log::warn!( - "[{}]: pingAllCandidates called with no candidate pairs. Connection is not possible yet.", - self.get_name(), - ); - } - for p in &mut *checklist { - let p_state = p.state.load(Ordering::SeqCst); - if p_state == CandidatePairState::Waiting as u8 { - p.state - .store(CandidatePairState::InProgress as u8, Ordering::SeqCst); - } else if p_state != CandidatePairState::InProgress as u8 { - continue; - } - - if p.binding_request_count.load(Ordering::SeqCst) > self.max_binding_requests { - log::trace!( - "[{}]: max requests reached for pair {}, marking it as failed", - self.get_name(), - p - ); - p.state - .store(CandidatePairState::Failed as u8, Ordering::SeqCst); - } else { - p.binding_request_count.fetch_add(1, Ordering::SeqCst); - let local = p.local.clone(); - let remote = p.remote.clone(); - pairs.push((local, remote)); - } - } - } - - for (local, remote) in pairs { - self.ping_candidate(&local, &remote).await; - } - } - - pub(crate) async fn add_pair( - &self, - local: Arc, - remote: Arc, - ) { - let p = Arc::new(CandidatePair::new( - local, - remote, - self.is_controlling.load(Ordering::SeqCst), - )); - let mut checklist = self.agent_conn.checklist.lock().await; - checklist.push(p); - } - - pub(crate) async fn find_pair( - &self, - local: &Arc, - remote: &Arc, - ) -> Option> { - let checklist = self.agent_conn.checklist.lock().await; - for p in &*checklist { - if p.local.equal(&**local) && p.remote.equal(&**remote) { - return Some(p.clone()); - } - } - None - } - - /// Checks if the selected pair is (still) valid. - /// Note: the caller should hold the agent lock. - pub(crate) async fn validate_selected_pair(&self) -> bool { - let (valid, disconnected_time) = { - let selected_pair = self.agent_conn.selected_pair.load(); - (*selected_pair).as_ref().map_or_else( - || (false, Duration::from_secs(0)), - |selected_pair| { - let disconnected_time = SystemTime::now() - .duration_since(selected_pair.remote.last_received()) - .unwrap_or_else(|_| Duration::from_secs(0)); - (true, disconnected_time) - }, - ) - }; - - if valid { - // Only allow transitions to failed if a.failedTimeout is non-zero - let mut total_time_to_failure = self.failed_timeout; - if total_time_to_failure != Duration::from_secs(0) { - total_time_to_failure += self.disconnected_timeout; - } - - if total_time_to_failure != Duration::from_secs(0) - && disconnected_time > total_time_to_failure - { - self.update_connection_state(ConnectionState::Failed).await; - } else if self.disconnected_timeout != Duration::from_secs(0) - && disconnected_time > self.disconnected_timeout - { - self.update_connection_state(ConnectionState::Disconnected) - .await; - } else { - self.update_connection_state(ConnectionState::Connected) - .await; - } - } - - valid - } - - /// Sends STUN Binding Indications to the selected pair. - /// if no packet has been sent on that pair in the last keepaliveInterval. - /// Note: the caller should hold the agent lock. - pub(crate) async fn check_keepalive(&self) { - let (local, remote) = { - let selected_pair = self.agent_conn.selected_pair.load(); - (*selected_pair) - .as_ref() - .map_or((None, None), |selected_pair| { - ( - Some(selected_pair.local.clone()), - Some(selected_pair.remote.clone()), - ) - }) - }; - - if let (Some(local), Some(remote)) = (local, remote) { - let last_sent = SystemTime::now() - .duration_since(local.last_sent()) - .unwrap_or_else(|_| Duration::from_secs(0)); - - let last_received = SystemTime::now() - .duration_since(remote.last_received()) - .unwrap_or_else(|_| Duration::from_secs(0)); - - if (self.keepalive_interval != Duration::from_secs(0)) - && ((last_sent > self.keepalive_interval) - || (last_received > self.keepalive_interval)) - { - // we use binding request instead of indication to support refresh consent schemas - // see https://tools.ietf.org/html/rfc7675 - self.ping_candidate(&local, &remote).await; - } - } - } - - fn request_connectivity_check(&self) { - let _ = self.force_candidate_contact_tx.try_send(true); - } - - /// Assumes you are holding the lock (must be execute using a.run). - pub(crate) async fn add_remote_candidate(&self, c: &Arc) { - let network_type = c.network_type(); - - { - let mut remote_candidates = self.remote_candidates.lock().await; - if let Some(cands) = remote_candidates.get(&network_type) { - for cand in cands { - if cand.equal(&**c) { - return; - } - } - } - - if let Some(cands) = remote_candidates.get_mut(&network_type) { - cands.push(c.clone()); - } else { - remote_candidates.insert(network_type, vec![c.clone()]); - } - } - - let mut local_cands = vec![]; - { - let local_candidates = self.local_candidates.lock().await; - if let Some(cands) = local_candidates.get(&network_type) { - local_cands.clone_from(cands); - } - } - - for cand in local_cands { - self.add_pair(cand, c.clone()).await; - } - - self.request_connectivity_check(); - } - - pub(crate) async fn add_candidate( - self: &Arc, - c: &Arc, - ) -> Result<()> { - let initialized_ch = { - let started_ch_tx = self.started_ch_tx.lock().await; - (*started_ch_tx).as_ref().map(|tx| tx.subscribe()) - }; - - self.start_candidate(c, initialized_ch).await; - - let network_type = c.network_type(); - { - let mut local_candidates = self.local_candidates.lock().await; - if let Some(cands) = local_candidates.get(&network_type) { - for cand in cands { - if cand.equal(&**c) { - if let Err(err) = c.close().await { - log::warn!( - "[{}]: Failed to close duplicate candidate: {}", - self.get_name(), - err - ); - } - //TODO: why return? - return Ok(()); - } - } - } - - if let Some(cands) = local_candidates.get_mut(&network_type) { - cands.push(c.clone()); - } else { - local_candidates.insert(network_type, vec![c.clone()]); - } - } - - let mut remote_cands = vec![]; - { - let remote_candidates = self.remote_candidates.lock().await; - if let Some(cands) = remote_candidates.get(&network_type) { - remote_cands.clone_from(cands); - } - } - - for cand in remote_cands { - self.add_pair(c.clone(), cand).await; - } - - self.request_connectivity_check(); - { - let chan_candidate_tx = self.chan_candidate_tx.lock().await; - if let Some(tx) = &*chan_candidate_tx { - let _ = tx.send(Some(c.clone())).await; - } - } - - Ok(()) - } - - pub(crate) async fn close(&self) -> Result<()> { - { - let mut done_tx = self.done_tx.lock().await; - if done_tx.is_none() { - return Err(Error::ErrClosed); - } - done_tx.take(); - }; - self.delete_all_candidates().await; - { - let mut started_ch_tx = self.started_ch_tx.lock().await; - started_ch_tx.take(); - } - - self.agent_conn.buffer.close().await; - - self.update_connection_state(ConnectionState::Closed).await; - - { - let mut chan_candidate_tx = self.chan_candidate_tx.lock().await; - chan_candidate_tx.take(); - } - { - let mut chan_candidate_pair_tx = self.chan_candidate_pair_tx.lock().await; - chan_candidate_pair_tx.take(); - } - { - let mut chan_state_tx = self.chan_state_tx.lock().await; - chan_state_tx.take(); - } - - self.agent_conn.done.store(true, Ordering::SeqCst); - - Ok(()) - } - - /// Remove all candidates. - /// This closes any listening sockets and removes both the local and remote candidate lists. - /// - /// This is used for restarts, failures and on close. - pub(crate) async fn delete_all_candidates(&self) { - { - let mut local_candidates = self.local_candidates.lock().await; - for cs in local_candidates.values_mut() { - for c in cs { - if let Err(err) = c.close().await { - log::warn!( - "[{}]: Failed to close candidate {}: {}", - self.get_name(), - c, - err - ); - } - } - } - local_candidates.clear(); - } - - { - let mut remote_candidates = self.remote_candidates.lock().await; - for cs in remote_candidates.values_mut() { - for c in cs { - if let Err(err) = c.close().await { - log::warn!( - "[{}]: Failed to close candidate {}: {}", - self.get_name(), - c, - err - ); - } - } - } - remote_candidates.clear(); - } - } - - pub(crate) async fn find_remote_candidate( - &self, - network_type: NetworkType, - addr: SocketAddr, - ) -> Option> { - let (ip, port) = (addr.ip(), addr.port()); - - let remote_candidates = self.remote_candidates.lock().await; - if let Some(cands) = remote_candidates.get(&network_type) { - for c in cands { - if c.address() == ip.to_string() && c.port() == port { - return Some(c.clone()); - } - } - } - None - } - - pub(crate) async fn send_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ) { - log::trace!( - "[{}]: ping STUN from {} to {}", - self.get_name(), - local, - remote - ); - - self.invalidate_pending_binding_requests(Instant::now()) - .await; - { - let mut pending_binding_requests = self.pending_binding_requests.lock().await; - pending_binding_requests.push(BindingRequest { - timestamp: Instant::now(), - transaction_id: m.transaction_id, - destination: remote.addr(), - is_use_candidate: m.contains(ATTR_USE_CANDIDATE), - }); - } - - self.send_stun(m, local, remote).await; - } - - pub(crate) async fn send_binding_success( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ) { - let addr = remote.addr(); - let (ip, port) = (addr.ip(), addr.port()); - let local_pwd = { - let ufrag_pwd = self.ufrag_pwd.lock().await; - ufrag_pwd.local_pwd.clone() - }; - - let (out, result) = { - let mut out = Message::new(); - let result = out.build(&[ - Box::new(m.clone()), - Box::new(BINDING_SUCCESS), - Box::new(XorMappedAddress { ip, port }), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), - Box::new(FINGERPRINT), - ]); - (out, result) - }; - - if let Err(err) = result { - log::warn!( - "[{}]: Failed to handle inbound ICE from: {} to: {} error: {}", - self.get_name(), - local, - remote, - err - ); - } else { - self.send_stun(&out, local, remote).await; - } - } - - /// Removes pending binding requests that are over `maxBindingRequestTimeout` old Let HTO be the - /// transaction timeout, which SHOULD be 2*RTT if RTT is known or 500 ms otherwise. - /// - /// reference: (IETF ref-8445)[https://tools.ietf.org/html/rfc8445#appendix-B.1]. - pub(crate) async fn invalidate_pending_binding_requests(&self, filter_time: Instant) { - let mut pending_binding_requests = self.pending_binding_requests.lock().await; - let initial_size = pending_binding_requests.len(); - - let mut temp = vec![]; - for binding_request in pending_binding_requests.drain(..) { - if filter_time - .checked_duration_since(binding_request.timestamp) - .map(|duration| duration < MAX_BINDING_REQUEST_TIMEOUT) - .unwrap_or(true) - { - temp.push(binding_request); - } - } - - *pending_binding_requests = temp; - let bind_requests_removed = initial_size - pending_binding_requests.len(); - if bind_requests_removed > 0 { - log::trace!( - "[{}]: Discarded {} binding requests because they expired", - self.get_name(), - bind_requests_removed - ); - } - } - - /// Assert that the passed `TransactionID` is in our `pendingBindingRequests` and returns the - /// destination, If the bindingRequest was valid remove it from our pending cache. - pub(crate) async fn handle_inbound_binding_success( - &self, - id: TransactionId, - ) -> Option { - self.invalidate_pending_binding_requests(Instant::now()) - .await; - - let mut pending_binding_requests = self.pending_binding_requests.lock().await; - for i in 0..pending_binding_requests.len() { - if pending_binding_requests[i].transaction_id == id { - let valid_binding_request = pending_binding_requests.remove(i); - return Some(valid_binding_request); - } - } - None - } - - /// Processes STUN traffic from a remote candidate. - pub(crate) async fn handle_inbound( - &self, - m: &mut Message, - local: &Arc, - remote: SocketAddr, - ) { - if m.typ.method != METHOD_BINDING - || !(m.typ.class == CLASS_SUCCESS_RESPONSE - || m.typ.class == CLASS_REQUEST - || m.typ.class == CLASS_INDICATION) - { - log::trace!( - "[{}]: unhandled STUN from {} to {} class({}) method({})", - self.get_name(), - remote, - local, - m.typ.class, - m.typ.method - ); - return; - } - - if self.is_controlling.load(Ordering::SeqCst) { - if m.contains(ATTR_ICE_CONTROLLING) { - log::debug!( - "[{}]: inbound isControlling && a.isControlling == true", - self.get_name(), - ); - return; - } else if m.contains(ATTR_USE_CANDIDATE) { - log::debug!( - "[{}]: useCandidate && a.isControlling == true", - self.get_name(), - ); - return; - } - } else if m.contains(ATTR_ICE_CONTROLLED) { - log::debug!( - "[{}]: inbound isControlled && a.isControlling == false", - self.get_name(), - ); - return; - } - - let mut remote_candidate = self - .find_remote_candidate(local.network_type(), remote) - .await; - if m.typ.class == CLASS_SUCCESS_RESPONSE { - { - let ufrag_pwd = self.ufrag_pwd.lock().await; - if let Err(err) = - assert_inbound_message_integrity(m, ufrag_pwd.remote_pwd.as_bytes()) - { - log::warn!( - "[{}]: discard message from ({}), {}", - self.get_name(), - remote, - err - ); - return; - } - } - - if let Some(rc) = &remote_candidate { - self.handle_success_response(m, local, rc, remote).await; - } else { - log::warn!( - "[{}]: discard success message from ({}), no such remote", - self.get_name(), - remote - ); - return; - } - } else if m.typ.class == CLASS_REQUEST { - { - let ufrag_pwd = self.ufrag_pwd.lock().await; - let username = - ufrag_pwd.local_ufrag.clone() + ":" + ufrag_pwd.remote_ufrag.as_str(); - if let Err(err) = assert_inbound_username(m, &username) { - log::warn!( - "[{}]: discard message from ({}), {}", - self.get_name(), - remote, - err - ); - return; - } else if let Err(err) = - assert_inbound_message_integrity(m, ufrag_pwd.local_pwd.as_bytes()) - { - log::warn!( - "[{}]: discard message from ({}), {}", - self.get_name(), - remote, - err - ); - return; - } - } - - if remote_candidate.is_none() { - let (ip, port, network_type) = (remote.ip(), remote.port(), NetworkType::Udp4); - - let prflx_candidate_config = CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network: network_type.to_string(), - address: ip.to_string(), - port, - component: local.component(), - ..CandidateBaseConfig::default() - }, - rel_addr: "".to_owned(), - rel_port: 0, - }; - - match prflx_candidate_config.new_candidate_peer_reflexive() { - Ok(prflx_candidate) => remote_candidate = Some(Arc::new(prflx_candidate)), - Err(err) => { - log::error!( - "[{}]: Failed to create new remote prflx candidate ({})", - self.get_name(), - err - ); - return; - } - }; - - log::debug!( - "[{}]: adding a new peer-reflexive candidate: {} ", - self.get_name(), - remote - ); - if let Some(rc) = &remote_candidate { - self.add_remote_candidate(rc).await; - } - } - - log::trace!( - "[{}]: inbound STUN (Request) from {} to {}", - self.get_name(), - remote, - local - ); - - if let Some(rc) = &remote_candidate { - self.handle_binding_request(m, local, rc).await; - } - } - - if let Some(rc) = remote_candidate { - rc.seen(false); - } - } - - /// Processes non STUN traffic from a remote candidate, and returns true if it is an actual - /// remote candidate. - pub(crate) async fn validate_non_stun_traffic( - &self, - local: &Arc, - remote: SocketAddr, - ) -> bool { - self.find_remote_candidate(local.network_type(), remote) - .await - .map_or(false, |remote_candidate| { - remote_candidate.seen(false); - true - }) - } - - /// Sets the credentials of the remote agent. - pub(crate) async fn set_remote_credentials( - &self, - remote_ufrag: String, - remote_pwd: String, - ) -> Result<()> { - if remote_ufrag.is_empty() { - return Err(Error::ErrRemoteUfragEmpty); - } else if remote_pwd.is_empty() { - return Err(Error::ErrRemotePwdEmpty); - } - - let mut ufrag_pwd = self.ufrag_pwd.lock().await; - ufrag_pwd.remote_ufrag = remote_ufrag; - ufrag_pwd.remote_pwd = remote_pwd; - Ok(()) - } - - pub(crate) async fn send_stun( - &self, - msg: &Message, - local: &Arc, - remote: &Arc, - ) { - if let Err(err) = local.write_to(&msg.raw, &**remote).await { - log::trace!( - "[{}]: failed to send STUN message: {}", - self.get_name(), - err - ); - } - } - - /// Runs the candidate using the provided connection. - async fn start_candidate( - self: &Arc, - candidate: &Arc, - initialized_ch: Option>, - ) { - let (closed_ch_tx, closed_ch_rx) = broadcast::channel(1); - { - let closed_ch = candidate.get_closed_ch(); - let mut closed = closed_ch.lock().await; - *closed = Some(closed_ch_tx); - } - - let cand = Arc::clone(candidate); - if let Some(conn) = candidate.get_conn() { - let conn = Arc::clone(conn); - let addr = candidate.addr(); - let ai = Arc::clone(self); - tokio::spawn(async move { - let _ = ai - .recv_loop(cand, closed_ch_rx, initialized_ch, conn, addr) - .await; - }); - } else { - log::error!("[{}]: Can't start due to conn is_none", self.get_name(),); - } - } - - pub(super) fn start_on_connection_state_change_routine( - self: &Arc, - mut chan_state_rx: mpsc::Receiver, - mut chan_candidate_rx: mpsc::Receiver>>, - mut chan_candidate_pair_rx: mpsc::Receiver<()>, - ) { - let ai = Arc::clone(self); - tokio::spawn(async move { - // CandidatePair and ConnectionState are usually changed at once. - // Blocking one by the other one causes deadlock. - while chan_candidate_pair_rx.recv().await.is_some() { - if let (Some(cb), Some(p)) = ( - &*ai.on_selected_candidate_pair_change_hdlr.load(), - &*ai.agent_conn.selected_pair.load(), - ) { - let mut f = cb.lock().await; - f(&p.local, &p.remote).await; - } - } - }); - - let ai = Arc::clone(self); - tokio::spawn(async move { - loop { - tokio::select! { - opt_state = chan_state_rx.recv() => { - if let Some(s) = opt_state { - if let Some(handler) = &*ai.on_connection_state_change_hdlr.load() { - let mut f = handler.lock().await; - f(s).await; - } - } else { - while let Some(c) = chan_candidate_rx.recv().await { - if let Some(handler) = &*ai.on_candidate_hdlr.load() { - let mut f = handler.lock().await; - f(c).await; - } - } - break; - } - }, - opt_cand = chan_candidate_rx.recv() => { - if let Some(c) = opt_cand { - if let Some(handler) = &*ai.on_candidate_hdlr.load() { - let mut f = handler.lock().await; - f(c).await; - } - } else { - while let Some(s) = chan_state_rx.recv().await { - if let Some(handler) = &*ai.on_connection_state_change_hdlr.load() { - let mut f = handler.lock().await; - f(s).await; - } - } - break; - } - } - } - } - }); - } - - async fn recv_loop( - self: &Arc, - candidate: Arc, - mut closed_ch_rx: broadcast::Receiver<()>, - initialized_ch: Option>, - conn: Arc, - addr: SocketAddr, - ) -> Result<()> { - if let Some(mut initialized_ch) = initialized_ch { - tokio::select! { - _ = initialized_ch.recv() => {} - _ = closed_ch_rx.recv() => return Err(Error::ErrClosed), - } - } - - let mut buffer = vec![0_u8; RECEIVE_MTU]; - let mut n; - let mut src_addr; - loop { - tokio::select! { - result = conn.recv_from(&mut buffer) => { - match result { - Ok((num, src)) => { - n = num; - src_addr = src; - } - Err(err) => return Err(Error::Other(err.to_string())), - } - }, - _ = closed_ch_rx.recv() => return Err(Error::ErrClosed), - } - - self.handle_inbound_candidate_msg(&candidate, &buffer[..n], src_addr, addr) - .await; - } - } - - async fn handle_inbound_candidate_msg( - self: &Arc, - c: &Arc, - buf: &[u8], - src_addr: SocketAddr, - addr: SocketAddr, - ) { - if stun::message::is_message(buf) { - let mut m = Message { - raw: vec![], - ..Message::default() - }; - // Explicitly copy raw buffer so Message can own the memory. - m.raw.extend_from_slice(buf); - - if let Err(err) = m.decode() { - log::warn!( - "[{}]: Failed to handle decode ICE from {} to {}: {}", - self.get_name(), - addr, - src_addr, - err - ); - } else { - self.handle_inbound(&mut m, c, src_addr).await; - } - } else if !self.validate_non_stun_traffic(c, src_addr).await { - log::warn!( - "[{}]: Discarded message, not a valid remote candidate", - self.get_name(), - //c.addr().await //from {} - ); - } else if let Err(err) = self.agent_conn.buffer.write(buf).await { - // NOTE This will return packetio.ErrFull if the buffer ever manages to fill up. - log::warn!("[{}]: failed to write packet: {}", self.get_name(), err); - } - } - - pub(crate) fn get_name(&self) -> &str { - if self.is_controlling.load(Ordering::SeqCst) { - "controlling" - } else { - "controlled" - } - } -} diff --git a/ice/src/agent/agent_selector.rs b/ice/src/agent/agent_selector.rs deleted file mode 100644 index b7e05fc40..000000000 --- a/ice/src/agent/agent_selector.rs +++ /dev/null @@ -1,545 +0,0 @@ -use std::net::SocketAddr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use stun::agent::*; -use stun::attributes::*; -use stun::fingerprint::*; -use stun::integrity::*; -use stun::message::*; -use stun::textattrs::*; -use tokio::time::{Duration, Instant}; - -use crate::agent::agent_internal::*; -use crate::candidate::*; -use crate::control::*; -use crate::priority::*; -use crate::use_candidate::*; - -#[async_trait] -trait ControllingSelector { - async fn start(&self); - async fn contact_candidates(&self); - async fn ping_candidate( - &self, - local: &Arc, - remote: &Arc, - ); - async fn handle_success_response( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - remote_addr: SocketAddr, - ); - async fn handle_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ); -} - -#[async_trait] -trait ControlledSelector { - async fn start(&self); - async fn contact_candidates(&self); - async fn ping_candidate( - &self, - local: &Arc, - remote: &Arc, - ); - async fn handle_success_response( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - remote_addr: SocketAddr, - ); - async fn handle_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ); -} - -impl AgentInternal { - fn is_nominatable(&self, c: &Arc) -> bool { - let start_time = *self.start_time.lock(); - match c.candidate_type() { - CandidateType::Host => { - Instant::now() - .checked_duration_since(start_time) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_nanos() - > self.host_acceptance_min_wait.as_nanos() - } - CandidateType::ServerReflexive => { - Instant::now() - .checked_duration_since(start_time) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_nanos() - > self.srflx_acceptance_min_wait.as_nanos() - } - CandidateType::PeerReflexive => { - Instant::now() - .checked_duration_since(start_time) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_nanos() - > self.prflx_acceptance_min_wait.as_nanos() - } - CandidateType::Relay => { - Instant::now() - .checked_duration_since(start_time) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_nanos() - > self.relay_acceptance_min_wait.as_nanos() - } - CandidateType::Unspecified => { - log::error!( - "is_nominatable invalid candidate type {}", - c.candidate_type() - ); - false - } - } - } - - async fn nominate_pair(&self) { - let result = { - let nominated_pair = self.nominated_pair.lock().await; - if let Some(pair) = &*nominated_pair { - // The controlling agent MUST include the USE-CANDIDATE attribute in - // order to nominate a candidate pair (Section 8.1.1). The controlled - // agent MUST NOT include the USE-CANDIDATE attribute in a Binding - // request. - - let (msg, result) = { - let ufrag_pwd = self.ufrag_pwd.lock().await; - let username = - ufrag_pwd.remote_ufrag.clone() + ":" + ufrag_pwd.local_ufrag.as_str(); - let mut msg = Message::new(); - let result = msg.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::::default(), - Box::new(AttrControlling(self.tie_breaker.load(Ordering::SeqCst))), - Box::new(PriorityAttr(pair.local.priority())), - Box::new(MessageIntegrity::new_short_term_integrity( - ufrag_pwd.remote_pwd.clone(), - )), - Box::new(FINGERPRINT), - ]); - (msg, result) - }; - - if let Err(err) = result { - log::error!("{}", err); - None - } else { - log::trace!( - "ping STUN (nominate candidate pair from {} to {}", - pair.local, - pair.remote - ); - let local = pair.local.clone(); - let remote = pair.remote.clone(); - Some((msg, local, remote)) - } - } else { - None - } - }; - - if let Some((msg, local, remote)) = result { - self.send_binding_request(&msg, &local, &remote).await; - } - } - - pub(crate) async fn start(&self) { - if self.is_controlling.load(Ordering::SeqCst) { - ControllingSelector::start(self).await; - } else { - ControlledSelector::start(self).await; - } - } - - pub(crate) async fn contact_candidates(&self) { - if self.is_controlling.load(Ordering::SeqCst) { - ControllingSelector::contact_candidates(self).await; - } else { - ControlledSelector::contact_candidates(self).await; - } - } - - pub(crate) async fn ping_candidate( - &self, - local: &Arc, - remote: &Arc, - ) { - if self.is_controlling.load(Ordering::SeqCst) { - ControllingSelector::ping_candidate(self, local, remote).await; - } else { - ControlledSelector::ping_candidate(self, local, remote).await; - } - } - - pub(crate) async fn handle_success_response( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - remote_addr: SocketAddr, - ) { - if self.is_controlling.load(Ordering::SeqCst) { - ControllingSelector::handle_success_response(self, m, local, remote, remote_addr).await; - } else { - ControlledSelector::handle_success_response(self, m, local, remote, remote_addr).await; - } - } - - pub(crate) async fn handle_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ) { - if self.is_controlling.load(Ordering::SeqCst) { - ControllingSelector::handle_binding_request(self, m, local, remote).await; - } else { - ControlledSelector::handle_binding_request(self, m, local, remote).await; - } - } -} - -#[async_trait] -impl ControllingSelector for AgentInternal { - async fn start(&self) { - { - let mut nominated_pair = self.nominated_pair.lock().await; - *nominated_pair = None; - } - *self.start_time.lock() = Instant::now(); - } - - async fn contact_candidates(&self) { - // A lite selector should not contact candidates - if self.lite.load(Ordering::SeqCst) { - // This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2 - log::trace!("now falling back to full agent"); - } - - let nominated_pair_is_some = { - let nominated_pair = self.nominated_pair.lock().await; - nominated_pair.is_some() - }; - - if self.agent_conn.get_selected_pair().is_some() { - if self.validate_selected_pair().await { - log::trace!("[{}]: checking keepalive", self.get_name()); - self.check_keepalive().await; - } - } else if nominated_pair_is_some { - self.nominate_pair().await; - } else { - let has_nominated_pair = - if let Some(p) = self.agent_conn.get_best_valid_candidate_pair().await { - self.is_nominatable(&p.local) && self.is_nominatable(&p.remote) - } else { - false - }; - - if has_nominated_pair { - if let Some(p) = self.agent_conn.get_best_valid_candidate_pair().await { - log::trace!( - "Nominatable pair found, nominating ({}, {})", - p.local.to_string(), - p.remote.to_string() - ); - p.nominated.store(true, Ordering::SeqCst); - { - let mut nominated_pair = self.nominated_pair.lock().await; - *nominated_pair = Some(p); - } - } - - self.nominate_pair().await; - } else { - self.ping_all_candidates().await; - } - } - } - - async fn ping_candidate( - &self, - local: &Arc, - remote: &Arc, - ) { - let (msg, result) = { - let ufrag_pwd = self.ufrag_pwd.lock().await; - let username = ufrag_pwd.remote_ufrag.clone() + ":" + ufrag_pwd.local_ufrag.as_str(); - let mut msg = Message::new(); - let result = msg.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(AttrControlling(self.tie_breaker.load(Ordering::SeqCst))), - Box::new(PriorityAttr(local.priority())), - Box::new(MessageIntegrity::new_short_term_integrity( - ufrag_pwd.remote_pwd.clone(), - )), - Box::new(FINGERPRINT), - ]); - (msg, result) - }; - - if let Err(err) = result { - log::error!("{}", err); - } else { - self.send_binding_request(&msg, local, remote).await; - } - } - - async fn handle_success_response( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - remote_addr: SocketAddr, - ) { - if let Some(pending_request) = self.handle_inbound_binding_success(m.transaction_id).await { - let transaction_addr = pending_request.destination; - - // Assert that NAT is not symmetric - // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 - if transaction_addr != remote_addr { - log::debug!("discard message: transaction source and destination does not match expected({}), actual({})", transaction_addr, remote); - return; - } - - log::trace!( - "inbound STUN (SuccessResponse) from {} to {}", - remote, - local - ); - let selected_pair_is_none = self.agent_conn.get_selected_pair().is_none(); - - if let Some(p) = self.find_pair(local, remote).await { - p.state - .store(CandidatePairState::Succeeded as u8, Ordering::SeqCst); - log::trace!( - "Found valid candidate pair: {}, p.state: {}, isUseCandidate: {}, {}", - p, - p.state.load(Ordering::SeqCst), - pending_request.is_use_candidate, - selected_pair_is_none - ); - if pending_request.is_use_candidate && selected_pair_is_none { - self.set_selected_pair(Some(Arc::clone(&p))).await; - } - } else { - // This shouldn't happen - log::error!("Success response from invalid candidate pair"); - } - } else { - log::warn!( - "discard message from ({}), unknown TransactionID 0x{:?}", - remote, - m.transaction_id - ); - } - } - - async fn handle_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ) { - self.send_binding_success(m, local, remote).await; - log::trace!("controllingSelector: sendBindingSuccess"); - - if let Some(p) = self.find_pair(local, remote).await { - let nominated_pair_is_none = { - let nominated_pair = self.nominated_pair.lock().await; - nominated_pair.is_none() - }; - - log::trace!( - "controllingSelector: after findPair {}, p.state: {}, {}", - p, - p.state.load(Ordering::SeqCst), - nominated_pair_is_none, - //self.agent_conn.get_selected_pair().await.is_none() //, {} - ); - if p.state.load(Ordering::SeqCst) == CandidatePairState::Succeeded as u8 - && nominated_pair_is_none - && self.agent_conn.get_selected_pair().is_none() - { - if let Some(best_pair) = self.agent_conn.get_best_available_candidate_pair().await { - log::trace!( - "controllingSelector: getBestAvailableCandidatePair {}", - best_pair - ); - if best_pair == p - && self.is_nominatable(&p.local) - && self.is_nominatable(&p.remote) - { - log::trace!("The candidate ({}, {}) is the best candidate available, marking it as nominated", - p.local, p.remote); - { - let mut nominated_pair = self.nominated_pair.lock().await; - *nominated_pair = Some(p); - } - self.nominate_pair().await; - } - } else { - log::trace!("No best pair available"); - } - } - } else { - log::trace!("controllingSelector: addPair"); - self.add_pair(local.clone(), remote.clone()).await; - } - } -} - -#[async_trait] -impl ControlledSelector for AgentInternal { - async fn start(&self) {} - - async fn contact_candidates(&self) { - // A lite selector should not contact candidates - if self.lite.load(Ordering::SeqCst) { - self.validate_selected_pair().await; - } else if self.agent_conn.get_selected_pair().is_some() { - if self.validate_selected_pair().await { - log::trace!("[{}]: checking keepalive", self.get_name()); - self.check_keepalive().await; - } - } else { - self.ping_all_candidates().await; - } - } - - async fn ping_candidate( - &self, - local: &Arc, - remote: &Arc, - ) { - let (msg, result) = { - let ufrag_pwd = self.ufrag_pwd.lock().await; - let username = ufrag_pwd.remote_ufrag.clone() + ":" + ufrag_pwd.local_ufrag.as_str(); - let mut msg = Message::new(); - let result = msg.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(AttrControlled(self.tie_breaker.load(Ordering::SeqCst))), - Box::new(PriorityAttr(local.priority())), - Box::new(MessageIntegrity::new_short_term_integrity( - ufrag_pwd.remote_pwd.clone(), - )), - Box::new(FINGERPRINT), - ]); - (msg, result) - }; - - if let Err(err) = result { - log::error!("{}", err); - } else { - self.send_binding_request(&msg, local, remote).await; - } - } - - async fn handle_success_response( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - remote_addr: SocketAddr, - ) { - // https://tools.ietf.org/html/rfc8445#section-7.3.1.5 - // If the controlled agent does not accept the request from the - // controlling agent, the controlled agent MUST reject the nomination - // request with an appropriate error code response (e.g., 400) - // [RFC5389]. - - if let Some(pending_request) = self.handle_inbound_binding_success(m.transaction_id).await { - let transaction_addr = pending_request.destination; - - // Assert that NAT is not symmetric - // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 - if transaction_addr != remote_addr { - log::debug!("discard message: transaction source and destination does not match expected({}), actual({})", transaction_addr, remote); - return; - } - - log::trace!( - "inbound STUN (SuccessResponse) from {} to {}", - remote, - local - ); - - if let Some(p) = self.find_pair(local, remote).await { - p.state - .store(CandidatePairState::Succeeded as u8, Ordering::SeqCst); - log::trace!("Found valid candidate pair: {}", p); - } else { - // This shouldn't happen - log::error!("Success response from invalid candidate pair"); - } - } else { - log::warn!( - "discard message from ({}), unknown TransactionID 0x{:?}", - remote, - m.transaction_id - ); - } - } - - async fn handle_binding_request( - &self, - m: &Message, - local: &Arc, - remote: &Arc, - ) { - if self.find_pair(local, remote).await.is_none() { - self.add_pair(local.clone(), remote.clone()).await; - } - - if let Some(p) = self.find_pair(local, remote).await { - let use_candidate = m.contains(ATTR_USE_CANDIDATE); - if use_candidate { - // https://tools.ietf.org/html/rfc8445#section-7.3.1.5 - - if p.state.load(Ordering::SeqCst) == CandidatePairState::Succeeded as u8 { - // If the state of this pair is Succeeded, it means that the check - // previously sent by this pair produced a successful response and - // generated a valid pair (Section 7.2.5.3.2). The agent sets the - // nominated flag value of the valid pair to true. - if self.agent_conn.get_selected_pair().is_none() { - self.set_selected_pair(Some(Arc::clone(&p))).await; - } - self.send_binding_success(m, local, remote).await; - } else { - // If the received Binding request triggered a new check to be - // enqueued in the triggered-check queue (Section 7.3.1.4), once the - // check is sent and if it generates a successful response, and - // generates a valid pair, the agent sets the nominated flag of the - // pair to true. If the request fails (Section 7.2.5.2), the agent - // MUST remove the candidate pair from the valid list, set the - // candidate pair state to Failed, and set the checklist state to - // Failed. - self.ping_candidate(local, remote).await; - } - } else { - self.send_binding_success(m, local, remote).await; - self.ping_candidate(local, remote).await; - } - } - } -} diff --git a/ice/src/agent/agent_stats.rs b/ice/src/agent/agent_stats.rs deleted file mode 100644 index 27ad3cc31..000000000 --- a/ice/src/agent/agent_stats.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::sync::atomic::Ordering; - -use tokio::time::Instant; - -use crate::agent::agent_internal::AgentInternal; -use crate::candidate::{CandidatePairState, CandidateType}; -use crate::network_type::NetworkType; - -/// Contains ICE candidate pair statistics. -pub struct CandidatePairStats { - /// The timestamp associated with this struct. - pub timestamp: Instant, - - /// The id of the local candidate. - pub local_candidate_id: String, - - /// The id of the remote candidate. - pub remote_candidate_id: String, - - /// The state of the checklist for the local and remote candidates in a pair. - pub state: CandidatePairState, - - /// It is true when this valid pair that should be used for media, - /// if it is the highest-priority one amongst those whose nominated flag is set. - pub nominated: bool, - - /// The total number of packets sent on this candidate pair. - pub packets_sent: u32, - - /// The total number of packets received on this candidate pair. - pub packets_received: u32, - - /// The total number of payload bytes sent on this candidate pair not including headers or - /// padding. - pub bytes_sent: u64, - - /// The total number of payload bytes received on this candidate pair not including headers or - /// padding. - pub bytes_received: u64, - - /// The timestamp at which the last packet was sent on this particular candidate pair, excluding - /// STUN packets. - pub last_packet_sent_timestamp: Instant, - - /// The timestamp at which the last packet was received on this particular candidate pair, - /// excluding STUN packets. - pub last_packet_received_timestamp: Instant, - - /// The timestamp at which the first STUN request was sent on this particular candidate pair. - pub first_request_timestamp: Instant, - - /// The timestamp at which the last STUN request was sent on this particular candidate pair. - /// The average interval between two consecutive connectivity checks sent can be calculated with - /// (last_request_timestamp - first_request_timestamp) / requests_sent. - pub last_request_timestamp: Instant, - - /// Timestamp at which the last STUN response was received on this particular candidate pair. - pub last_response_timestamp: Instant, - - /// The sum of all round trip time measurements in seconds since the beginning of the session, - /// based on STUN connectivity check responses (responses_received), including those that reply - /// to requests that are sent in order to verify consent. The average round trip time can be - /// computed from total_round_trip_time by dividing it by responses_received. - pub total_round_trip_time: f64, - - /// The latest round trip time measured in seconds, computed from both STUN connectivity checks, - /// including those that are sent for consent verification. - pub current_round_trip_time: f64, - - /// It is calculated by the underlying congestion control by combining the available bitrate for - /// all the outgoing RTP streams using this candidate pair. The bitrate measurement does not - /// count the size of the IP or other transport layers like TCP or UDP. It is similar to the - /// TIAS defined in RFC 3890, i.e., it is measured in bits per second and the bitrate is - /// calculated over a 1 second window. - pub available_outgoing_bitrate: f64, - - /// It is calculated by the underlying congestion control by combining the available bitrate for - /// all the incoming RTP streams using this candidate pair. The bitrate measurement does not - /// count the size of the IP or other transport layers like TCP or UDP. It is similar to the - /// TIAS defined in RFC 3890, i.e., it is measured in bits per second and the bitrate is - /// calculated over a 1 second window. - pub available_incoming_bitrate: f64, - - /// The number of times the circuit breaker is triggered for this particular 5-tuple, - /// ceasing transmission. - pub circuit_breaker_trigger_count: u32, - - /// The total number of connectivity check requests received (including retransmissions). - /// It is impossible for the receiver to tell whether the request was sent in order to check - /// connectivity or check consent, so all connectivity checks requests are counted here. - pub requests_received: u64, - - /// The total number of connectivity check requests sent (not including retransmissions). - pub requests_sent: u64, - - /// The total number of connectivity check responses received. - pub responses_received: u64, - - /// The total number of connectivity check responses sent. Since we cannot distinguish - /// connectivity check requests and consent requests, all responses are counted. - pub responses_sent: u64, - - /// The total number of connectivity check request retransmissions received. - pub retransmissions_received: u64, - - /// The total number of connectivity check request retransmissions sent. - pub retransmissions_sent: u64, - - /// The total number of consent requests sent. - pub consent_requests_sent: u64, - - /// The timestamp at which the latest valid STUN binding response expired. - pub consent_expired_timestamp: Instant, -} - -impl Default for CandidatePairStats { - fn default() -> Self { - Self { - timestamp: Instant::now(), - local_candidate_id: String::new(), - remote_candidate_id: String::new(), - state: CandidatePairState::default(), - nominated: false, - packets_sent: 0, - packets_received: 0, - bytes_sent: 0, - bytes_received: 0, - last_packet_sent_timestamp: Instant::now(), - last_packet_received_timestamp: Instant::now(), - first_request_timestamp: Instant::now(), - last_request_timestamp: Instant::now(), - last_response_timestamp: Instant::now(), - total_round_trip_time: 0.0, - current_round_trip_time: 0.0, - available_outgoing_bitrate: 0.0, - available_incoming_bitrate: 0.0, - circuit_breaker_trigger_count: 0, - requests_received: 0, - requests_sent: 0, - responses_received: 0, - responses_sent: 0, - retransmissions_received: 0, - retransmissions_sent: 0, - consent_requests_sent: 0, - consent_expired_timestamp: Instant::now(), - } - } -} - -/// Contains ICE candidate statistics related to the `ICETransport` objects. -#[derive(Debug, Clone)] -pub struct CandidateStats { - // The timestamp associated with this struct. - pub timestamp: Instant, - - /// The candidate id. - pub id: String, - - /// The type of network interface used by the base of a local candidate (the address the ICE - /// agent sends from). Only present for local candidates; it's not possible to know what type of - /// network interface a remote candidate is using. - /// - /// Note: This stat only tells you about the network interface used by the first "hop"; it's - /// possible that a connection will be bottlenecked by another type of network. For example, - /// when using Wi-Fi tethering, the networkType of the relevant candidate would be "wifi", even - /// when the next hop is over a cellular connection. - pub network_type: NetworkType, - - /// The IP address of the candidate, allowing for IPv4 addresses and IPv6 addresses, but fully - /// qualified domain names (FQDNs) are not allowed. - pub ip: String, - - /// The port number of the candidate. - pub port: u16, - - /// The `Type` field of the ICECandidate. - pub candidate_type: CandidateType, - - /// The `priority` field of the ICECandidate. - pub priority: u32, - - /// The url of the TURN or STUN server indicated in the that translated this IP address. - /// It is the url address surfaced in an PeerConnectionICEEvent. - pub url: String, - - /// The protocol used by the endpoint to communicate with the TURN server. This is only present - /// for local candidates. Valid values for the TURN url protocol is one of udp, tcp, or tls. - pub relay_protocol: String, - - /// It is true if the candidate has been deleted/freed. For host candidates, this means that any - /// network resources (typically a socket) associated with the candidate have been released. For - /// TURN candidates, this means the TURN allocation is no longer active. - /// - /// Only defined for local candidates. For remote candidates, this property is not applicable. - pub deleted: bool, -} - -impl Default for CandidateStats { - fn default() -> Self { - Self { - timestamp: Instant::now(), - id: String::new(), - network_type: NetworkType::default(), - ip: String::new(), - port: 0, - candidate_type: CandidateType::default(), - priority: 0, - url: String::new(), - relay_protocol: String::new(), - deleted: false, - } - } -} - -impl AgentInternal { - /// Returns a list of candidate pair stats. - pub(crate) async fn get_candidate_pairs_stats(&self) -> Vec { - let checklist = self.agent_conn.checklist.lock().await; - let mut res = Vec::with_capacity(checklist.len()); - for cp in &*checklist { - let stat = CandidatePairStats { - timestamp: Instant::now(), - local_candidate_id: cp.local.id(), - remote_candidate_id: cp.remote.id(), - state: cp.state.load(Ordering::SeqCst).into(), - nominated: cp.nominated.load(Ordering::SeqCst), - ..CandidatePairStats::default() - }; - res.push(stat); - } - res - } - - /// Returns a list of local candidates stats. - pub(crate) async fn get_local_candidates_stats(&self) -> Vec { - let local_candidates = self.local_candidates.lock().await; - let mut res = Vec::with_capacity(local_candidates.len()); - for (network_type, local_candidates) in &*local_candidates { - for c in local_candidates { - let stat = CandidateStats { - timestamp: Instant::now(), - id: c.id(), - network_type: *network_type, - ip: c.address(), - port: c.port(), - candidate_type: c.candidate_type(), - priority: c.priority(), - // URL string - relay_protocol: "udp".to_owned(), - // Deleted bool - ..CandidateStats::default() - }; - res.push(stat); - } - } - res - } - - /// Returns a list of remote candidates stats. - pub(crate) async fn get_remote_candidates_stats(&self) -> Vec { - let remote_candidates = self.remote_candidates.lock().await; - let mut res = Vec::with_capacity(remote_candidates.len()); - for (network_type, remote_candidates) in &*remote_candidates { - for c in remote_candidates { - let stat = CandidateStats { - timestamp: Instant::now(), - id: c.id(), - network_type: *network_type, - ip: c.address(), - port: c.port(), - candidate_type: c.candidate_type(), - priority: c.priority(), - // URL string - relay_protocol: "udp".to_owned(), - // Deleted bool - ..CandidateStats::default() - }; - res.push(stat); - } - } - res - } -} diff --git a/ice/src/agent/agent_test.rs b/ice/src/agent/agent_test.rs deleted file mode 100644 index 0af395a6f..000000000 --- a/ice/src/agent/agent_test.rs +++ /dev/null @@ -1,2203 +0,0 @@ -use std::net::Ipv4Addr; -use std::ops::Sub; -use std::str::FromStr; - -use async_trait::async_trait; -use stun::message::*; -use stun::textattrs::Username; -use util::vnet::*; -use util::Conn; -use waitgroup::{WaitGroup, Worker}; - -use super::agent_vnet_test::*; -use super::*; -use crate::agent::agent_transport_test::pipe; -use crate::candidate::candidate_base::*; -use crate::candidate::candidate_host::*; -use crate::candidate::candidate_peer_reflexive::*; -use crate::candidate::candidate_relay::*; -use crate::candidate::candidate_server_reflexive::*; -use crate::control::AttrControlling; -use crate::priority::PriorityAttr; -use crate::use_candidate::UseCandidateAttr; - -#[tokio::test] -async fn test_pair_search() -> Result<()> { - let config = AgentConfig::default(); - let a = Agent::new(config).await?; - - { - { - let checklist = a.internal.agent_conn.checklist.lock().await; - assert!( - checklist.is_empty(), - "TestPairSearch is only a valid test if a.validPairs is empty on construction" - ); - } - - let cp = a - .internal - .agent_conn - .get_best_available_candidate_pair() - .await; - assert!(cp.is_none(), "No Candidate pairs should exist"); - } - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_pair_priority() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.1.1".to_owned(), - port: 19216, - component: 1, - ..Default::default() - }, - ..Default::default() - }; - let host_local: Arc = Arc::new(host_config.new_candidate_host()?); - - let relay_config = CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.4".to_owned(), - port: 12340, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43210, - ..Default::default() - }; - - let relay_remote = relay_config.new_candidate_relay()?; - - let srflx_config = CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19218, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43212, - }; - - let srflx_remote = srflx_config.new_candidate_server_reflexive()?; - - let prflx_config = CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19217, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43211, - }; - - let prflx_remote = prflx_config.new_candidate_peer_reflexive()?; - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.5".to_owned(), - port: 12350, - component: 1, - ..Default::default() - }, - ..Default::default() - }; - let host_remote = host_config.new_candidate_host()?; - - let remotes: Vec> = vec![ - Arc::new(relay_remote), - Arc::new(srflx_remote), - Arc::new(prflx_remote), - Arc::new(host_remote), - ]; - - { - for remote in remotes { - if a.internal.find_pair(&host_local, &remote).await.is_none() { - a.internal - .add_pair(host_local.clone(), remote.clone()) - .await; - } - - if let Some(p) = a.internal.find_pair(&host_local, &remote).await { - p.state - .store(CandidatePairState::Succeeded as u8, Ordering::SeqCst); - } - - if let Some(best_pair) = a - .internal - .agent_conn - .get_best_available_candidate_pair() - .await - { - assert_eq!( - best_pair.to_string(), - CandidatePair { - remote: remote.clone(), - local: host_local.clone(), - ..Default::default() - } - .to_string(), - "Unexpected bestPair {best_pair} (expected remote: {remote})", - ); - } else { - panic!("expected Some, but got None"); - } - } - } - - a.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_agent_get_stats() -> Result<()> { - let (conn_a, conn_b, agent_a, agent_b) = pipe(None, None).await?; - assert_eq!(agent_a.get_bytes_received(), 0); - assert_eq!(agent_a.get_bytes_sent(), 0); - assert_eq!(agent_b.get_bytes_received(), 0); - assert_eq!(agent_b.get_bytes_sent(), 0); - - let _na = conn_a.send(&[0u8; 10]).await?; - let mut buf = vec![0u8; 10]; - let _nb = conn_b.recv(&mut buf).await?; - - assert_eq!(agent_a.get_bytes_received(), 0); - assert_eq!(agent_a.get_bytes_sent(), 10); - - assert_eq!(agent_b.get_bytes_received(), 10); - assert_eq!(agent_b.get_bytes_sent(), 0); - - Ok(()) -} - -#[tokio::test] -async fn test_on_selected_candidate_pair_change() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - let (callback_called_tx, mut callback_called_rx) = mpsc::channel::<()>(1); - let callback_called_tx = Arc::new(Mutex::new(Some(callback_called_tx))); - let cb: OnSelectedCandidatePairChangeHdlrFn = Box::new(move |_, _| { - let callback_called_tx_clone = Arc::clone(&callback_called_tx); - Box::pin(async move { - let mut tx = callback_called_tx_clone.lock().await; - tx.take(); - }) - }); - a.on_selected_candidate_pair_change(cb); - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.1.1".to_owned(), - port: 19216, - component: 1, - ..Default::default() - }, - ..Default::default() - }; - let host_local = host_config.new_candidate_host()?; - - let relay_config = CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.4".to_owned(), - port: 12340, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43210, - ..Default::default() - }; - let relay_remote = relay_config.new_candidate_relay()?; - - // select the pair - let p = Arc::new(CandidatePair::new( - Arc::new(host_local), - Arc::new(relay_remote), - false, - )); - a.internal.set_selected_pair(Some(p)).await; - - // ensure that the callback fired on setting the pair - let _ = callback_called_rx.recv().await; - - a.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.0.2".to_owned(), - port: 777, - component: 1, - conn: Some(Arc::new(MockConn {})), - ..Default::default() - }, - ..Default::default() - }; - - let local: Arc = Arc::new(host_config.new_candidate_host()?); - let remote = SocketAddr::from_str("172.17.0.3:999")?; - - let (username, local_pwd, tie_breaker) = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ( - ufrag_pwd.local_ufrag.to_owned() + ":" + ufrag_pwd.remote_ufrag.as_str(), - ufrag_pwd.local_pwd.clone(), - a.internal.tie_breaker.load(Ordering::SeqCst), - ) - }; - - let mut msg = Message::new(); - msg.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(UseCandidateAttr::new()), - Box::new(AttrControlling(tie_breaker)), - Box::new(PriorityAttr(local.priority())), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), - Box::new(FINGERPRINT), - ])?; - - { - a.internal.handle_inbound(&mut msg, &local, remote).await; - - let remote_candidates = a.internal.remote_candidates.lock().await; - // length of remote candidate list must be one now - assert_eq!( - remote_candidates.len(), - 1, - "failed to add a network type to the remote candidate list" - ); - - // length of remote candidate list for a network type must be 1 - if let Some(cands) = remote_candidates.get(&local.network_type()) { - assert_eq!( - cands.len(), - 1, - "failed to add prflx candidate to remote candidate list" - ); - - let c = &cands[0]; - - assert_eq!( - c.candidate_type(), - CandidateType::PeerReflexive, - "candidate type must be prflx" - ); - - assert_eq!(c.address(), "172.17.0.3", "IP address mismatch"); - - assert_eq!(c.port(), 999, "Port number mismatch"); - } else { - panic!( - "expected non-empty remote candidate for network type {}", - local.network_type() - ); - } - } - - a.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let mut tid = TransactionId::default(); - tid.0[..3].copy_from_slice("ABC".as_bytes()); - - let remote_pwd = { - { - let mut pending_binding_requests = a.internal.pending_binding_requests.lock().await; - *pending_binding_requests = vec![BindingRequest { - timestamp: Instant::now(), - transaction_id: tid, - destination: SocketAddr::from_str("0.0.0.0:0")?, - is_use_candidate: false, - }]; - } - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ufrag_pwd.remote_pwd.clone() - }; - - let host_config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.0.2".to_owned(), - port: 777, - component: 1, - conn: Some(Arc::new(MockConn {})), - ..Default::default() - }, - ..Default::default() - }; - - let local: Arc = Arc::new(host_config.new_candidate_host()?); - let remote = SocketAddr::from_str("172.17.0.3:999")?; - - let mut msg = Message::new(); - msg.build(&[ - Box::new(BINDING_SUCCESS), - Box::new(tid), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), - Box::new(FINGERPRINT), - ])?; - - { - a.internal.handle_inbound(&mut msg, &local, remote).await; - - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_eq!( - remote_candidates.len(), - 0, - "unknown remote was able to create a candidate" - ); - } - - a.close().await?; - Ok(()) -} - -//use std::io::Write; - -// Assert that Agent on startup sends message, and doesn't wait for connectivityTicker to fire -#[tokio::test] -async fn test_connectivity_on_startup() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - // Create a network with two interfaces - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.1".to_owned()], - ..Default::default() - }))); - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.2".to_owned()], - ..Default::default() - }))); - - connect_net2router(&net0, &wan).await?; - connect_net2router(&net1, &wan).await?; - start_router(&wan).await?; - - let (a_notifier, mut a_connected) = on_connected(); - let (b_notifier, mut b_connected) = on_connected(); - - let keepalive_interval = Some(Duration::from_secs(3600)); //time.Hour - let check_interval = Duration::from_secs(3600); //time.Hour - let cfg0 = AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(net0), - - keepalive_interval, - check_interval, - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(net1), - - keepalive_interval, - check_interval, - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - b_agent.on_connection_state_change(b_notifier); - - // Manual signaling - let (a_ufrag, a_pwd) = a_agent.get_local_user_credentials().await; - let (b_ufrag, b_pwd) = b_agent.get_local_user_credentials().await; - - gather_and_exchange_candidates(&a_agent, &b_agent).await?; - - let (accepted_tx, mut accepted_rx) = mpsc::channel::<()>(1); - let (accepting_tx, mut accepting_rx) = mpsc::channel::<()>(1); - let (_a_cancel_tx, a_cancel_rx) = mpsc::channel(1); - let (_b_cancel_tx, b_cancel_rx) = mpsc::channel(1); - - let accepting_tx = Arc::new(Mutex::new(Some(accepting_tx))); - a_agent.on_connection_state_change(Box::new(move |s: ConnectionState| { - let accepted_tx_clone = Arc::clone(&accepting_tx); - Box::pin(async move { - if s == ConnectionState::Checking { - let mut tx = accepted_tx_clone.lock().await; - tx.take(); - } - }) - })); - - tokio::spawn(async move { - let result = a_agent.accept(a_cancel_rx, b_ufrag, b_pwd).await; - assert!(result.is_ok(), "agent accept expected OK"); - drop(accepted_tx); - }); - - let _ = accepting_rx.recv().await; - - let _ = b_agent.dial(b_cancel_rx, a_ufrag, a_pwd).await?; - - // Ensure accepted - let _ = accepted_rx.recv().await; - - // Ensure pair selected - // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - { - let mut w = wan.lock().await; - w.stop().await?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_lite() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let stun_server_url = Url { - scheme: SchemeType::Stun, - host: "1.2.3.4".to_owned(), - port: 3478, - proto: ProtoType::Udp, - ..Default::default() - }; - - let nat_type = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointIndependent, - filtering_behavior: nat::EndpointDependencyType::EndpointIndependent, - ..Default::default() - }; - - let v = build_vnet(nat_type, nat_type).await?; - - let (a_notifier, mut a_connected) = on_connected(); - let (b_notifier, mut b_connected) = on_connected(); - - let cfg0 = AgentConfig { - urls: vec![stun_server_url], - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&v.net0)), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - urls: vec![], - lite: true, - candidate_types: vec![CandidateType::Host], - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&v.net1)), - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - b_agent.on_connection_state_change(b_notifier); - - let _ = connect_with_vnet(&a_agent, &b_agent).await?; - - // Ensure pair selected - // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - v.close().await?; - - Ok(()) -} - -struct MockPacketConn; - -#[async_trait] -impl Conn for MockPacketConn { - async fn connect(&self, _addr: SocketAddr) -> std::result::Result<(), util::Error> { - Ok(()) - } - - async fn recv(&self, _buf: &mut [u8]) -> std::result::Result { - Ok(0) - } - - async fn recv_from( - &self, - _buf: &mut [u8], - ) -> std::result::Result<(usize, SocketAddr), util::Error> { - Ok((0, SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0))) - } - - async fn send(&self, _buf: &[u8]) -> std::result::Result { - Ok(0) - } - - async fn send_to( - &self, - _buf: &[u8], - _target: SocketAddr, - ) -> std::result::Result { - Ok(0) - } - - fn local_addr(&self) -> std::result::Result { - Ok(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0)) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> std::result::Result<(), util::Error> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -fn build_msg(c: MessageClass, username: String, key: String) -> Result { - let mut msg = Message::new(); - msg.build(&[ - Box::new(MessageType::new(METHOD_BINDING, c)), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(key)), - Box::new(FINGERPRINT), - ])?; - Ok(msg) -} - -#[tokio::test] -async fn test_inbound_validity() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let remote = SocketAddr::from_str("172.17.0.3:999")?; - let local: Arc = Arc::new( - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.0.2".to_owned(), - port: 777, - component: 1, - conn: Some(Arc::new(MockPacketConn {})), - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host()?, - ); - - //"Invalid Binding requests should be discarded" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let local_pwd = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ufrag_pwd.local_pwd.clone() - }; - a.internal - .handle_inbound( - &mut build_msg(CLASS_REQUEST, "invalid".to_owned(), local_pwd)?, - &local, - remote, - ) - .await; - { - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_ne!( - remote_candidates.len(), - 1, - "Binding with invalid Username was able to create prflx candidate" - ); - } - - let username = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag) - }; - a.internal - .handle_inbound( - &mut build_msg(CLASS_REQUEST, username, "Invalid".to_owned())?, - &local, - remote, - ) - .await; - { - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_ne!( - remote_candidates.len(), - 1, - "Binding with invalid MessageIntegrity was able to create prflx candidate" - ); - } - } - - a.close().await?; - } - - //"Invalid Binding success responses should be discarded" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let username = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag) - }; - a.internal - .handle_inbound( - &mut build_msg(CLASS_SUCCESS_RESPONSE, username, "Invalid".to_owned())?, - &local, - remote, - ) - .await; - { - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_ne!( - remote_candidates.len(), - 1, - "Binding with invalid Username was able to create prflx candidate" - ); - } - } - - a.close().await?; - } - - //"Discard non-binding messages" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let username = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag) - }; - a.internal - .handle_inbound( - &mut build_msg(CLASS_ERROR_RESPONSE, username, "Invalid".to_owned())?, - &local, - remote, - ) - .await; - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_ne!( - remote_candidates.len(), - 1, - "non-binding message was able to create prflxRemote" - ); - } - - a.close().await?; - } - - //"Valid bind request" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let (username, local_pwd) = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ( - format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag), - ufrag_pwd.local_pwd.clone(), - ) - }; - a.internal - .handle_inbound( - &mut build_msg(CLASS_REQUEST, username, local_pwd)?, - &local, - remote, - ) - .await; - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_eq!( - remote_candidates.len(), - 1, - "Binding with valid values was unable to create prflx candidate" - ); - } - - a.close().await?; - } - - //"Valid bind without fingerprint" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let (username, local_pwd) = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ( - format!("{}:{}", ufrag_pwd.local_ufrag, ufrag_pwd.remote_ufrag), - ufrag_pwd.local_pwd.clone(), - ) - }; - - let mut msg = Message::new(); - msg.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId::new()), - Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), - ])?; - - a.internal.handle_inbound(&mut msg, &local, remote).await; - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_eq!( - remote_candidates.len(), - 1, - "Binding with valid values (but no fingerprint) was unable to create prflx candidate" - ); - } - - a.close().await?; - } - - //"Success with invalid TransactionID" - { - let a = Agent::new(AgentConfig::default()).await?; - - { - let remote = SocketAddr::from_str("172.17.0.3:999")?; - - let mut t_id = TransactionId::default(); - t_id.0[..3].copy_from_slice(b"ABC"); - - let remote_pwd = { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - ufrag_pwd.remote_pwd.clone() - }; - - let mut msg = Message::new(); - msg.build(&[ - Box::new(BINDING_SUCCESS), - Box::new(t_id), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), - Box::new(FINGERPRINT), - ])?; - - a.internal.handle_inbound(&mut msg, &local, remote).await; - - { - let remote_candidates = a.internal.remote_candidates.lock().await; - assert_eq!( - remote_candidates.len(), - 0, - "unknown remote was able to create a candidate" - ); - } - } - - a.close().await?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_invalid_agent_starts() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let (_cancel_tx1, cancel_rx1) = mpsc::channel(1); - let result = a.dial(cancel_rx1, "".to_owned(), "bar".to_owned()).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrRemoteUfragEmpty, err); - } - - let (_cancel_tx2, cancel_rx2) = mpsc::channel(1); - let result = a.dial(cancel_rx2, "foo".to_owned(), "".to_owned()).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrRemotePwdEmpty, err); - } - - let (cancel_tx3, cancel_rx3) = mpsc::channel(1); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; - drop(cancel_tx3); - }); - - let result = a.dial(cancel_rx3, "foo".to_owned(), "bar".to_owned()).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrCanceledByCaller, err); - } - - let (_cancel_tx4, cancel_rx4) = mpsc::channel(1); - let result = a.dial(cancel_rx4, "foo".to_owned(), "bar".to_owned()).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrMultipleStart, err); - } - - a.close().await?; - - Ok(()) -} - -//use std::io::Write; - -// Assert that Agent emits Connecting/Connected/Disconnected/Failed/Closed messages -#[tokio::test] -async fn test_connection_state_callback() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let disconnected_duration = Duration::from_secs(1); - let failed_duration = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(disconnected_duration), - failed_timeout: Some(failed_duration), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - - let cfg1 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(disconnected_duration), - failed_timeout: Some(failed_duration), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let (is_checking_tx, mut is_checking_rx) = mpsc::channel::<()>(1); - let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); - let (is_disconnected_tx, mut is_disconnected_rx) = mpsc::channel::<()>(1); - let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1); - let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1); - - let is_checking_tx = Arc::new(Mutex::new(Some(is_checking_tx))); - let is_connected_tx = Arc::new(Mutex::new(Some(is_connected_tx))); - let is_disconnected_tx = Arc::new(Mutex::new(Some(is_disconnected_tx))); - let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx))); - let is_closed_tx = Arc::new(Mutex::new(Some(is_closed_tx))); - - a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let is_checking_tx_clone = Arc::clone(&is_checking_tx); - let is_connected_tx_clone = Arc::clone(&is_connected_tx); - let is_disconnected_tx_clone = Arc::clone(&is_disconnected_tx); - let is_failed_tx_clone = Arc::clone(&is_failed_tx); - let is_closed_tx_clone = Arc::clone(&is_closed_tx); - Box::pin(async move { - match c { - ConnectionState::Checking => { - log::debug!("drop is_checking_tx"); - let mut tx = is_checking_tx_clone.lock().await; - tx.take(); - } - ConnectionState::Connected => { - log::debug!("drop is_connected_tx"); - let mut tx = is_connected_tx_clone.lock().await; - tx.take(); - } - ConnectionState::Disconnected => { - log::debug!("drop is_disconnected_tx"); - let mut tx = is_disconnected_tx_clone.lock().await; - tx.take(); - } - ConnectionState::Failed => { - log::debug!("drop is_failed_tx"); - let mut tx = is_failed_tx_clone.lock().await; - tx.take(); - } - ConnectionState::Closed => { - log::debug!("drop is_closed_tx"); - let mut tx = is_closed_tx_clone.lock().await; - tx.take(); - } - _ => {} - }; - }) - })); - - connect_with_vnet(&a_agent, &b_agent).await?; - - log::debug!("wait is_checking_tx"); - let _ = is_checking_rx.recv().await; - log::debug!("wait is_connected_rx"); - let _ = is_connected_rx.recv().await; - log::debug!("wait is_disconnected_rx"); - let _ = is_disconnected_rx.recv().await; - log::debug!("wait is_failed_rx"); - let _ = is_failed_rx.recv().await; - - a_agent.close().await?; - b_agent.close().await?; - - log::debug!("wait is_closed_rx"); - let _ = is_closed_rx.recv().await; - - Ok(()) -} - -#[tokio::test] -async fn test_invalid_gather() -> Result<()> { - //"Gather with no OnCandidate should error" - let a = Agent::new(AgentConfig::default()).await?; - - if let Err(err) = a.gather_candidates() { - assert_eq!( - Error::ErrNoOnCandidateHandler, - err, - "trickle GatherCandidates succeeded without OnCandidate" - ); - } - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_candidate_pair_stats() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let host_local: Arc = Arc::new( - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.1.1".to_owned(), - port: 19216, - component: 1, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host()?, - ); - - let relay_remote: Arc = Arc::new( - CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.4".to_owned(), - port: 2340, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43210, - ..Default::default() - } - .new_candidate_relay()?, - ); - - let srflx_remote: Arc = Arc::new( - CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19218, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43212, - } - .new_candidate_server_reflexive()?, - ); - - let prflx_remote: Arc = Arc::new( - CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19217, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43211, - } - .new_candidate_peer_reflexive()?, - ); - - let host_remote: Arc = Arc::new( - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.5".to_owned(), - port: 12350, - component: 1, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host()?, - ); - - for remote in &[ - Arc::clone(&relay_remote), - Arc::clone(&srflx_remote), - Arc::clone(&prflx_remote), - Arc::clone(&host_remote), - ] { - let p = a.internal.find_pair(&host_local, remote).await; - - if p.is_none() { - a.internal - .add_pair(Arc::clone(&host_local), Arc::clone(remote)) - .await; - } - } - - { - if let Some(p) = a.internal.find_pair(&host_local, &prflx_remote).await { - p.state - .store(CandidatePairState::Failed as u8, Ordering::SeqCst); - } - } - - let stats = a.get_candidate_pairs_stats().await; - assert_eq!(stats.len(), 4, "expected 4 candidate pairs stats"); - - let (mut relay_pair_stat, mut srflx_pair_stat, mut prflx_pair_stat, mut host_pair_stat) = ( - CandidatePairStats::default(), - CandidatePairStats::default(), - CandidatePairStats::default(), - CandidatePairStats::default(), - ); - - for cps in stats { - assert_eq!( - cps.local_candidate_id, - host_local.id(), - "invalid local candidate id" - ); - - if cps.remote_candidate_id == relay_remote.id() { - relay_pair_stat = cps; - } else if cps.remote_candidate_id == srflx_remote.id() { - srflx_pair_stat = cps; - } else if cps.remote_candidate_id == prflx_remote.id() { - prflx_pair_stat = cps; - } else if cps.remote_candidate_id == host_remote.id() { - host_pair_stat = cps; - } else { - panic!("invalid remote candidate ID"); - } - } - - assert_eq!( - relay_pair_stat.remote_candidate_id, - relay_remote.id(), - "missing host-relay pair stat" - ); - assert_eq!( - srflx_pair_stat.remote_candidate_id, - srflx_remote.id(), - "missing host-srflx pair stat" - ); - assert_eq!( - prflx_pair_stat.remote_candidate_id, - prflx_remote.id(), - "missing host-prflx pair stat" - ); - assert_eq!( - host_pair_stat.remote_candidate_id, - host_remote.id(), - "missing host-host pair stat" - ); - assert_eq!( - prflx_pair_stat.state, - CandidatePairState::Failed, - "expected host-prfflx pair to have state failed, it has state {} instead", - prflx_pair_stat.state - ); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_local_candidate_stats() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let host_local: Arc = Arc::new( - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.1.1".to_owned(), - port: 19216, - component: 1, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host()?, - ); - - let srflx_local: Arc = Arc::new( - CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "192.168.1.1".to_owned(), - port: 19217, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43212, - } - .new_candidate_server_reflexive()?, - ); - - { - let mut local_candidates = a.internal.local_candidates.lock().await; - local_candidates.insert( - NetworkType::Udp4, - vec![Arc::clone(&host_local), Arc::clone(&srflx_local)], - ); - } - - let local_stats = a.get_local_candidates_stats().await; - assert_eq!( - local_stats.len(), - 2, - "expected 2 local candidates stats, got {} instead", - local_stats.len() - ); - - let (mut host_local_stat, mut srflx_local_stat) = - (CandidateStats::default(), CandidateStats::default()); - for stats in local_stats { - let candidate = if stats.id == host_local.id() { - host_local_stat = stats.clone(); - Arc::clone(&host_local) - } else if stats.id == srflx_local.id() { - srflx_local_stat = stats.clone(); - Arc::clone(&srflx_local) - } else { - panic!("invalid local candidate ID"); - }; - - assert_eq!( - stats.candidate_type, - candidate.candidate_type(), - "invalid stats CandidateType" - ); - assert_eq!( - stats.priority, - candidate.priority(), - "invalid stats CandidateType" - ); - assert_eq!(stats.ip, candidate.address(), "invalid stats IP"); - } - - assert_eq!( - host_local_stat.id, - host_local.id(), - "missing host local stat" - ); - assert_eq!( - srflx_local_stat.id, - srflx_local.id(), - "missing srflx local stat" - ); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_remote_candidate_stats() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let relay_remote: Arc = Arc::new( - CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.4".to_owned(), - port: 12340, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43210, - ..Default::default() - } - .new_candidate_relay()?, - ); - - let srflx_remote: Arc = Arc::new( - CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19218, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43212, - } - .new_candidate_server_reflexive()?, - ); - - let prflx_remote: Arc = Arc::new( - CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "10.10.10.2".to_owned(), - port: 19217, - component: 1, - ..Default::default() - }, - rel_addr: "4.3.2.1".to_owned(), - rel_port: 43211, - } - .new_candidate_peer_reflexive()?, - ); - - let host_remote: Arc = Arc::new( - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "1.2.3.5".to_owned(), - port: 12350, - component: 1, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host()?, - ); - - { - let mut remote_candidates = a.internal.remote_candidates.lock().await; - remote_candidates.insert( - NetworkType::Udp4, - vec![ - Arc::clone(&relay_remote), - Arc::clone(&srflx_remote), - Arc::clone(&prflx_remote), - Arc::clone(&host_remote), - ], - ); - } - - let remote_stats = a.get_remote_candidates_stats().await; - assert_eq!( - remote_stats.len(), - 4, - "expected 4 remote candidates stats, got {} instead", - remote_stats.len() - ); - - let (mut relay_remote_stat, mut srflx_remote_stat, mut prflx_remote_stat, mut host_remote_stat) = ( - CandidateStats::default(), - CandidateStats::default(), - CandidateStats::default(), - CandidateStats::default(), - ); - for stats in remote_stats { - let candidate = if stats.id == relay_remote.id() { - relay_remote_stat = stats.clone(); - Arc::clone(&relay_remote) - } else if stats.id == srflx_remote.id() { - srflx_remote_stat = stats.clone(); - Arc::clone(&srflx_remote) - } else if stats.id == prflx_remote.id() { - prflx_remote_stat = stats.clone(); - Arc::clone(&prflx_remote) - } else if stats.id == host_remote.id() { - host_remote_stat = stats.clone(); - Arc::clone(&host_remote) - } else { - panic!("invalid remote candidate ID"); - }; - - assert_eq!( - stats.candidate_type, - candidate.candidate_type(), - "invalid stats CandidateType" - ); - assert_eq!( - stats.priority, - candidate.priority(), - "invalid stats CandidateType" - ); - assert_eq!(stats.ip, candidate.address(), "invalid stats IP"); - } - - assert_eq!( - relay_remote_stat.id, - relay_remote.id(), - "missing relay remote stat" - ); - assert_eq!( - srflx_remote_stat.id, - srflx_remote.id(), - "missing srflx remote stat" - ); - assert_eq!( - prflx_remote_stat.id, - prflx_remote.id(), - "missing prflx remote stat" - ); - assert_eq!( - host_remote_stat.id, - host_remote.id(), - "missing host remote stat" - ); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_init_ext_ip_mapping() -> Result<()> { - // a.extIPMapper should be nil by default - let a = Agent::new(AgentConfig::default()).await?; - assert!( - a.ext_ip_mapper.is_none(), - "a.extIPMapper should be none by default" - ); - a.close().await?; - - // a.extIPMapper should be nil when NAT1To1IPs is a non-nil empty array - let a = Agent::new(AgentConfig { - nat_1to1_ips: vec![], - nat_1to1_ip_candidate_type: CandidateType::Host, - ..Default::default() - }) - .await?; - assert!( - a.ext_ip_mapper.is_none(), - "a.extIPMapper should be none by default" - ); - a.close().await?; - - // NewAgent should return an error when 1:1 NAT for host candidate is enabled - // but the candidate type does not appear in the CandidateTypes. - if let Err(err) = Agent::new(AgentConfig { - nat_1to1_ips: vec!["1.2.3.4".to_owned()], - nat_1to1_ip_candidate_type: CandidateType::Host, - candidate_types: vec![CandidateType::Relay], - ..Default::default() - }) - .await - { - assert_eq!( - Error::ErrIneffectiveNat1to1IpMappingHost, - err, - "Unexpected error: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - // NewAgent should return an error when 1:1 NAT for srflx candidate is enabled - // but the candidate type does not appear in the CandidateTypes. - if let Err(err) = Agent::new(AgentConfig { - nat_1to1_ips: vec!["1.2.3.4".to_owned()], - nat_1to1_ip_candidate_type: CandidateType::ServerReflexive, - candidate_types: vec![CandidateType::Relay], - ..Default::default() - }) - .await - { - assert_eq!( - Error::ErrIneffectiveNat1to1IpMappingSrflx, - err, - "Unexpected error: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - // NewAgent should return an error when 1:1 NAT for host candidate is enabled - // along with mDNS with MulticastDNSModeQueryAndGather - if let Err(err) = Agent::new(AgentConfig { - nat_1to1_ips: vec!["1.2.3.4".to_owned()], - nat_1to1_ip_candidate_type: CandidateType::Host, - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - ..Default::default() - }) - .await - { - assert_eq!( - Error::ErrMulticastDnsWithNat1to1IpMapping, - err, - "Unexpected error: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - // NewAgent should return if newExternalIPMapper() returns an error. - if let Err(err) = Agent::new(AgentConfig { - nat_1to1_ips: vec!["bad.2.3.4".to_owned()], // bad IP - nat_1to1_ip_candidate_type: CandidateType::Host, - ..Default::default() - }) - .await - { - assert_eq!( - Error::ErrInvalidNat1to1IpMapping, - err, - "Unexpected error: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_binding_request_timeout() -> Result<()> { - const EXPECTED_REMOVAL_COUNT: usize = 2; - - let a = Agent::new(AgentConfig::default()).await?; - - let now = Instant::now(); - { - { - let mut pending_binding_requests = a.internal.pending_binding_requests.lock().await; - pending_binding_requests.push(BindingRequest { - timestamp: now, // valid - ..Default::default() - }); - pending_binding_requests.push(BindingRequest { - timestamp: now.sub(Duration::from_millis(3900)), // valid - ..Default::default() - }); - pending_binding_requests.push(BindingRequest { - timestamp: now.sub(Duration::from_millis(4100)), // invalid - ..Default::default() - }); - pending_binding_requests.push(BindingRequest { - timestamp: now.sub(Duration::from_secs(75)), // invalid - ..Default::default() - }); - } - - a.internal.invalidate_pending_binding_requests(now).await; - { - let pending_binding_requests = a.internal.pending_binding_requests.lock().await; - assert_eq!(pending_binding_requests.len(), EXPECTED_REMOVAL_COUNT, "Binding invalidation due to timeout did not remove the correct number of binding requests") - } - } - - a.close().await?; - - Ok(()) -} - -// test_agent_credentials checks if local username fragments and passwords (if set) meet RFC standard -// and ensure it's backwards compatible with previous versions of the pion/ice -#[tokio::test] -async fn test_agent_credentials() -> Result<()> { - // Agent should not require any of the usernames and password to be set - // If set, they should follow the default 16/128 bits random number generator strategy - - let a = Agent::new(AgentConfig::default()).await?; - { - let ufrag_pwd = a.internal.ufrag_pwd.lock().await; - assert!(ufrag_pwd.local_ufrag.as_bytes().len() * 8 >= 24); - assert!(ufrag_pwd.local_pwd.as_bytes().len() * 8 >= 128); - } - a.close().await?; - - // Should honor RFC standards - // Local values MUST be unguessable, with at least 128 bits of - // random number generator output used to generate the password, and - // at least 24 bits of output to generate the username fragment. - - if let Err(err) = Agent::new(AgentConfig { - local_ufrag: "xx".to_owned(), - ..Default::default() - }) - .await - { - assert_eq!(Error::ErrLocalUfragInsufficientBits, err); - } else { - panic!("expected error, but got ok"); - } - - if let Err(err) = Agent::new(AgentConfig { - local_pwd: "xxxxxx".to_owned(), - ..Default::default() - }) - .await - { - assert_eq!(Error::ErrLocalPwdInsufficientBits, err); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -// Assert that Agent on Failure deletes all existing candidates -// User can then do an ICE Restart to bring agent back -#[tokio::test] -async fn test_connection_state_failed_delete_all_candidates() -> Result<()> { - let one_second = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - let cfg1 = AgentConfig { - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1); - let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx))); - a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let is_failed_tx_clone = Arc::clone(&is_failed_tx); - Box::pin(async move { - if c == ConnectionState::Failed { - let mut tx = is_failed_tx_clone.lock().await; - tx.take(); - } - }) - })); - - connect_with_vnet(&a_agent, &b_agent).await?; - let _ = is_failed_rx.recv().await; - - { - { - let remote_candidates = a_agent.internal.remote_candidates.lock().await; - assert_eq!(remote_candidates.len(), 0); - } - { - let local_candidates = a_agent.internal.local_candidates.lock().await; - assert_eq!(local_candidates.len(), 0); - } - } - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -// Assert that the ICE Agent can go directly from Connecting -> Failed on both sides -#[tokio::test] -async fn test_connection_state_connecting_to_failed() -> Result<()> { - let one_second = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - let cfg1 = AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let is_failed = WaitGroup::new(); - let is_checking = WaitGroup::new(); - - let connection_state_check = move |wf: Worker, wc: Worker| { - let wf = Arc::new(Mutex::new(Some(wf))); - let wc = Arc::new(Mutex::new(Some(wc))); - let hdlr_fn: OnConnectionStateChangeHdlrFn = Box::new(move |c: ConnectionState| { - let wf_clone = Arc::clone(&wf); - let wc_clone = Arc::clone(&wc); - Box::pin(async move { - if c == ConnectionState::Failed { - let mut f = wf_clone.lock().await; - f.take(); - } else if c == ConnectionState::Checking { - let mut c = wc_clone.lock().await; - c.take(); - } else if c == ConnectionState::Connected || c == ConnectionState::Completed { - panic!("Unexpected ConnectionState: {c}"); - } - }) - }); - hdlr_fn - }; - - let (wf1, wc1) = (is_failed.worker(), is_checking.worker()); - a_agent.on_connection_state_change(connection_state_check(wf1, wc1)); - - let (wf2, wc2) = (is_failed.worker(), is_checking.worker()); - b_agent.on_connection_state_change(connection_state_check(wf2, wc2)); - - let agent_a = Arc::clone(&a_agent); - tokio::spawn(async move { - let (_cancel_tx, cancel_rx) = mpsc::channel(1); - let result = agent_a - .accept(cancel_rx, "InvalidFrag".to_owned(), "InvalidPwd".to_owned()) - .await; - assert!(result.is_err()); - }); - - let agent_b = Arc::clone(&b_agent); - tokio::spawn(async move { - let (_cancel_tx, cancel_rx) = mpsc::channel(1); - let result = agent_b - .dial(cancel_rx, "InvalidFrag".to_owned(), "InvalidPwd".to_owned()) - .await; - assert!(result.is_err()); - }); - - is_checking.wait().await; - is_failed.wait().await; - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_agent_restart_during_gather() -> Result<()> { - //"Restart During Gather" - - let agent = Agent::new(AgentConfig::default()).await?; - - agent - .gathering_state - .store(GatheringState::Gathering as u8, Ordering::SeqCst); - - if let Err(err) = agent.restart("".to_owned(), "".to_owned()).await { - assert_eq!(Error::ErrRestartWhenGathering, err); - } else { - panic!("expected error, but got ok"); - } - - agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_agent_restart_when_closed() -> Result<()> { - //"Restart When Closed" - - let agent = Agent::new(AgentConfig::default()).await?; - agent.close().await?; - - if let Err(err) = agent.restart("".to_owned(), "".to_owned()).await { - assert_eq!(Error::ErrClosed, err); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_agent_restart_one_side() -> Result<()> { - let one_second = Duration::from_secs(1); - - //"Restart One Side" - let (_, _, agent_a, agent_b) = pipe( - Some(AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - ..Default::default() - }), - Some(AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - ..Default::default() - }), - ) - .await?; - - let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1); - let cancel_tx = Arc::new(Mutex::new(Some(cancel_tx))); - agent_b.on_connection_state_change(Box::new(move |c: ConnectionState| { - let cancel_tx_clone = Arc::clone(&cancel_tx); - Box::pin(async move { - if c == ConnectionState::Failed || c == ConnectionState::Disconnected { - let mut tx = cancel_tx_clone.lock().await; - tx.take(); - } - }) - })); - - agent_a.restart("".to_owned(), "".to_owned()).await?; - - let _ = cancel_rx.recv().await; - - agent_a.close().await?; - agent_b.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_agent_restart_both_side() -> Result<()> { - let one_second = Duration::from_secs(1); - //"Restart Both Sides" - - // Get all addresses of candidates concatenated - let generate_candidate_address_strings = - |res: Result>>| -> String { - assert!(res.is_ok()); - - let mut out = String::new(); - if let Ok(candidates) = res { - for c in candidates { - out += c.address().as_str(); - out += ":"; - out += c.port().to_string().as_str(); - } - } - out - }; - - // Store the original candidates, confirm that after we reconnect we have new pairs - let (_, _, agent_a, agent_b) = pipe( - Some(AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - ..Default::default() - }), - Some(AgentConfig { - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - ..Default::default() - }), - ) - .await?; - - let conn_afirst_candidates = - generate_candidate_address_strings(agent_a.get_local_candidates().await); - let conn_bfirst_candidates = - generate_candidate_address_strings(agent_b.get_local_candidates().await); - - let (a_notifier, mut a_connected) = on_connected(); - agent_a.on_connection_state_change(a_notifier); - - let (b_notifier, mut b_connected) = on_connected(); - agent_b.on_connection_state_change(b_notifier); - - // Restart and Re-Signal - agent_a.restart("".to_owned(), "".to_owned()).await?; - agent_b.restart("".to_owned(), "".to_owned()).await?; - - // Exchange Candidates and Credentials - let (ufrag, pwd) = agent_b.get_local_user_credentials().await; - agent_a.set_remote_credentials(ufrag, pwd).await?; - - let (ufrag, pwd) = agent_a.get_local_user_credentials().await; - agent_b.set_remote_credentials(ufrag, pwd).await?; - - gather_and_exchange_candidates(&agent_a, &agent_b).await?; - - // Wait until both have gone back to connected - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - // Assert that we have new candidates each time - assert_ne!( - conn_afirst_candidates, - generate_candidate_address_strings(agent_a.get_local_candidates().await) - ); - assert_ne!( - conn_bfirst_candidates, - generate_candidate_address_strings(agent_b.get_local_candidates().await) - ); - - agent_a.close().await?; - agent_b.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_get_remote_credentials() -> Result<()> { - let a = Agent::new(AgentConfig::default()).await?; - - let (remote_ufrag, remote_pwd) = { - let mut ufrag_pwd = a.internal.ufrag_pwd.lock().await; - "remoteUfrag".clone_into(&mut ufrag_pwd.remote_ufrag); - "remotePwd".clone_into(&mut ufrag_pwd.remote_pwd); - ( - ufrag_pwd.remote_ufrag.to_owned(), - ufrag_pwd.remote_pwd.to_owned(), - ) - }; - - let (actual_ufrag, actual_pwd) = a.get_remote_user_credentials().await; - - assert_eq!(actual_ufrag, remote_ufrag); - assert_eq!(actual_pwd, remote_pwd); - - a.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_close_in_connection_state_callback() -> Result<()> { - let disconnected_duration = Duration::from_secs(1); - let failed_duration = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(disconnected_duration), - failed_timeout: Some(failed_duration), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(500), - ..Default::default() - }; - - let cfg1 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(disconnected_duration), - failed_timeout: Some(failed_duration), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(500), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1); - let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); - let is_closed_tx = Arc::new(Mutex::new(Some(is_closed_tx))); - let is_connected_tx = Arc::new(Mutex::new(Some(is_connected_tx))); - a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let is_closed_tx_clone = Arc::clone(&is_closed_tx); - let is_connected_tx_clone = Arc::clone(&is_connected_tx); - Box::pin(async move { - if c == ConnectionState::Connected { - let mut tx = is_connected_tx_clone.lock().await; - tx.take(); - } else if c == ConnectionState::Closed { - let mut tx = is_closed_tx_clone.lock().await; - tx.take(); - } - }) - })); - - connect_with_vnet(&a_agent, &b_agent).await?; - - let _ = is_connected_rx.recv().await; - a_agent.close().await?; - - let _ = is_closed_rx.recv().await; - b_agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_run_task_in_connection_state_callback() -> Result<()> { - let one_second = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(50), - ..Default::default() - }; - - let cfg1 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(50), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1); - let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx))); - a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let is_complete_tx_clone = Arc::clone(&is_complete_tx); - Box::pin(async move { - if c == ConnectionState::Connected { - let mut tx = is_complete_tx_clone.lock().await; - tx.take(); - } - }) - })); - - connect_with_vnet(&a_agent, &b_agent).await?; - - let _ = is_complete_rx.recv().await; - let _ = a_agent.get_local_user_credentials().await; - a_agent.restart("".to_owned(), "".to_owned()).await?; - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_run_task_in_selected_candidate_pair_change_callback() -> Result<()> { - let one_second = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let cfg0 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(50), - ..Default::default() - }; - - let cfg1 = AgentConfig { - urls: vec![], - network_types: supported_network_types(), - disconnected_timeout: Some(one_second), - failed_timeout: Some(one_second), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(50), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let b_agent = Arc::new(Agent::new(cfg1).await?); - - let (is_tested_tx, mut is_tested_rx) = mpsc::channel::<()>(1); - let is_tested_tx = Arc::new(Mutex::new(Some(is_tested_tx))); - a_agent.on_selected_candidate_pair_change(Box::new( - move |_: &Arc, _: &Arc| { - let is_tested_tx_clone = Arc::clone(&is_tested_tx); - Box::pin(async move { - let mut tx = is_tested_tx_clone.lock().await; - tx.take(); - }) - }, - )); - - let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1); - let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx))); - a_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let is_complete_tx_clone = Arc::clone(&is_complete_tx); - Box::pin(async move { - if c == ConnectionState::Connected { - let mut tx = is_complete_tx_clone.lock().await; - tx.take(); - } - }) - })); - - connect_with_vnet(&a_agent, &b_agent).await?; - - let _ = is_complete_rx.recv().await; - let _ = is_tested_rx.recv().await; - - let _ = a_agent.get_local_user_credentials().await; - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -// Assert that a Lite agent goes to disconnected and failed -#[tokio::test] -async fn test_lite_lifecycle() -> Result<()> { - let (a_notifier, mut a_connected_rx) = on_connected(); - - let a_agent = Arc::new( - Agent::new(AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - ..Default::default() - }) - .await?, - ); - - a_agent.on_connection_state_change(a_notifier); - - let disconnected_duration = Duration::from_secs(1); - let failed_duration = Duration::from_secs(1); - let keepalive_interval = Duration::from_secs(0); - - let b_agent = Arc::new( - Agent::new(AgentConfig { - lite: true, - candidate_types: vec![CandidateType::Host], - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - disconnected_timeout: Some(disconnected_duration), - failed_timeout: Some(failed_duration), - keepalive_interval: Some(keepalive_interval), - check_interval: Duration::from_millis(500), - ..Default::default() - }) - .await?, - ); - - let (b_connected_tx, mut b_connected_rx) = mpsc::channel::<()>(1); - let (b_disconnected_tx, mut b_disconnected_rx) = mpsc::channel::<()>(1); - let (b_failed_tx, mut b_failed_rx) = mpsc::channel::<()>(1); - let b_connected_tx = Arc::new(Mutex::new(Some(b_connected_tx))); - let b_disconnected_tx = Arc::new(Mutex::new(Some(b_disconnected_tx))); - let b_failed_tx = Arc::new(Mutex::new(Some(b_failed_tx))); - - b_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let b_connected_tx_clone = Arc::clone(&b_connected_tx); - let b_disconnected_tx_clone = Arc::clone(&b_disconnected_tx); - let b_failed_tx_clone = Arc::clone(&b_failed_tx); - - Box::pin(async move { - if c == ConnectionState::Connected { - let mut tx = b_connected_tx_clone.lock().await; - tx.take(); - } else if c == ConnectionState::Disconnected { - let mut tx = b_disconnected_tx_clone.lock().await; - tx.take(); - } else if c == ConnectionState::Failed { - let mut tx = b_failed_tx_clone.lock().await; - tx.take(); - } - }) - })); - - connect_with_vnet(&b_agent, &a_agent).await?; - - let _ = a_connected_rx.recv().await; - let _ = b_connected_rx.recv().await; - a_agent.close().await?; - - let _ = b_disconnected_rx.recv().await; - let _ = b_failed_rx.recv().await; - - b_agent.close().await?; - - Ok(()) -} diff --git a/ice/src/agent/agent_transport.rs b/ice/src/agent/agent_transport.rs deleted file mode 100644 index 6b213554d..000000000 --- a/ice/src/agent/agent_transport.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::io; -use std::sync::atomic::Ordering; - -use arc_swap::ArcSwapOption; -use async_trait::async_trait; -use portable_atomic::AtomicBool; -use util::Conn; - -use super::*; -use crate::error::*; - -impl Agent { - /// Connects to the remote agent, acting as the controlling ice agent. - /// The method blocks until at least one ice candidate pair has successfully connected. - /// - /// The operation will be cancelled if `cancel_rx` either receives a message or its channel - /// closes. - pub async fn dial( - &self, - mut cancel_rx: mpsc::Receiver<()>, - remote_ufrag: String, - remote_pwd: String, - ) -> Result> { - let (on_connected_rx, agent_conn) = { - self.internal - .start_connectivity_checks(true, remote_ufrag, remote_pwd) - .await?; - - let mut on_connected_rx = self.internal.on_connected_rx.lock().await; - ( - on_connected_rx.take(), - Arc::clone(&self.internal.agent_conn), - ) - }; - - if let Some(mut on_connected_rx) = on_connected_rx { - // block until pair selected - tokio::select! { - _ = on_connected_rx.recv() => {}, - _ = cancel_rx.recv() => { - return Err(Error::ErrCanceledByCaller); - } - } - } - Ok(agent_conn) - } - - /// Connects to the remote agent, acting as the controlled ice agent. - /// The method blocks until at least one ice candidate pair has successfully connected. - /// - /// The operation will be cancelled if `cancel_rx` either receives a message or its channel - /// closes. - pub async fn accept( - &self, - mut cancel_rx: mpsc::Receiver<()>, - remote_ufrag: String, - remote_pwd: String, - ) -> Result> { - let (on_connected_rx, agent_conn) = { - self.internal - .start_connectivity_checks(false, remote_ufrag, remote_pwd) - .await?; - - let mut on_connected_rx = self.internal.on_connected_rx.lock().await; - ( - on_connected_rx.take(), - Arc::clone(&self.internal.agent_conn), - ) - }; - - if let Some(mut on_connected_rx) = on_connected_rx { - // block until pair selected - tokio::select! { - _ = on_connected_rx.recv() => {}, - _ = cancel_rx.recv() => { - return Err(Error::ErrCanceledByCaller); - } - } - } - - Ok(agent_conn) - } -} - -pub(crate) struct AgentConn { - pub(crate) selected_pair: ArcSwapOption, - pub(crate) checklist: Mutex>>, - - pub(crate) buffer: Buffer, - pub(crate) bytes_received: AtomicUsize, - pub(crate) bytes_sent: AtomicUsize, - pub(crate) done: AtomicBool, -} - -impl AgentConn { - pub(crate) fn new() -> Self { - Self { - selected_pair: ArcSwapOption::empty(), - checklist: Mutex::new(vec![]), - // Make sure the buffer doesn't grow indefinitely. - // NOTE: We actually won't get anywhere close to this limit. - // SRTP will constantly read from the endpoint and drop packets if it's full. - buffer: Buffer::new(0, MAX_BUFFER_SIZE), - bytes_received: AtomicUsize::new(0), - bytes_sent: AtomicUsize::new(0), - done: AtomicBool::new(false), - } - } - pub(crate) fn get_selected_pair(&self) -> Option> { - self.selected_pair.load().clone() - } - - pub(crate) async fn get_best_available_candidate_pair(&self) -> Option> { - let mut best: Option<&Arc> = None; - - let checklist = self.checklist.lock().await; - for p in &*checklist { - if p.state.load(Ordering::SeqCst) == CandidatePairState::Failed as u8 { - continue; - } - - if let Some(b) = &mut best { - if b.priority() < p.priority() { - *b = p; - } - } else { - best = Some(p); - } - } - - best.cloned() - } - - pub(crate) async fn get_best_valid_candidate_pair(&self) -> Option> { - let mut best: Option<&Arc> = None; - - let checklist = self.checklist.lock().await; - for p in &*checklist { - if p.state.load(Ordering::SeqCst) != CandidatePairState::Succeeded as u8 { - continue; - } - - if let Some(b) = &mut best { - if b.priority() < p.priority() { - *b = p; - } - } else { - best = Some(p); - } - } - - best.cloned() - } - - /// Returns the number of bytes sent. - pub fn bytes_sent(&self) -> usize { - self.bytes_sent.load(Ordering::SeqCst) - } - - /// Returns the number of bytes received. - pub fn bytes_received(&self) -> usize { - self.bytes_received.load(Ordering::SeqCst) - } -} - -#[async_trait] -impl Conn for AgentConn { - async fn connect(&self, _addr: SocketAddr) -> std::result::Result<(), util::Error> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, buf: &mut [u8]) -> std::result::Result { - if self.done.load(Ordering::SeqCst) { - return Err(io::Error::new(io::ErrorKind::Other, "Conn is closed").into()); - } - - let n = match self.buffer.read(buf, None).await { - Ok(n) => n, - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err.to_string()).into()), - }; - self.bytes_received.fetch_add(n, Ordering::SeqCst); - - Ok(n) - } - - async fn recv_from( - &self, - buf: &mut [u8], - ) -> std::result::Result<(usize, SocketAddr), util::Error> { - if let Some(raddr) = self.remote_addr() { - let n = self.recv(buf).await?; - Ok((n, raddr)) - } else { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - } - - async fn send(&self, buf: &[u8]) -> std::result::Result { - if self.done.load(Ordering::SeqCst) { - return Err(io::Error::new(io::ErrorKind::Other, "Conn is closed").into()); - } - - if is_message(buf) { - return Err(util::Error::Other("ErrIceWriteStunMessage".into())); - } - - let result = if let Some(pair) = self.get_selected_pair() { - pair.write(buf).await - } else if let Some(pair) = self.get_best_available_candidate_pair().await { - pair.write(buf).await - } else { - Ok(0) - }; - - match result { - Ok(n) => { - self.bytes_sent.fetch_add(buf.len(), Ordering::SeqCst); - Ok(n) - } - Err(err) => Err(io::Error::new(io::ErrorKind::Other, err.to_string()).into()), - } - } - - async fn send_to( - &self, - _buf: &[u8], - _target: SocketAddr, - ) -> std::result::Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> std::result::Result { - if let Some(pair) = self.get_selected_pair() { - Ok(pair.local.addr()) - } else { - Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "Addr Not Available").into()) - } - } - - fn remote_addr(&self) -> Option { - self.get_selected_pair().map(|pair| pair.remote.addr()) - } - - async fn close(&self) -> std::result::Result<(), util::Error> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/ice/src/agent/agent_transport_test.rs b/ice/src/agent/agent_transport_test.rs deleted file mode 100644 index 8ab909df6..000000000 --- a/ice/src/agent/agent_transport_test.rs +++ /dev/null @@ -1,125 +0,0 @@ -use util::vnet::*; -use util::Conn; -use waitgroup::WaitGroup; - -use super::agent_vnet_test::*; -use super::*; -use crate::agent::agent_transport::AgentConn; - -pub(crate) async fn pipe( - default_config0: Option, - default_config1: Option, -) -> Result<(Arc, Arc, Arc, Arc)> { - let (a_notifier, mut a_connected) = on_connected(); - let (b_notifier, mut b_connected) = on_connected(); - - let mut cfg0 = default_config0.unwrap_or_default(); - cfg0.urls = vec![]; - cfg0.network_types = supported_network_types(); - - let a_agent = Arc::new(Agent::new(cfg0).await?); - a_agent.on_connection_state_change(a_notifier); - - let mut cfg1 = default_config1.unwrap_or_default(); - cfg1.urls = vec![]; - cfg1.network_types = supported_network_types(); - - let b_agent = Arc::new(Agent::new(cfg1).await?); - b_agent.on_connection_state_change(b_notifier); - - let (a_conn, b_conn) = connect_with_vnet(&a_agent, &b_agent).await?; - - // Ensure pair selected - // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - Ok((a_conn, b_conn, a_agent, b_agent)) -} - -#[tokio::test] -async fn test_remote_local_addr() -> Result<()> { - // Agent0 is behind 1:1 NAT - let nat_type0 = nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }; - // Agent1 is behind 1:1 NAT - let nat_type1 = nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }; - - let v = build_vnet(nat_type0, nat_type1).await?; - - let stun_server_url = Url { - scheme: SchemeType::Stun, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - proto: ProtoType::Udp, - ..Default::default() - }; - - //"Disconnected Returns nil" - { - let disconnected_conn = AgentConn::new(); - let result = disconnected_conn.local_addr(); - assert!(result.is_err(), "Disconnected Returns nil"); - } - - //"Remote/Local Pair Match between Agents" - { - let (ca, cb) = pipe_with_vnet( - &v, - AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }, - AgentTestConfig { - urls: vec![stun_server_url], - ..Default::default() - }, - ) - .await?; - - let a_laddr = ca.local_addr()?; - let b_laddr = cb.local_addr()?; - - // Assert addresses - assert_eq!(a_laddr.ip().to_string(), VNET_LOCAL_IPA.to_string()); - assert_eq!(b_laddr.ip().to_string(), VNET_LOCAL_IPB.to_string()); - - // Close - //ca.close().await?; - //cb.close().await?; - } - - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_conn_stats() -> Result<()> { - let (ca, cb, _, _) = pipe(None, None).await?; - let na = ca.send(&[0u8; 10]).await?; - - let wg = WaitGroup::new(); - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - let mut buf = vec![0u8; 10]; - let nb = cb.recv(&mut buf).await?; - assert_eq!(nb, 10, "bytes received don't match"); - - Result::<()>::Ok(()) - }); - - wg.wait().await; - - assert_eq!(na, 10, "bytes sent don't match"); - - Ok(()) -} diff --git a/ice/src/agent/agent_vnet_test.rs b/ice/src/agent/agent_vnet_test.rs deleted file mode 100644 index 5974a63e0..000000000 --- a/ice/src/agent/agent_vnet_test.rs +++ /dev/null @@ -1,1019 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr}; -use std::result::Result; -use std::str::FromStr; - -use async_trait::async_trait; -use portable_atomic::AtomicU64; -use util::vnet::chunk::Chunk; -use util::vnet::router::Nic; -use util::vnet::*; -use util::Conn; -use waitgroup::WaitGroup; - -use super::*; -use crate::candidate::candidate_base::unmarshal_candidate; - -pub(crate) struct MockConn; - -#[async_trait] -impl Conn for MockConn { - async fn connect(&self, _addr: SocketAddr) -> Result<(), util::Error> { - Ok(()) - } - async fn recv(&self, _buf: &mut [u8]) -> Result { - Ok(0) - } - async fn recv_from(&self, _buf: &mut [u8]) -> Result<(usize, SocketAddr), util::Error> { - Ok((0, SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0))) - } - async fn send(&self, _buf: &[u8]) -> Result { - Ok(0) - } - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Ok(0) - } - fn local_addr(&self) -> Result { - Ok(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0)) - } - fn remote_addr(&self) -> Option { - None - } - async fn close(&self) -> Result<(), util::Error> { - Ok(()) - } - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -pub(crate) struct VNet { - pub(crate) wan: Arc>, - pub(crate) net0: Arc, - pub(crate) net1: Arc, - pub(crate) server: turn::server::Server, -} - -impl VNet { - pub(crate) async fn close(&self) -> Result<(), Error> { - self.server.close().await?; - let mut w = self.wan.lock().await; - w.stop().await?; - Ok(()) - } -} - -pub(crate) const VNET_GLOBAL_IPA: &str = "27.1.1.1"; -pub(crate) const VNET_LOCAL_IPA: &str = "192.168.0.1"; -pub(crate) const VNET_LOCAL_SUBNET_MASK_A: &str = "24"; -pub(crate) const VNET_GLOBAL_IPB: &str = "28.1.1.1"; -pub(crate) const VNET_LOCAL_IPB: &str = "10.2.0.1"; -pub(crate) const VNET_LOCAL_SUBNET_MASK_B: &str = "24"; -pub(crate) const VNET_STUN_SERVER_IP: &str = "1.2.3.4"; -pub(crate) const VNET_STUN_SERVER_PORT: u16 = 3478; - -pub(crate) async fn build_simple_vnet( - _nat_type0: nat::NatType, - _nat_type1: nat::NatType, -) -> Result { - // WAN - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?)); - - let wnet = Arc::new(net::Net::new(Some(net::NetConfig { - static_ip: VNET_STUN_SERVER_IP.to_owned(), // will be assigned to eth0 - ..Default::default() - }))); - - connect_net2router(&wnet, &wan).await?; - - // LAN - let lan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: format!("{VNET_LOCAL_IPA}/{VNET_LOCAL_SUBNET_MASK_A}"), - ..Default::default() - })?)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.1".to_owned()], - ..Default::default() - }))); - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.2".to_owned()], - ..Default::default() - }))); - - connect_net2router(&net0, &lan).await?; - connect_net2router(&net1, &lan).await?; - connect_router2router(&lan, &wan).await?; - - // start routers... - start_router(&wan).await?; - - let server = add_vnet_stun(wnet).await?; - - Ok(VNet { - wan, - net0, - net1, - server, - }) -} - -pub(crate) async fn build_vnet( - nat_type0: nat::NatType, - nat_type1: nat::NatType, -) -> Result { - // WAN - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?)); - - let wnet = Arc::new(net::Net::new(Some(net::NetConfig { - static_ip: VNET_STUN_SERVER_IP.to_owned(), // will be assigned to eth0 - ..Default::default() - }))); - - connect_net2router(&wnet, &wan).await?; - - // LAN 0 - let lan0 = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - static_ips: if nat_type0.mode == nat::NatMode::Nat1To1 { - vec![format!("{VNET_GLOBAL_IPA}/{VNET_LOCAL_IPA}")] - } else { - vec![VNET_GLOBAL_IPA.to_owned()] - }, - cidr: format!("{VNET_LOCAL_IPA}/{VNET_LOCAL_SUBNET_MASK_A}"), - nat_type: Some(nat_type0), - ..Default::default() - })?)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec![VNET_LOCAL_IPA.to_owned()], - ..Default::default() - }))); - - connect_net2router(&net0, &lan0).await?; - connect_router2router(&lan0, &wan).await?; - - // LAN 1 - let lan1 = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - static_ips: if nat_type1.mode == nat::NatMode::Nat1To1 { - vec![format!("{VNET_GLOBAL_IPB}/{VNET_LOCAL_IPB}")] - } else { - vec![VNET_GLOBAL_IPB.to_owned()] - }, - cidr: format!("{VNET_LOCAL_IPB}/{VNET_LOCAL_SUBNET_MASK_B}"), - nat_type: Some(nat_type1), - ..Default::default() - })?)); - - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec![VNET_LOCAL_IPB.to_owned()], - ..Default::default() - }))); - - connect_net2router(&net1, &lan1).await?; - connect_router2router(&lan1, &wan).await?; - - // start routers... - start_router(&wan).await?; - - let server = add_vnet_stun(wnet).await?; - - Ok(VNet { - wan, - net0, - net1, - server, - }) -} - -pub(crate) struct TestAuthHandler { - pub(crate) cred_map: HashMap>, -} - -impl TestAuthHandler { - pub(crate) fn new() -> Self { - let mut cred_map = HashMap::new(); - cred_map.insert( - "user".to_owned(), - turn::auth::generate_auth_key("user", "webrtc.rs", "pass"), - ); - - TestAuthHandler { cred_map } - } -} - -impl turn::auth::AuthHandler for TestAuthHandler { - fn auth_handle( - &self, - username: &str, - _realm: &str, - _src_addr: SocketAddr, - ) -> Result, turn::Error> { - if let Some(pw) = self.cred_map.get(username) { - Ok(pw.to_vec()) - } else { - Err(turn::Error::Other("fake error".to_owned())) - } - } -} - -pub(crate) async fn add_vnet_stun(wan_net: Arc) -> Result { - // Run TURN(STUN) server - let conn = wan_net - .bind(SocketAddr::from_str(&format!( - "{VNET_STUN_SERVER_IP}:{VNET_STUN_SERVER_PORT}" - ))?) - .await?; - - let server = turn::server::Server::new(turn::server::config::ServerConfig { - conn_configs: vec![turn::server::config::ConnConfig { - conn, - relay_addr_generator: Box::new( - turn::relay::relay_static::RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str(VNET_STUN_SERVER_IP)?, - address: "0.0.0.0".to_owned(), - net: wan_net, - }, - ), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(TestAuthHandler::new()), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - Ok(server) -} - -pub(crate) async fn connect_with_vnet( - a_agent: &Arc, - b_agent: &Arc, -) -> Result<(Arc, Arc), Error> { - // Manual signaling - let (a_ufrag, a_pwd) = a_agent.get_local_user_credentials().await; - let (b_ufrag, b_pwd) = b_agent.get_local_user_credentials().await; - - gather_and_exchange_candidates(a_agent, b_agent).await?; - - let (accepted_tx, mut accepted_rx) = mpsc::channel(1); - let (_a_cancel_tx, a_cancel_rx) = mpsc::channel(1); - - let agent_a = Arc::clone(a_agent); - tokio::spawn(async move { - let a_conn = agent_a.accept(a_cancel_rx, b_ufrag, b_pwd).await?; - - let _ = accepted_tx.send(a_conn).await; - - Result::<(), Error>::Ok(()) - }); - - let (_b_cancel_tx, b_cancel_rx) = mpsc::channel(1); - let b_conn = b_agent.dial(b_cancel_rx, a_ufrag, a_pwd).await?; - - // Ensure accepted - if let Some(a_conn) = accepted_rx.recv().await { - Ok((a_conn, b_conn)) - } else { - Err(Error::Other("no a_conn".to_owned())) - } -} - -#[derive(Default)] -pub(crate) struct AgentTestConfig { - pub(crate) urls: Vec, - pub(crate) nat_1to1_ip_candidate_type: CandidateType, -} - -pub(crate) async fn pipe_with_vnet( - v: &VNet, - a0test_config: AgentTestConfig, - a1test_config: AgentTestConfig, -) -> Result<(Arc, Arc), Error> { - let (a_notifier, mut a_connected) = on_connected(); - let (b_notifier, mut b_connected) = on_connected(); - - let nat_1to1_ips = if a0test_config.nat_1to1_ip_candidate_type != CandidateType::Unspecified { - vec![VNET_GLOBAL_IPA.to_owned()] - } else { - vec![] - }; - - let cfg0 = AgentConfig { - urls: a0test_config.urls, - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - nat_1to1_ips, - nat_1to1_ip_candidate_type: a0test_config.nat_1to1_ip_candidate_type, - net: Some(Arc::clone(&v.net0)), - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - a_agent.on_connection_state_change(a_notifier); - - let nat_1to1_ips = if a1test_config.nat_1to1_ip_candidate_type != CandidateType::Unspecified { - vec![VNET_GLOBAL_IPB.to_owned()] - } else { - vec![] - }; - let cfg1 = AgentConfig { - urls: a1test_config.urls, - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - nat_1to1_ips, - nat_1to1_ip_candidate_type: a1test_config.nat_1to1_ip_candidate_type, - net: Some(Arc::clone(&v.net1)), - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - b_agent.on_connection_state_change(b_notifier); - - let (a_conn, b_conn) = connect_with_vnet(&a_agent, &b_agent).await?; - - // Ensure pair selected - // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - Ok((a_conn, b_conn)) -} - -pub(crate) fn on_connected() -> (OnConnectionStateChangeHdlrFn, mpsc::Receiver<()>) { - let (done_tx, done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - let hdlr_fn: OnConnectionStateChangeHdlrFn = Box::new(move |state: ConnectionState| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if state == ConnectionState::Connected { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }); - (hdlr_fn, done_rx) -} - -pub(crate) async fn gather_and_exchange_candidates( - a_agent: &Arc, - b_agent: &Arc, -) -> Result<(), Error> { - let wg = WaitGroup::new(); - - let w1 = Arc::new(Mutex::new(Some(wg.worker()))); - a_agent.on_candidate(Box::new( - move |candidate: Option>| { - let w3 = Arc::clone(&w1); - Box::pin(async move { - if candidate.is_none() { - let mut w = w3.lock().await; - w.take(); - } - }) - }, - )); - a_agent.gather_candidates()?; - - let w2 = Arc::new(Mutex::new(Some(wg.worker()))); - b_agent.on_candidate(Box::new( - move |candidate: Option>| { - let w3 = Arc::clone(&w2); - Box::pin(async move { - if candidate.is_none() { - let mut w = w3.lock().await; - w.take(); - } - }) - }, - )); - b_agent.gather_candidates()?; - - wg.wait().await; - - let candidates = a_agent.get_local_candidates().await?; - for c in candidates { - let c2: Arc = - Arc::new(unmarshal_candidate(c.marshal().as_str())?); - b_agent.add_remote_candidate(&c2)?; - } - - let candidates = b_agent.get_local_candidates().await?; - for c in candidates { - let c2: Arc = - Arc::new(unmarshal_candidate(c.marshal().as_str())?); - a_agent.add_remote_candidate(&c2)?; - } - - Ok(()) -} - -pub(crate) async fn start_router(router: &Arc>) -> Result<(), Error> { - let mut w = router.lock().await; - Ok(w.start().await?) -} - -pub(crate) async fn connect_net2router( - net: &Arc, - router: &Arc>, -) -> Result<(), Error> { - let nic = net.get_nic()?; - - { - let mut w = router.lock().await; - w.add_net(Arc::clone(&nic)).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(router)).await?; - } - - Ok(()) -} - -pub(crate) async fn connect_router2router( - child: &Arc>, - parent: &Arc>, -) -> Result<(), Error> { - { - let mut w = parent.lock().await; - w.add_router(Arc::clone(child)).await?; - } - - { - let l = child.lock().await; - l.set_router(Arc::clone(parent)).await?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_simple_vnet_full_cone_nats_on_both_ends() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let stun_server_url = Url { - scheme: SchemeType::Stun, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - proto: ProtoType::Udp, - ..Default::default() - }; - - // buildVNet with a Full-cone NATs both LANs - let nat_type = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointIndependent, - filtering_behavior: nat::EndpointDependencyType::EndpointIndependent, - ..Default::default() - }; - - let v = build_simple_vnet(nat_type, nat_type).await?; - - log::debug!("Connecting..."); - let a0test_config = AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }; - let a1test_config = AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }; - let (_ca, _cb) = pipe_with_vnet(&v, a0test_config, a1test_config).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - - log::debug!("Closing..."); - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_vnet_full_cone_nats_on_both_ends() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let stun_server_url = Url { - scheme: SchemeType::Stun, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - proto: ProtoType::Udp, - ..Default::default() - }; - - let _turn_server_url = Url { - scheme: SchemeType::Turn, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - username: "user".to_owned(), - password: "pass".to_owned(), - proto: ProtoType::Udp, - }; - - // buildVNet with a Full-cone NATs both LANs - let nat_type = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointIndependent, - filtering_behavior: nat::EndpointDependencyType::EndpointIndependent, - ..Default::default() - }; - - let v = build_vnet(nat_type, nat_type).await?; - - log::debug!("Connecting..."); - let a0test_config = AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }; - let a1test_config = AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }; - let (_ca, _cb) = pipe_with_vnet(&v, a0test_config, a1test_config).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - - log::debug!("Closing..."); - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_vnet_symmetric_nats_on_both_ends() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let stun_server_url = Url { - scheme: SchemeType::Stun, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - proto: ProtoType::Udp, - ..Default::default() - }; - - let turn_server_url = Url { - scheme: SchemeType::Turn, - host: VNET_STUN_SERVER_IP.to_owned(), - port: VNET_STUN_SERVER_PORT, - username: "user".to_owned(), - password: "pass".to_owned(), - proto: ProtoType::Udp, - }; - - // buildVNet with a Symmetric NATs for both LANs - let nat_type = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - filtering_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - ..Default::default() - }; - - let v = build_vnet(nat_type, nat_type).await?; - - log::debug!("Connecting..."); - let a0test_config = AgentTestConfig { - urls: vec![stun_server_url.clone(), turn_server_url.clone()], - ..Default::default() - }; - let a1test_config = AgentTestConfig { - urls: vec![stun_server_url.clone()], - ..Default::default() - }; - let (_ca, _cb) = pipe_with_vnet(&v, a0test_config, a1test_config).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - - log::debug!("Closing..."); - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_vnet_1to1_nat_with_host_candidate_vs_symmetric_nats() -> Result<(), Error> -{ - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - // Agent0 is behind 1:1 NAT - let nat_type0 = nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }; - // Agent1 is behind a symmetric NAT - let nat_type1 = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - filtering_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - ..Default::default() - }; - log::debug!("natType0: {:?}", nat_type0); - log::debug!("natType1: {:?}", nat_type1); - - let v = build_vnet(nat_type0, nat_type1).await?; - - log::debug!("Connecting..."); - let a0test_config = AgentTestConfig { - urls: vec![], - nat_1to1_ip_candidate_type: CandidateType::Host, // Use 1:1 NAT IP as a host candidate - }; - let a1test_config = AgentTestConfig { - urls: vec![], - ..Default::default() - }; - let (_ca, _cb) = pipe_with_vnet(&v, a0test_config, a1test_config).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - - log::debug!("Closing..."); - v.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_connectivity_vnet_1to1_nat_with_srflx_candidate_vs_symmetric_nats( -) -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - // Agent0 is behind 1:1 NAT - let nat_type0 = nat::NatType { - mode: nat::NatMode::Nat1To1, - ..Default::default() - }; - // Agent1 is behind a symmetric NAT - let nat_type1 = nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - filtering_behavior: nat::EndpointDependencyType::EndpointAddrPortDependent, - ..Default::default() - }; - log::debug!("natType0: {:?}", nat_type0); - log::debug!("natType1: {:?}", nat_type1); - - let v = build_vnet(nat_type0, nat_type1).await?; - - log::debug!("Connecting..."); - let a0test_config = AgentTestConfig { - urls: vec![], - nat_1to1_ip_candidate_type: CandidateType::ServerReflexive, // Use 1:1 NAT IP as a srflx candidate - }; - let a1test_config = AgentTestConfig { - urls: vec![], - ..Default::default() - }; - let (_ca, _cb) = pipe_with_vnet(&v, a0test_config, a1test_config).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - - log::debug!("Closing..."); - v.close().await?; - - Ok(()) -} - -async fn block_until_state_seen( - expected_state: ConnectionState, - state_queue: &mut mpsc::Receiver, -) { - while let Some(s) = state_queue.recv().await { - if s == expected_state { - return; - } - } -} - -// test_disconnected_to_connected asserts that an agent can go to disconnected, and then return to connected successfully -#[tokio::test] -async fn test_disconnected_to_connected() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - // Create a network with two interfaces - let wan = router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?; - - let drop_all_data = Arc::new(AtomicU64::new(0)); - let drop_all_data2 = Arc::clone(&drop_all_data); - wan.add_chunk_filter(Box::new(move |_c: &(dyn Chunk + Send + Sync)| -> bool { - drop_all_data2.load(Ordering::SeqCst) != 1 - })) - .await; - let wan = Arc::new(Mutex::new(wan)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.1".to_owned()], - ..Default::default() - }))); - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.2".to_owned()], - ..Default::default() - }))); - - connect_net2router(&net0, &wan).await?; - connect_net2router(&net1, &wan).await?; - start_router(&wan).await?; - - let disconnected_timeout = Duration::from_secs(1); - let keepalive_interval = Duration::from_millis(20); - - // Create two agents and connect them - let controlling_agent = Arc::new( - Agent::new(AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&net0)), - disconnected_timeout: Some(disconnected_timeout), - keepalive_interval: Some(keepalive_interval), - check_interval: keepalive_interval, - ..Default::default() - }) - .await?, - ); - - let controlled_agent = Arc::new( - Agent::new(AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&net1)), - disconnected_timeout: Some(disconnected_timeout), - keepalive_interval: Some(keepalive_interval), - check_interval: keepalive_interval, - ..Default::default() - }) - .await?, - ); - - let (controlling_state_changes_tx, mut controlling_state_changes_rx) = - mpsc::channel::(100); - let controlling_state_changes_tx = Arc::new(controlling_state_changes_tx); - controlling_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let controlling_state_changes_tx_clone = Arc::clone(&controlling_state_changes_tx); - Box::pin(async move { - let _ = controlling_state_changes_tx_clone.try_send(c); - }) - })); - - let (controlled_state_changes_tx, mut controlled_state_changes_rx) = - mpsc::channel::(100); - let controlled_state_changes_tx = Arc::new(controlled_state_changes_tx); - controlled_agent.on_connection_state_change(Box::new(move |c: ConnectionState| { - let controlled_state_changes_tx_clone = Arc::clone(&controlled_state_changes_tx); - Box::pin(async move { - let _ = controlled_state_changes_tx_clone.try_send(c); - }) - })); - - connect_with_vnet(&controlling_agent, &controlled_agent).await?; - - // Assert we have gone to connected - block_until_state_seen( - ConnectionState::Connected, - &mut controlling_state_changes_rx, - ) - .await; - block_until_state_seen(ConnectionState::Connected, &mut controlled_state_changes_rx).await; - - // Drop all packets, and block until we have gone to disconnected - drop_all_data.store(1, Ordering::SeqCst); - block_until_state_seen( - ConnectionState::Disconnected, - &mut controlling_state_changes_rx, - ) - .await; - block_until_state_seen( - ConnectionState::Disconnected, - &mut controlled_state_changes_rx, - ) - .await; - - // Allow all packets through again, block until we have gone to connected - drop_all_data.store(0, Ordering::SeqCst); - block_until_state_seen( - ConnectionState::Connected, - &mut controlling_state_changes_rx, - ) - .await; - block_until_state_seen(ConnectionState::Connected, &mut controlled_state_changes_rx).await; - - { - let mut w = wan.lock().await; - w.stop().await?; - } - - controlling_agent.close().await?; - controlled_agent.close().await?; - - Ok(()) -} - -//use std::io::Write; - -// Agent.Write should use the best valid pair if a selected pair is not yet available -#[tokio::test] -async fn test_write_use_valid_pair() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - // Create a network with two interfaces - let wan = router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?; - - wan.add_chunk_filter(Box::new(move |c: &(dyn Chunk + Send + Sync)| -> bool { - let raw = c.user_data(); - if stun::message::is_message(&raw) { - let mut m = stun::message::Message { - raw, - ..Default::default() - }; - let result = m.decode(); - if result.is_err() | m.contains(stun::attributes::ATTR_USE_CANDIDATE) { - return false; - } - } - - true - })) - .await; - let wan = Arc::new(Mutex::new(wan)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.1".to_owned()], - ..Default::default() - }))); - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ips: vec!["192.168.0.2".to_owned()], - ..Default::default() - }))); - - connect_net2router(&net0, &wan).await?; - connect_net2router(&net1, &wan).await?; - start_router(&wan).await?; - - // Create two agents and connect them - let controlling_agent = Arc::new( - Agent::new(AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&net0)), - ..Default::default() - }) - .await?, - ); - - let controlled_agent = Arc::new( - Agent::new(AgentConfig { - network_types: supported_network_types(), - multicast_dns_mode: MulticastDnsMode::Disabled, - net: Some(Arc::clone(&net1)), - ..Default::default() - }) - .await?, - ); - - gather_and_exchange_candidates(&controlling_agent, &controlled_agent).await?; - - let (controlling_ufrag, controlling_pwd) = controlling_agent.get_local_user_credentials().await; - let (controlled_ufrag, controlled_pwd) = controlled_agent.get_local_user_credentials().await; - - let controlling_agent_tx = Arc::clone(&controlling_agent); - tokio::spawn(async move { - let test_message = "Test Message"; - let controlling_agent_conn = { - controlling_agent_tx - .internal - .start_connectivity_checks(true, controlled_ufrag, controlled_pwd) - .await?; - Arc::clone(&controlling_agent_tx.internal.agent_conn) as Arc - }; - - log::debug!("controlling_agent start_connectivity_checks done..."); - loop { - let result = controlling_agent_conn.send(test_message.as_bytes()).await; - if result.is_err() { - break; - } - - tokio::time::sleep(Duration::from_millis(20)).await; - } - - Result::<(), Error>::Ok(()) - }); - - let controlled_agent_conn = { - controlled_agent - .internal - .start_connectivity_checks(false, controlling_ufrag, controlling_pwd) - .await?; - Arc::clone(&controlled_agent.internal.agent_conn) as Arc - }; - - log::debug!("controlled_agent start_connectivity_checks done..."); - - let test_message = "Test Message"; - let mut read_buf = vec![0u8; test_message.as_bytes().len()]; - controlled_agent_conn.recv(&mut read_buf).await?; - - assert_eq!(read_buf, test_message.as_bytes(), "should match"); - - { - let mut w = wan.lock().await; - w.stop().await?; - } - - controlling_agent.close().await?; - controlled_agent.close().await?; - - Ok(()) -} diff --git a/ice/src/agent/mod.rs b/ice/src/agent/mod.rs deleted file mode 100644 index 389b861c9..000000000 --- a/ice/src/agent/mod.rs +++ /dev/null @@ -1,517 +0,0 @@ -#[cfg(test)] -mod agent_gather_test; -#[cfg(test)] -mod agent_test; -#[cfg(test)] -mod agent_transport_test; -#[cfg(test)] -pub(crate) mod agent_vnet_test; - -pub mod agent_config; -pub mod agent_gather; -pub(crate) mod agent_internal; -pub mod agent_selector; -pub mod agent_stats; -pub mod agent_transport; - -use std::collections::HashMap; -use std::future::Future; -use std::net::{Ipv4Addr, SocketAddr}; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use agent_config::*; -use agent_internal::*; -use agent_stats::*; -use mdns::conn::*; -use portable_atomic::{AtomicU8, AtomicUsize}; -use stun::agent::*; -use stun::attributes::*; -use stun::fingerprint::*; -use stun::integrity::*; -use stun::message::*; -use stun::xoraddr::*; -use tokio::sync::{broadcast, mpsc, Mutex}; -use tokio::time::{Duration, Instant}; -use util::vnet::net::*; -use util::Buffer; - -use crate::agent::agent_gather::GatherCandidatesInternalParams; -use crate::candidate::*; -use crate::error::*; -use crate::external_ip_mapper::*; -use crate::mdns::*; -use crate::network_type::*; -use crate::rand::*; -use crate::state::*; -use crate::tcp_type::TcpType; -use crate::udp_mux::UDPMux; -use crate::udp_network::UDPNetwork; -use crate::url::*; - -#[derive(Debug, Clone)] -pub(crate) struct BindingRequest { - pub(crate) timestamp: Instant, - pub(crate) transaction_id: TransactionId, - pub(crate) destination: SocketAddr, - pub(crate) is_use_candidate: bool, -} - -impl Default for BindingRequest { - fn default() -> Self { - Self { - timestamp: Instant::now(), - transaction_id: TransactionId::default(), - destination: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), - is_use_candidate: false, - } - } -} - -pub type OnConnectionStateChangeHdlrFn = Box< - dyn (FnMut(ConnectionState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; -pub type OnSelectedCandidatePairChangeHdlrFn = Box< - dyn (FnMut( - &Arc, - &Arc, - ) -> Pin + Send + 'static>>) - + Send - + Sync, ->; -pub type OnCandidateHdlrFn = Box< - dyn (FnMut( - Option>, - ) -> Pin + Send + 'static>>) - + Send - + Sync, ->; -pub type GatherCandidateCancelFn = Box; - -struct ChanReceivers { - chan_state_rx: mpsc::Receiver, - chan_candidate_rx: mpsc::Receiver>>, - chan_candidate_pair_rx: mpsc::Receiver<()>, -} - -/// Represents the ICE agent. -pub struct Agent { - pub(crate) internal: Arc, - - pub(crate) udp_network: UDPNetwork, - pub(crate) interface_filter: Arc>, - pub(crate) ip_filter: Arc>, - pub(crate) mdns_mode: MulticastDnsMode, - pub(crate) mdns_name: String, - pub(crate) mdns_conn: Option>, - pub(crate) net: Arc, - - // 1:1 D-NAT IP address mapping - pub(crate) ext_ip_mapper: Arc>, - pub(crate) gathering_state: Arc, //GatheringState, - pub(crate) candidate_types: Vec, - pub(crate) urls: Vec, - pub(crate) network_types: Vec, - - pub(crate) gather_candidate_cancel: Option, -} - -impl Agent { - /// Creates a new Agent. - pub async fn new(config: AgentConfig) -> Result { - let mut mdns_name = config.multicast_dns_host_name.clone(); - if mdns_name.is_empty() { - mdns_name = generate_multicast_dns_name(); - } - - if !mdns_name.ends_with(".local") || mdns_name.split('.').count() != 2 { - return Err(Error::ErrInvalidMulticastDnshostName); - } - - let mdns_mode = config.multicast_dns_mode; - - let mdns_conn = - match create_multicast_dns(mdns_mode, &mdns_name, &config.multicast_dns_dest_addr) { - Ok(c) => c, - Err(err) => { - // Opportunistic mDNS: If we can't open the connection, that's ok: we - // can continue without it. - log::warn!("Failed to initialize mDNS {}: {}", mdns_name, err); - None - } - }; - - let (mut ai, chan_receivers) = AgentInternal::new(&config); - let (chan_state_rx, chan_candidate_rx, chan_candidate_pair_rx) = ( - chan_receivers.chan_state_rx, - chan_receivers.chan_candidate_rx, - chan_receivers.chan_candidate_pair_rx, - ); - - config.init_with_defaults(&mut ai); - - let candidate_types = if config.candidate_types.is_empty() { - default_candidate_types() - } else { - config.candidate_types.clone() - }; - - if ai.lite.load(Ordering::SeqCst) - && (candidate_types.len() != 1 || candidate_types[0] != CandidateType::Host) - { - Self::close_multicast_conn(&mdns_conn).await; - return Err(Error::ErrLiteUsingNonHostCandidates); - } - - if !config.urls.is_empty() - && !contains_candidate_type(CandidateType::ServerReflexive, &candidate_types) - && !contains_candidate_type(CandidateType::Relay, &candidate_types) - { - Self::close_multicast_conn(&mdns_conn).await; - return Err(Error::ErrUselessUrlsProvided); - } - - let ext_ip_mapper = match config.init_ext_ip_mapping(mdns_mode, &candidate_types) { - Ok(ext_ip_mapper) => ext_ip_mapper, - Err(err) => { - Self::close_multicast_conn(&mdns_conn).await; - return Err(err); - } - }; - - let net = if let Some(net) = config.net { - if net.is_virtual() { - log::warn!("vnet is enabled"); - if mdns_mode != MulticastDnsMode::Disabled { - log::warn!("vnet does not support mDNS yet"); - } - } - - net - } else { - Arc::new(Net::new(None)) - }; - - let agent = Self { - udp_network: config.udp_network, - internal: Arc::new(ai), - interface_filter: Arc::clone(&config.interface_filter), - ip_filter: Arc::clone(&config.ip_filter), - mdns_mode, - mdns_name, - mdns_conn, - net, - ext_ip_mapper: Arc::new(ext_ip_mapper), - gathering_state: Arc::new(AtomicU8::new(0)), //GatheringState::New, - candidate_types, - urls: config.urls.clone(), - network_types: config.network_types.clone(), - - gather_candidate_cancel: None, //TODO: add cancel - }; - - agent.internal.start_on_connection_state_change_routine( - chan_state_rx, - chan_candidate_rx, - chan_candidate_pair_rx, - ); - - // Restart is also used to initialize the agent for the first time - if let Err(err) = agent.restart(config.local_ufrag, config.local_pwd).await { - Self::close_multicast_conn(&agent.mdns_conn).await; - let _ = agent.close().await; - return Err(err); - } - - Ok(agent) - } - - pub fn get_bytes_received(&self) -> usize { - self.internal.agent_conn.bytes_received() - } - - pub fn get_bytes_sent(&self) -> usize { - self.internal.agent_conn.bytes_sent() - } - - /// Sets a handler that is fired when the connection state changes. - pub fn on_connection_state_change(&self, f: OnConnectionStateChangeHdlrFn) { - self.internal - .on_connection_state_change_hdlr - .store(Some(Arc::new(Mutex::new(f)))) - } - - /// Sets a handler that is fired when the final candidate pair is selected. - pub fn on_selected_candidate_pair_change(&self, f: OnSelectedCandidatePairChangeHdlrFn) { - self.internal - .on_selected_candidate_pair_change_hdlr - .store(Some(Arc::new(Mutex::new(f)))) - } - - /// Sets a handler that is fired when new candidates gathered. When the gathering process - /// complete the last candidate is nil. - pub fn on_candidate(&self, f: OnCandidateHdlrFn) { - self.internal - .on_candidate_hdlr - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// Adds a new remote candidate. - pub fn add_remote_candidate(&self, c: &Arc) -> Result<()> { - // cannot check for network yet because it might not be applied - // when mDNS hostame is used. - if c.tcp_type() == TcpType::Active { - // TCP Candidates with tcptype active will probe server passive ones, so - // no need to do anything with them. - log::info!("Ignoring remote candidate with tcpType active: {}", c); - return Ok(()); - } - - // If we have a mDNS Candidate lets fully resolve it before adding it locally - if c.candidate_type() == CandidateType::Host && c.address().ends_with(".local") { - if self.mdns_mode == MulticastDnsMode::Disabled { - log::warn!( - "remote mDNS candidate added, but mDNS is disabled: ({})", - c.address() - ); - return Ok(()); - } - - if c.candidate_type() != CandidateType::Host { - return Err(Error::ErrAddressParseFailed); - } - - let ai = Arc::clone(&self.internal); - let host_candidate = Arc::clone(c); - let mdns_conn = self.mdns_conn.clone(); - tokio::spawn(async move { - if let Some(mdns_conn) = mdns_conn { - if let Ok(candidate) = - Self::resolve_and_add_multicast_candidate(mdns_conn, host_candidate).await - { - ai.add_remote_candidate(&candidate).await; - } - } - }); - } else { - let ai = Arc::clone(&self.internal); - let candidate = Arc::clone(c); - tokio::spawn(async move { - ai.add_remote_candidate(&candidate).await; - }); - } - - Ok(()) - } - - /// Returns the local candidates. - pub async fn get_local_candidates(&self) -> Result>> { - let mut res = vec![]; - - { - let local_candidates = self.internal.local_candidates.lock().await; - for candidates in local_candidates.values() { - for candidate in candidates { - res.push(Arc::clone(candidate)); - } - } - } - - Ok(res) - } - - /// Returns the local user credentials. - pub async fn get_local_user_credentials(&self) -> (String, String) { - let ufrag_pwd = self.internal.ufrag_pwd.lock().await; - (ufrag_pwd.local_ufrag.clone(), ufrag_pwd.local_pwd.clone()) - } - - /// Returns the remote user credentials. - pub async fn get_remote_user_credentials(&self) -> (String, String) { - let ufrag_pwd = self.internal.ufrag_pwd.lock().await; - (ufrag_pwd.remote_ufrag.clone(), ufrag_pwd.remote_pwd.clone()) - } - - /// Cleans up the Agent. - pub async fn close(&self) -> Result<()> { - if let Some(gather_candidate_cancel) = &self.gather_candidate_cancel { - gather_candidate_cancel(); - } - - if let UDPNetwork::Muxed(ref udp_mux) = self.udp_network { - let (ufrag, _) = self.get_local_user_credentials().await; - udp_mux.remove_conn_by_ufrag(&ufrag).await; - } - - //FIXME: deadlock here - self.internal.close().await - } - - /// Returns the selected pair or nil if there is none - pub fn get_selected_candidate_pair(&self) -> Option> { - self.internal.agent_conn.get_selected_pair() - } - - /// Sets the credentials of the remote agent. - pub async fn set_remote_credentials( - &self, - remote_ufrag: String, - remote_pwd: String, - ) -> Result<()> { - self.internal - .set_remote_credentials(remote_ufrag, remote_pwd) - .await - } - - /// Restarts the ICE Agent with the provided ufrag/pwd - /// If no ufrag/pwd is provided the Agent will generate one itself. - /// - /// Restart must only be called when `GatheringState` is `GatheringStateComplete` - /// a user must then call `GatherCandidates` explicitly to start generating new ones. - pub async fn restart(&self, mut ufrag: String, mut pwd: String) -> Result<()> { - if ufrag.is_empty() { - ufrag = generate_ufrag(); - } - if pwd.is_empty() { - pwd = generate_pwd(); - } - - if ufrag.len() * 8 < 24 { - return Err(Error::ErrLocalUfragInsufficientBits); - } - if pwd.len() * 8 < 128 { - return Err(Error::ErrLocalPwdInsufficientBits); - } - - if GatheringState::from(self.gathering_state.load(Ordering::SeqCst)) - == GatheringState::Gathering - { - return Err(Error::ErrRestartWhenGathering); - } - self.gathering_state - .store(GatheringState::New as u8, Ordering::SeqCst); - - { - let done_tx = self.internal.done_tx.lock().await; - if done_tx.is_none() { - return Err(Error::ErrClosed); - } - } - - // Clear all agent needed to take back to fresh state - { - let mut ufrag_pwd = self.internal.ufrag_pwd.lock().await; - ufrag_pwd.local_ufrag = ufrag; - ufrag_pwd.local_pwd = pwd; - ufrag_pwd.remote_ufrag = String::new(); - ufrag_pwd.remote_pwd = String::new(); - } - { - let mut pending_binding_requests = self.internal.pending_binding_requests.lock().await; - *pending_binding_requests = vec![]; - } - - { - let mut checklist = self.internal.agent_conn.checklist.lock().await; - *checklist = vec![]; - } - - self.internal.set_selected_pair(None).await; - self.internal.delete_all_candidates().await; - self.internal.start().await; - - // Restart is used by NewAgent. Accept/Connect should be used to move to checking - // for new Agents - if self.internal.connection_state.load(Ordering::SeqCst) != ConnectionState::New as u8 { - self.internal - .update_connection_state(ConnectionState::Checking) - .await; - } - - Ok(()) - } - - /// Initiates the trickle based gathering process. - pub fn gather_candidates(&self) -> Result<()> { - if self.gathering_state.load(Ordering::SeqCst) != GatheringState::New as u8 { - return Err(Error::ErrMultipleGatherAttempted); - } - - if self.internal.on_candidate_hdlr.load().is_none() { - return Err(Error::ErrNoOnCandidateHandler); - } - - if let Some(gather_candidate_cancel) = &self.gather_candidate_cancel { - gather_candidate_cancel(); // Cancel previous gathering routine - } - - //TODO: a.gatherCandidateCancel = cancel - - let params = GatherCandidatesInternalParams { - udp_network: self.udp_network.clone(), - candidate_types: self.candidate_types.clone(), - urls: self.urls.clone(), - network_types: self.network_types.clone(), - mdns_mode: self.mdns_mode, - mdns_name: self.mdns_name.clone(), - net: Arc::clone(&self.net), - interface_filter: self.interface_filter.clone(), - ip_filter: self.ip_filter.clone(), - ext_ip_mapper: Arc::clone(&self.ext_ip_mapper), - agent_internal: Arc::clone(&self.internal), - gathering_state: Arc::clone(&self.gathering_state), - chan_candidate_tx: Arc::clone(&self.internal.chan_candidate_tx), - }; - tokio::spawn(async move { - Self::gather_candidates_internal(params).await; - }); - - Ok(()) - } - - /// Returns a list of candidate pair stats. - pub async fn get_candidate_pairs_stats(&self) -> Vec { - self.internal.get_candidate_pairs_stats().await - } - - /// Returns a list of local candidates stats. - pub async fn get_local_candidates_stats(&self) -> Vec { - self.internal.get_local_candidates_stats().await - } - - /// Returns a list of remote candidates stats. - pub async fn get_remote_candidates_stats(&self) -> Vec { - self.internal.get_remote_candidates_stats().await - } - - async fn resolve_and_add_multicast_candidate( - mdns_conn: Arc, - c: Arc, - ) -> Result> { - //TODO: hook up _close_query_signal_tx to Agent or Candidate's Close signal? - let (_close_query_signal_tx, close_query_signal_rx) = mpsc::channel(1); - let src = match mdns_conn.query(&c.address(), close_query_signal_rx).await { - Ok((_, src)) => src, - Err(err) => { - log::warn!("Failed to discover mDNS candidate {}: {}", c.address(), err); - return Err(err.into()); - } - }; - - c.set_ip(&src.ip())?; - - Ok(c) - } - - async fn close_multicast_conn(mdns_conn: &Option>) { - if let Some(conn) = mdns_conn { - if let Err(err) = conn.close().await { - log::warn!("failed to close mDNS Conn: {}", err); - } - } - } -} diff --git a/ice/src/candidate/candidate_base.rs b/ice/src/candidate/candidate_base.rs deleted file mode 100644 index 08125672c..000000000 --- a/ice/src/candidate/candidate_base.rs +++ /dev/null @@ -1,525 +0,0 @@ -use std::fmt; -use std::ops::Add; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use async_trait::async_trait; -use crc::{Crc, CRC_32_ISCSI}; -use portable_atomic::{AtomicU16, AtomicU64, AtomicU8}; -use tokio::sync::{broadcast, Mutex}; -use util::sync::Mutex as SyncMutex; - -use super::*; -use crate::candidate::candidate_host::CandidateHostConfig; -use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; -use crate::candidate::candidate_relay::CandidateRelayConfig; -use crate::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; -use crate::error::*; -use crate::util::*; - -#[derive(Default)] -pub struct CandidateBaseConfig { - pub candidate_id: String, - pub network: String, - pub address: String, - pub port: u16, - pub component: u16, - pub priority: u32, - pub foundation: String, - pub conn: Option>, - pub initialized_ch: Option>, -} - -pub struct CandidateBase { - pub(crate) id: String, - pub(crate) network_type: AtomicU8, - pub(crate) candidate_type: CandidateType, - - pub(crate) component: AtomicU16, - pub(crate) address: String, - pub(crate) port: u16, - pub(crate) related_address: Option, - pub(crate) tcp_type: TcpType, - - pub(crate) resolved_addr: SyncMutex, - - pub(crate) last_sent: AtomicU64, - pub(crate) last_received: AtomicU64, - - pub(crate) conn: Option>, - pub(crate) closed_ch: Arc>>>, - - pub(crate) foundation_override: String, - pub(crate) priority_override: u32, - - //CandidateHost - pub(crate) network: String, - //CandidateRelay - pub(crate) relay_client: Option>, -} - -impl Default for CandidateBase { - fn default() -> Self { - Self { - id: String::new(), - network_type: AtomicU8::new(0), - candidate_type: CandidateType::default(), - - component: AtomicU16::new(0), - address: String::new(), - port: 0, - related_address: None, - tcp_type: TcpType::default(), - - resolved_addr: SyncMutex::new(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0)), - - last_sent: AtomicU64::new(0), - last_received: AtomicU64::new(0), - - conn: None, - closed_ch: Arc::new(Mutex::new(None)), - - foundation_override: String::new(), - priority_override: 0, - network: String::new(), - relay_client: None, - } - } -} - -// String makes the candidateBase printable -impl fmt::Display for CandidateBase { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(related_address) = self.related_address() { - write!( - f, - "{} {} {}:{}{}", - self.network_type(), - self.candidate_type(), - self.address(), - self.port(), - related_address, - ) - } else { - write!( - f, - "{} {} {}:{}", - self.network_type(), - self.candidate_type(), - self.address(), - self.port(), - ) - } - } -} - -#[async_trait] -impl Candidate for CandidateBase { - fn foundation(&self) -> String { - if !self.foundation_override.is_empty() { - return self.foundation_override.clone(); - } - - let mut buf = vec![]; - buf.extend_from_slice(self.candidate_type().to_string().as_bytes()); - buf.extend_from_slice(self.address.as_bytes()); - buf.extend_from_slice(self.network_type().to_string().as_bytes()); - - let checksum = Crc::::new(&CRC_32_ISCSI).checksum(&buf); - - format!("{checksum}") - } - - /// Returns Candidate ID. - fn id(&self) -> String { - self.id.clone() - } - - /// Returns candidate component. - fn component(&self) -> u16 { - self.component.load(Ordering::SeqCst) - } - - fn set_component(&self, component: u16) { - self.component.store(component, Ordering::SeqCst); - } - - /// Returns a time indicating the last time this candidate was received. - fn last_received(&self) -> SystemTime { - UNIX_EPOCH.add(Duration::from_nanos( - self.last_received.load(Ordering::SeqCst), - )) - } - - /// Returns a time indicating the last time this candidate was sent. - fn last_sent(&self) -> SystemTime { - UNIX_EPOCH.add(Duration::from_nanos(self.last_sent.load(Ordering::SeqCst))) - } - - /// Returns candidate NetworkType. - fn network_type(&self) -> NetworkType { - NetworkType::from(self.network_type.load(Ordering::SeqCst)) - } - - /// Returns Candidate Address. - fn address(&self) -> String { - self.address.clone() - } - - /// Returns Candidate Port. - fn port(&self) -> u16 { - self.port - } - - /// Computes the priority for this ICE Candidate. - fn priority(&self) -> u32 { - if self.priority_override != 0 { - return self.priority_override; - } - - // The local preference MUST be an integer from 0 (lowest preference) to - // 65535 (highest preference) inclusive. When there is only a single IP - // address, this value SHOULD be set to 65535. If there are multiple - // candidates for a particular component for a particular data stream - // that have the same type, the local preference MUST be unique for each - // one. - (1 << 24) * u32::from(self.candidate_type().preference()) - + (1 << 8) * u32::from(self.local_preference()) - + (256 - u32::from(self.component())) - } - - /// Returns `Option`. - fn related_address(&self) -> Option { - self.related_address.as_ref().cloned() - } - - /// Returns candidate type. - fn candidate_type(&self) -> CandidateType { - self.candidate_type - } - - fn tcp_type(&self) -> TcpType { - self.tcp_type - } - - /// Returns the string representation of the ICECandidate. - fn marshal(&self) -> String { - let mut val = format!( - "{} {} {} {} {} {} typ {}", - self.foundation(), - self.component(), - self.network_type().network_short(), - self.priority(), - self.address(), - self.port(), - self.candidate_type() - ); - - if self.tcp_type != TcpType::Unspecified { - val += format!(" tcptype {}", self.tcp_type()).as_str(); - } - - if let Some(related_address) = self.related_address() { - val += format!( - " raddr {} rport {}", - related_address.address, related_address.port, - ) - .as_str(); - } - - val - } - - fn addr(&self) -> SocketAddr { - *self.resolved_addr.lock() - } - - /// Stops the recvLoop. - async fn close(&self) -> Result<()> { - { - let mut closed_ch = self.closed_ch.lock().await; - if closed_ch.is_none() { - return Err(Error::ErrClosed); - } - closed_ch.take(); - } - - if let Some(relay_client) = &self.relay_client { - let _ = relay_client.close().await; - } - - if let Some(conn) = &self.conn { - let _ = conn.close().await; - } - - Ok(()) - } - - fn seen(&self, outbound: bool) { - let d = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)); - - if outbound { - self.set_last_sent(d); - } else { - self.set_last_received(d); - } - } - - async fn write_to(&self, raw: &[u8], dst: &(dyn Candidate + Send + Sync)) -> Result { - let n = if let Some(conn) = &self.conn { - let addr = dst.addr(); - conn.send_to(raw, addr).await? - } else { - 0 - }; - self.seen(true); - Ok(n) - } - - /// Used to compare two candidateBases. - fn equal(&self, other: &dyn Candidate) -> bool { - self.network_type() == other.network_type() - && self.candidate_type() == other.candidate_type() - && self.address() == other.address() - && self.port() == other.port() - && self.tcp_type() == other.tcp_type() - && self.related_address() == other.related_address() - } - - fn set_ip(&self, ip: &IpAddr) -> Result<()> { - let network_type = determine_network_type(&self.network, ip)?; - - self.network_type - .store(network_type as u8, Ordering::SeqCst); - - let addr = create_addr(network_type, *ip, self.port); - *self.resolved_addr.lock() = addr; - - Ok(()) - } - - fn get_conn(&self) -> Option<&Arc> { - self.conn.as_ref() - } - - fn get_closed_ch(&self) -> Arc>>> { - self.closed_ch.clone() - } -} - -impl CandidateBase { - pub fn set_last_received(&self, d: Duration) { - #[allow(clippy::cast_possible_truncation)] - self.last_received - .store(d.as_nanos() as u64, Ordering::SeqCst); - } - - pub fn set_last_sent(&self, d: Duration) { - #[allow(clippy::cast_possible_truncation)] - self.last_sent.store(d.as_nanos() as u64, Ordering::SeqCst); - } - - /// Returns the local preference for this candidate. - pub fn local_preference(&self) -> u16 { - if self.network_type().is_tcp() { - // RFC 6544, section 4.2 - // - // In Section 4.1.2.1 of [RFC5245], a recommended formula for UDP ICE - // candidate prioritization is defined. For TCP candidates, the same - // formula and candidate type preferences SHOULD be used, and the - // RECOMMENDED type preferences for the new candidate types defined in - // this document (see Section 5) are 105 for NAT-assisted candidates and - // 75 for UDP-tunneled candidates. - // - // (...) - // - // With TCP candidates, the local preference part of the recommended - // priority formula is updated to also include the directionality - // (active, passive, or simultaneous-open) of the TCP connection. The - // RECOMMENDED local preference is then defined as: - // - // local preference = (2^13) * direction-pref + other-pref - // - // The direction-pref MUST be between 0 and 7 (both inclusive), with 7 - // being the most preferred. The other-pref MUST be between 0 and 8191 - // (both inclusive), with 8191 being the most preferred. It is - // RECOMMENDED that the host, UDP-tunneled, and relayed TCP candidates - // have the direction-pref assigned as follows: 6 for active, 4 for - // passive, and 2 for S-O. For the NAT-assisted and server reflexive - // candidates, the RECOMMENDED values are: 6 for S-O, 4 for active, and - // 2 for passive. - // - // (...) - // - // If any two candidates have the same type-preference and direction- - // pref, they MUST have a unique other-pref. With this specification, - // this usually only happens with multi-homed hosts, in which case - // other-pref is the preference for the particular IP address from which - // the candidate was obtained. When there is only a single IP address, - // this value SHOULD be set to the maximum allowed value (8191). - let other_pref: u16 = 8191; - - let direction_pref: u16 = match self.candidate_type() { - CandidateType::Host | CandidateType::Relay => match self.tcp_type() { - TcpType::Active => 6, - TcpType::Passive => 4, - TcpType::SimultaneousOpen => 2, - TcpType::Unspecified => 0, - }, - CandidateType::PeerReflexive | CandidateType::ServerReflexive => { - match self.tcp_type() { - TcpType::SimultaneousOpen => 6, - TcpType::Active => 4, - TcpType::Passive => 2, - TcpType::Unspecified => 0, - } - } - CandidateType::Unspecified => 0, - }; - - (1 << 13) * direction_pref + other_pref - } else { - DEFAULT_LOCAL_PREFERENCE - } - } -} - -/// Creates a Candidate from its string representation. -pub fn unmarshal_candidate(raw: &str) -> Result { - let split: Vec<&str> = raw.split_whitespace().collect(); - if split.len() < 8 { - return Err(Error::Other(format!( - "{:?} ({})", - Error::ErrAttributeTooShortIceCandidate, - split.len() - ))); - } - - // Foundation - let foundation = split[0].to_owned(); - - // Component - let component: u16 = split[1].parse()?; - - // Network - let network = split[2].to_owned(); - - // Priority - let priority: u32 = split[3].parse()?; - - // Address - let address = split[4].to_owned(); - - // Port - let port: u16 = split[5].parse()?; - - let typ = split[7]; - - let mut rel_addr = String::new(); - let mut rel_port = 0; - let mut tcp_type = TcpType::Unspecified; - - if split.len() > 8 { - let split2 = &split[8..]; - - if split2[0] == "raddr" { - if split2.len() < 4 { - return Err(Error::Other(format!( - "{:?}: incorrect length", - Error::ErrParseRelatedAddr - ))); - } - - // RelatedAddress - split2[1].clone_into(&mut rel_addr); - - // RelatedPort - rel_port = split2[3].parse()?; - } else if split2[0] == "tcptype" { - if split2.len() < 2 { - return Err(Error::Other(format!( - "{:?}: incorrect length", - Error::ErrParseType - ))); - } - - tcp_type = TcpType::from(split2[1]); - } - } - - match typ { - "host" => { - let config = CandidateHostConfig { - base_config: CandidateBaseConfig { - network, - address, - port, - component, - priority, - foundation, - ..CandidateBaseConfig::default() - }, - tcp_type, - }; - config.new_candidate_host() - } - "srflx" => { - let config = CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network, - address, - port, - component, - priority, - foundation, - ..CandidateBaseConfig::default() - }, - rel_addr, - rel_port, - }; - config.new_candidate_server_reflexive() - } - "prflx" => { - let config = CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network, - address, - port, - component, - priority, - foundation, - ..CandidateBaseConfig::default() - }, - rel_addr, - rel_port, - }; - - config.new_candidate_peer_reflexive() - } - "relay" => { - let config = CandidateRelayConfig { - base_config: CandidateBaseConfig { - network, - address, - port, - component, - priority, - foundation, - ..CandidateBaseConfig::default() - }, - rel_addr, - rel_port, - ..CandidateRelayConfig::default() - }; - config.new_candidate_relay() - } - _ => Err(Error::Other(format!( - "{:?} ({})", - Error::ErrUnknownCandidateType, - typ - ))), - } -} diff --git a/ice/src/candidate/candidate_host.rs b/ice/src/candidate/candidate_host.rs deleted file mode 100644 index 6cc2441ba..000000000 --- a/ice/src/candidate/candidate_host.rs +++ /dev/null @@ -1,45 +0,0 @@ -use portable_atomic::{AtomicU16, AtomicU8}; - -use super::candidate_base::*; -use super::*; -use crate::rand::generate_cand_id; - -/// The config required to create a new `CandidateHost`. -#[derive(Default)] -pub struct CandidateHostConfig { - pub base_config: CandidateBaseConfig, - - pub tcp_type: TcpType, -} - -impl CandidateHostConfig { - /// Creates a new host candidate. - pub fn new_candidate_host(self) -> Result { - let mut candidate_id = self.base_config.candidate_id; - if candidate_id.is_empty() { - candidate_id = generate_cand_id(); - } - - let c = CandidateBase { - id: candidate_id, - address: self.base_config.address.clone(), - candidate_type: CandidateType::Host, - component: AtomicU16::new(self.base_config.component), - port: self.base_config.port, - tcp_type: self.tcp_type, - foundation_override: self.base_config.foundation, - priority_override: self.base_config.priority, - network: self.base_config.network, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - conn: self.base_config.conn, - ..CandidateBase::default() - }; - - if !self.base_config.address.ends_with(".local") { - let ip = self.base_config.address.parse()?; - c.set_ip(&ip)?; - }; - - Ok(c) - } -} diff --git a/ice/src/candidate/candidate_pair_test.rs b/ice/src/candidate/candidate_pair_test.rs deleted file mode 100644 index 7b2765a82..000000000 --- a/ice/src/candidate/candidate_pair_test.rs +++ /dev/null @@ -1,155 +0,0 @@ -use super::*; -use crate::candidate::candidate_host::CandidateHostConfig; -use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; -use crate::candidate::candidate_relay::CandidateRelayConfig; -use crate::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; - -pub(crate) fn host_candidate() -> Result { - CandidateHostConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "0.0.0.0".to_owned(), - component: COMPONENT_RTP, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_host() -} - -pub(crate) fn prflx_candidate() -> Result { - CandidatePeerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "0.0.0.0".to_owned(), - component: COMPONENT_RTP, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_peer_reflexive() -} - -pub(crate) fn srflx_candidate() -> Result { - CandidateServerReflexiveConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "0.0.0.0".to_owned(), - component: COMPONENT_RTP, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_server_reflexive() -} - -pub(crate) fn relay_candidate() -> Result { - CandidateRelayConfig { - base_config: CandidateBaseConfig { - network: "udp".to_owned(), - address: "0.0.0.0".to_owned(), - component: COMPONENT_RTP, - ..Default::default() - }, - ..Default::default() - } - .new_candidate_relay() -} - -#[test] -fn test_candidate_pair_priority() -> Result<()> { - let tests = vec![ - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(host_candidate()?), - false, - ), - 9151314440652587007, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(host_candidate()?), - true, - ), - 9151314440652587007, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(prflx_candidate()?), - true, - ), - 7998392936314175488, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(prflx_candidate()?), - false, - ), - 7998392936314175487, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(srflx_candidate()?), - true, - ), - 7277816996102668288, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(srflx_candidate()?), - false, - ), - 7277816996102668287, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(relay_candidate()?), - true, - ), - 72057593987596288, - ), - ( - CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(relay_candidate()?), - false, - ), - 72057593987596287, - ), - ]; - - for (pair, want) in tests { - let got = pair.priority(); - assert_eq!( - got, want, - "CandidatePair({pair}).Priority() = {got}, want {want}" - ); - } - - Ok(()) -} - -#[test] -fn test_candidate_pair_equality() -> Result<()> { - let pair_a = CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(srflx_candidate()?), - true, - ); - let pair_b = CandidatePair::new( - Arc::new(host_candidate()?), - Arc::new(srflx_candidate()?), - false, - ); - - assert_eq!(pair_a, pair_b, "Expected {pair_a} to equal {pair_b}"); - - Ok(()) -} diff --git a/ice/src/candidate/candidate_peer_reflexive.rs b/ice/src/candidate/candidate_peer_reflexive.rs deleted file mode 100644 index dbb0b7d27..000000000 --- a/ice/src/candidate/candidate_peer_reflexive.rs +++ /dev/null @@ -1,54 +0,0 @@ -use portable_atomic::{AtomicU16, AtomicU8}; - -use util::sync::Mutex as SyncMutex; - -use super::candidate_base::*; -use super::*; -use crate::error::*; -use crate::rand::generate_cand_id; -use crate::util::*; - -/// The config required to create a new `CandidatePeerReflexive`. -#[derive(Default)] -pub struct CandidatePeerReflexiveConfig { - pub base_config: CandidateBaseConfig, - - pub rel_addr: String, - pub rel_port: u16, -} - -impl CandidatePeerReflexiveConfig { - /// Creates a new peer reflective candidate. - pub fn new_candidate_peer_reflexive(self) -> Result { - let ip: IpAddr = match self.base_config.address.parse() { - Ok(ip) => ip, - Err(_) => return Err(Error::ErrAddressParseFailed), - }; - let network_type = determine_network_type(&self.base_config.network, &ip)?; - - let mut candidate_id = self.base_config.candidate_id; - if candidate_id.is_empty() { - candidate_id = generate_cand_id(); - } - - let c = CandidateBase { - id: candidate_id, - network_type: AtomicU8::new(network_type as u8), - candidate_type: CandidateType::PeerReflexive, - address: self.base_config.address, - port: self.base_config.port, - resolved_addr: SyncMutex::new(create_addr(network_type, ip, self.base_config.port)), - component: AtomicU16::new(self.base_config.component), - foundation_override: self.base_config.foundation, - priority_override: self.base_config.priority, - related_address: Some(CandidateRelatedAddress { - address: self.rel_addr, - port: self.rel_port, - }), - conn: self.base_config.conn, - ..CandidateBase::default() - }; - - Ok(c) - } -} diff --git a/ice/src/candidate/candidate_relay.rs b/ice/src/candidate/candidate_relay.rs deleted file mode 100644 index 5a4548412..000000000 --- a/ice/src/candidate/candidate_relay.rs +++ /dev/null @@ -1,57 +0,0 @@ -use portable_atomic::{AtomicU16, AtomicU8}; -use std::sync::Arc; - -use util::sync::Mutex as SyncMutex; - -use super::candidate_base::*; -use super::*; -use crate::error::*; -use crate::rand::generate_cand_id; -use crate::util::*; - -/// The config required to create a new `CandidateRelay`. -#[derive(Default)] -pub struct CandidateRelayConfig { - pub base_config: CandidateBaseConfig, - - pub rel_addr: String, - pub rel_port: u16, - pub relay_client: Option>, -} - -impl CandidateRelayConfig { - /// Creates a new relay candidate. - pub fn new_candidate_relay(self) -> Result { - let mut candidate_id = self.base_config.candidate_id; - if candidate_id.is_empty() { - candidate_id = generate_cand_id(); - } - - let ip: IpAddr = match self.base_config.address.parse() { - Ok(ip) => ip, - Err(_) => return Err(Error::ErrAddressParseFailed), - }; - let network_type = determine_network_type(&self.base_config.network, &ip)?; - - let c = CandidateBase { - id: candidate_id, - network_type: AtomicU8::new(network_type as u8), - candidate_type: CandidateType::Relay, - address: self.base_config.address, - port: self.base_config.port, - resolved_addr: SyncMutex::new(create_addr(network_type, ip, self.base_config.port)), - component: AtomicU16::new(self.base_config.component), - foundation_override: self.base_config.foundation, - priority_override: self.base_config.priority, - related_address: Some(CandidateRelatedAddress { - address: self.rel_addr, - port: self.rel_port, - }), - conn: self.base_config.conn, - relay_client: self.relay_client.clone(), - ..CandidateBase::default() - }; - - Ok(c) - } -} diff --git a/ice/src/candidate/candidate_relay_test.rs b/ice/src/candidate/candidate_relay_test.rs deleted file mode 100644 index c1fd4bfd0..000000000 --- a/ice/src/candidate/candidate_relay_test.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::result::Result; -use std::time::Duration; - -use tokio::net::UdpSocket; -use turn::auth::AuthHandler; - -use super::*; -use crate::agent::agent_config::AgentConfig; -use crate::agent::agent_vnet_test::{connect_with_vnet, on_connected}; -use crate::agent::Agent; -use crate::error::Error; -use crate::url::{ProtoType, SchemeType, Url}; - -pub(crate) struct OptimisticAuthHandler; - -impl AuthHandler for OptimisticAuthHandler { - fn auth_handle( - &self, - _username: &str, - _realm: &str, - _src_addr: SocketAddr, - ) -> Result, turn::Error> { - Ok(turn::auth::generate_auth_key( - "username", - "webrtc.rs", - "password", - )) - } -} - -//use std::io::Write; - -#[tokio::test] -async fn test_relay_only_connection() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let server_listener = Arc::new(UdpSocket::bind("127.0.0.1:0").await?); - let server_port = server_listener.local_addr()?.port(); - - let server = turn::server::Server::new(turn::server::config::ServerConfig { - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(OptimisticAuthHandler {}), - conn_configs: vec![turn::server::config::ConnConfig { - conn: server_listener, - relay_addr_generator: Box::new(turn::relay::relay_none::RelayAddressGeneratorNone { - address: "127.0.0.1".to_owned(), - net: Arc::new(util::vnet::net::Net::new(None)), - }), - }], - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - let cfg0 = AgentConfig { - network_types: supported_network_types(), - urls: vec![Url { - scheme: SchemeType::Turn, - host: "127.0.0.1".to_owned(), - username: "username".to_owned(), - password: "password".to_owned(), - port: server_port, - proto: ProtoType::Udp, - }], - candidate_types: vec![CandidateType::Relay], - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let (a_notifier, mut a_connected) = on_connected(); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - network_types: supported_network_types(), - urls: vec![Url { - scheme: SchemeType::Turn, - host: "127.0.0.1".to_owned(), - username: "username".to_owned(), - password: "password".to_owned(), - port: server_port, - proto: ProtoType::Udp, - }], - candidate_types: vec![CandidateType::Relay], - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - let (b_notifier, mut b_connected) = on_connected(); - b_agent.on_connection_state_change(b_notifier); - - connect_with_vnet(&a_agent, &b_agent).await?; - - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - a_agent.close().await?; - b_agent.close().await?; - server.close().await?; - - Ok(()) -} diff --git a/ice/src/candidate/candidate_server_reflexive.rs b/ice/src/candidate/candidate_server_reflexive.rs deleted file mode 100644 index c8e9133bb..000000000 --- a/ice/src/candidate/candidate_server_reflexive.rs +++ /dev/null @@ -1,54 +0,0 @@ -use portable_atomic::{AtomicU16, AtomicU8}; - -use util::sync::Mutex as SyncMutex; - -use super::candidate_base::*; -use super::*; -use crate::error::*; -use crate::rand::generate_cand_id; -use crate::util::*; - -/// The config required to create a new `CandidateServerReflexive`. -#[derive(Default)] -pub struct CandidateServerReflexiveConfig { - pub base_config: CandidateBaseConfig, - - pub rel_addr: String, - pub rel_port: u16, -} - -impl CandidateServerReflexiveConfig { - /// Creates a new server reflective candidate. - pub fn new_candidate_server_reflexive(self) -> Result { - let ip: IpAddr = match self.base_config.address.parse() { - Ok(ip) => ip, - Err(_) => return Err(Error::ErrAddressParseFailed), - }; - let network_type = determine_network_type(&self.base_config.network, &ip)?; - - let mut candidate_id = self.base_config.candidate_id; - if candidate_id.is_empty() { - candidate_id = generate_cand_id(); - } - - let c = CandidateBase { - id: candidate_id, - network_type: AtomicU8::new(network_type as u8), - candidate_type: CandidateType::ServerReflexive, - address: self.base_config.address, - port: self.base_config.port, - resolved_addr: SyncMutex::new(create_addr(network_type, ip, self.base_config.port)), - component: AtomicU16::new(self.base_config.component), - foundation_override: self.base_config.foundation, - priority_override: self.base_config.priority, - related_address: Some(CandidateRelatedAddress { - address: self.rel_addr, - port: self.rel_port, - }), - conn: self.base_config.conn, - ..CandidateBase::default() - }; - - Ok(c) - } -} diff --git a/ice/src/candidate/candidate_server_reflexive_test.rs b/ice/src/candidate/candidate_server_reflexive_test.rs deleted file mode 100644 index ca40de1a7..000000000 --- a/ice/src/candidate/candidate_server_reflexive_test.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::time::Duration; - -use tokio::net::UdpSocket; - -use super::candidate_relay_test::OptimisticAuthHandler; -use super::*; -use crate::agent::agent_config::AgentConfig; -use crate::agent::agent_vnet_test::{connect_with_vnet, on_connected}; -use crate::agent::Agent; -use crate::url::{SchemeType, Url}; - -//use std::io::Write; - -#[tokio::test] -async fn test_server_reflexive_only_connection() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let server_listener = Arc::new(UdpSocket::bind("127.0.0.1:0").await?); - let server_port = server_listener.local_addr()?.port(); - - let server = turn::server::Server::new(turn::server::config::ServerConfig { - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(OptimisticAuthHandler {}), - conn_configs: vec![turn::server::config::ConnConfig { - conn: server_listener, - relay_addr_generator: Box::new(turn::relay::relay_none::RelayAddressGeneratorNone { - address: "127.0.0.1".to_owned(), - net: Arc::new(util::vnet::net::Net::new(None)), - }), - }], - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - let cfg0 = AgentConfig { - network_types: vec![NetworkType::Udp4], - urls: vec![Url { - scheme: SchemeType::Stun, - host: "127.0.0.1".to_owned(), - port: server_port, - ..Default::default() - }], - candidate_types: vec![CandidateType::ServerReflexive], - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let (a_notifier, mut a_connected) = on_connected(); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - network_types: vec![NetworkType::Udp4], - urls: vec![Url { - scheme: SchemeType::Stun, - host: "127.0.0.1".to_owned(), - port: server_port, - ..Default::default() - }], - candidate_types: vec![CandidateType::ServerReflexive], - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - let (b_notifier, mut b_connected) = on_connected(); - b_agent.on_connection_state_change(b_notifier); - - connect_with_vnet(&a_agent, &b_agent).await?; - - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - a_agent.close().await?; - b_agent.close().await?; - server.close().await?; - - Ok(()) -} diff --git a/ice/src/candidate/candidate_test.rs b/ice/src/candidate/candidate_test.rs deleted file mode 100644 index b9f2928c7..000000000 --- a/ice/src/candidate/candidate_test.rs +++ /dev/null @@ -1,411 +0,0 @@ -use std::time::UNIX_EPOCH; - -use super::*; - -#[test] -fn test_candidate_priority() -> Result<()> { - let tests = vec![ - ( - CandidateBase { - candidate_type: CandidateType::Host, - component: AtomicU16::new(COMPONENT_RTP), - ..Default::default() - }, - 2130706431, - ), - ( - CandidateBase { - candidate_type: CandidateType::Host, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp4 as u8), - tcp_type: TcpType::Active, - ..Default::default() - }, - 2128609279, - ), - ( - CandidateBase { - candidate_type: CandidateType::Host, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp4 as u8), - tcp_type: TcpType::Passive, - ..Default::default() - }, - 2124414975, - ), - ( - CandidateBase { - candidate_type: CandidateType::Host, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp4 as u8), - tcp_type: TcpType::SimultaneousOpen, - ..Default::default() - }, - 2120220671, - ), - ( - CandidateBase { - candidate_type: CandidateType::PeerReflexive, - component: AtomicU16::new(COMPONENT_RTP), - ..Default::default() - }, - 1862270975, - ), - ( - CandidateBase { - candidate_type: CandidateType::PeerReflexive, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp6 as u8), - tcp_type: TcpType::SimultaneousOpen, - ..Default::default() - }, - 1860173823, - ), - ( - CandidateBase { - candidate_type: CandidateType::PeerReflexive, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp6 as u8), - tcp_type: TcpType::Active, - ..Default::default() - }, - 1855979519, - ), - ( - CandidateBase { - candidate_type: CandidateType::PeerReflexive, - component: AtomicU16::new(COMPONENT_RTP), - network_type: AtomicU8::new(NetworkType::Tcp6 as u8), - tcp_type: TcpType::Passive, - ..Default::default() - }, - 1851785215, - ), - ( - CandidateBase { - candidate_type: CandidateType::ServerReflexive, - component: AtomicU16::new(COMPONENT_RTP), - ..Default::default() - }, - 1694498815, - ), - ( - CandidateBase { - candidate_type: CandidateType::Relay, - component: AtomicU16::new(COMPONENT_RTP), - ..Default::default() - }, - 16777215, - ), - ]; - - for (candidate, want) in tests { - let got = candidate.priority(); - assert_eq!( - got, want, - "Candidate({candidate}).Priority() = {got}, want {want}" - ); - } - - Ok(()) -} - -#[test] -fn test_candidate_last_sent() -> Result<()> { - let candidate = CandidateBase::default(); - assert_eq!(candidate.last_sent(), UNIX_EPOCH); - - let now = SystemTime::now(); - let d = now.duration_since(UNIX_EPOCH)?; - candidate.set_last_sent(d); - assert_eq!(candidate.last_sent(), now); - - Ok(()) -} - -#[test] -fn test_candidate_last_received() -> Result<()> { - let candidate = CandidateBase::default(); - assert_eq!(candidate.last_received(), UNIX_EPOCH); - - let now = SystemTime::now(); - let d = now.duration_since(UNIX_EPOCH)?; - candidate.set_last_received(d); - assert_eq!(candidate.last_received(), now); - - Ok(()) -} - -#[test] -fn test_candidate_foundation() -> Result<()> { - // All fields are the same - assert_eq!( - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation() - ); - - // Different Address - assert_ne!( - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "B".to_owned(), - ..Default::default() - }) - .foundation(), - ); - - // Different networkType - assert_ne!( - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp6 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - ); - - // Different candidateType - assert_ne!( - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - (CandidateBase { - candidate_type: CandidateType::PeerReflexive, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - ..Default::default() - }) - .foundation(), - ); - - // Port has no effect - assert_eq!( - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - port: 8080, - ..Default::default() - }) - .foundation(), - (CandidateBase { - candidate_type: CandidateType::Host, - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - address: "A".to_owned(), - port: 80, - ..Default::default() - }) - .foundation() - ); - - Ok(()) -} - -#[test] -fn test_candidate_pair_state_serialization() { - let tests = vec![ - (CandidatePairState::Unspecified, "\"unspecified\""), - (CandidatePairState::Waiting, "\"waiting\""), - (CandidatePairState::InProgress, "\"in-progress\""), - (CandidatePairState::Failed, "\"failed\""), - (CandidatePairState::Succeeded, "\"succeeded\""), - ]; - - for (candidate_pair_state, expected_string) in tests { - assert_eq!( - expected_string.to_string(), - serde_json::to_string(&candidate_pair_state).unwrap() - ); - } -} - -#[test] -fn test_candidate_pair_state_to_string() { - let tests = vec![ - (CandidatePairState::Unspecified, "unspecified"), - (CandidatePairState::Waiting, "waiting"), - (CandidatePairState::InProgress, "in-progress"), - (CandidatePairState::Failed, "failed"), - (CandidatePairState::Succeeded, "succeeded"), - ]; - - for (candidate_pair_state, expected_string) in tests { - assert_eq!(candidate_pair_state.to_string(), expected_string); - } -} - -#[test] -fn test_candidate_type_serialization() { - let tests = vec![ - (CandidateType::Unspecified, "\"unspecified\""), - (CandidateType::Host, "\"host\""), - (CandidateType::ServerReflexive, "\"srflx\""), - (CandidateType::PeerReflexive, "\"prflx\""), - (CandidateType::Relay, "\"relay\""), - ]; - - for (candidate_type, expected_string) in tests { - assert_eq!( - serde_json::to_string(&candidate_type).unwrap(), - expected_string.to_string() - ); - } -} - -#[test] -fn test_candidate_type_to_string() { - let tests = vec![ - (CandidateType::Unspecified, "Unknown candidate type"), - (CandidateType::Host, "host"), - (CandidateType::ServerReflexive, "srflx"), - (CandidateType::PeerReflexive, "prflx"), - (CandidateType::Relay, "relay"), - ]; - - for (candidate_type, expected_string) in tests { - assert_eq!(candidate_type.to_string(), expected_string); - } -} - -#[test] -fn test_candidate_marshal() -> Result<()> { - let tests = vec![ - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Udp6 as u8), - candidate_type: CandidateType::Host, - address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a".to_owned(), - port: 53987, - priority_override: 500, - foundation_override: "750".to_owned(), - ..Default::default() - }), - "750 1 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host", - ), - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - candidate_type: CandidateType::Host, - address: "10.0.75.1".to_owned(), - port: 53634, - ..Default::default() - }), - "4273957277 1 udp 2130706431 10.0.75.1 53634 typ host", - ), - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - candidate_type: CandidateType::ServerReflexive, - address: "191.228.238.68".to_owned(), - port: 53991, - related_address: Some(CandidateRelatedAddress{ - address: "192.168.0.274".to_owned(), - port:53991 - }), - ..Default::default() - }), - "647372371 1 udp 1694498815 191.228.238.68 53991 typ srflx raddr 192.168.0.274 rport 53991", - ), - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - candidate_type: CandidateType::Relay, - address: "50.0.0.1".to_owned(), - port: 5000, - related_address: Some( - CandidateRelatedAddress{ - address: "192.168.0.1".to_owned(), - port:5001} - ), - ..Default::default() - }), - "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 5001", - ), - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Tcp4 as u8), - candidate_type: CandidateType::Host, - address: "192.168.0.196".to_owned(), - port: 0, - tcp_type: TcpType::Active, - ..Default::default() - }), - "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active", - ), - ( - Some(CandidateBase{ - network_type: AtomicU8::new(NetworkType::Udp4 as u8), - candidate_type: CandidateType::Host, - address: "e2494022-4d9a-4c1e-a750-cc48d4f8d6ee.local".to_owned(), - port: 60542, - ..Default::default() - }), - "1380287402 1 udp 2130706431 e2494022-4d9a-4c1e-a750-cc48d4f8d6ee.local 60542 typ host", - ), - // Invalid candidates - (None, ""), - (None, "1938809241"), - (None, "1986380506 99999999 udp 2122063615 10.0.75.1 53634 typ host generation 0 network-id 2"), - (None, "1986380506 1 udp 99999999999 10.0.75.1 53634 typ host"), - (None, "4207374051 1 udp 1685790463 191.228.238.68 99999999 typ srflx raddr 192.168.0.278 rport 53991 generation 0 network-id 3"), - (None, "4207374051 1 udp 1685790463 191.228.238.68 53991 typ srflx raddr"), - (None, "4207374051 1 udp 1685790463 191.228.238.68 53991 typ srflx raddr 192.168.0.278 rport 99999999 generation 0 network-id 3"), - (None, "4207374051 INVALID udp 2130706431 10.0.75.1 53634 typ host"), - (None, "4207374051 1 udp INVALID 10.0.75.1 53634 typ host"), - (None, "4207374051 INVALID udp 2130706431 10.0.75.1 INVALID typ host"), - (None, "4207374051 1 udp 2130706431 10.0.75.1 53634 typ INVALID"), - ]; - - for (candidate, marshaled) in tests { - let actual_candidate = unmarshal_candidate(marshaled); - if let Some(candidate) = candidate { - if let Ok(actual_candidate) = actual_candidate { - assert!( - candidate.equal(&actual_candidate), - "{} vs {}", - candidate.marshal(), - marshaled - ); - assert_eq!(marshaled, actual_candidate.marshal()); - } else { - panic!("expected ok"); - } - } else { - assert!(actual_candidate.is_err(), "expected error"); - } - } - - Ok(()) -} diff --git a/ice/src/candidate/mod.rs b/ice/src/candidate/mod.rs deleted file mode 100644 index d764d44c7..000000000 --- a/ice/src/candidate/mod.rs +++ /dev/null @@ -1,325 +0,0 @@ -#[cfg(test)] -mod candidate_pair_test; -#[cfg(test)] -mod candidate_relay_test; -#[cfg(test)] -mod candidate_server_reflexive_test; -#[cfg(test)] -mod candidate_test; - -pub mod candidate_base; -pub mod candidate_host; -pub mod candidate_peer_reflexive; -pub mod candidate_relay; -pub mod candidate_server_reflexive; - -use std::fmt; -use std::net::{IpAddr, SocketAddr}; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use async_trait::async_trait; -use candidate_base::*; -use portable_atomic::{AtomicBool, AtomicU16, AtomicU8}; -use serde::{Deserialize, Serialize}; -use tokio::sync::{broadcast, Mutex}; - -use crate::error::Result; -use crate::network_type::*; -use crate::tcp_type::*; - -pub(crate) const RECEIVE_MTU: usize = 8192; -pub(crate) const DEFAULT_LOCAL_PREFERENCE: u16 = 65535; - -/// Indicates that the candidate is used for RTP. -pub(crate) const COMPONENT_RTP: u16 = 1; -/// Indicates that the candidate is used for RTCP. -pub(crate) const COMPONENT_RTCP: u16 = 0; - -/// Candidate represents an ICE candidate -#[async_trait] -pub trait Candidate: fmt::Display { - /// An arbitrary string used in the freezing algorithm to - /// group similar candidates. It is the same for two candidates that - /// have the same type, base IP address, protocol (UDP, TCP, etc.), - /// and STUN or TURN server. - fn foundation(&self) -> String; - - /// A unique identifier for just this candidate - /// Unlike the foundation this is different for each candidate. - fn id(&self) -> String; - - /// A component is a piece of a data stream. - /// An example is one for RTP, and one for RTCP - fn component(&self) -> u16; - fn set_component(&self, c: u16); - - /// The last time this candidate received traffic - fn last_received(&self) -> SystemTime; - - /// The last time this candidate sent traffic - fn last_sent(&self) -> SystemTime; - - fn network_type(&self) -> NetworkType; - fn address(&self) -> String; - fn port(&self) -> u16; - - fn priority(&self) -> u32; - - /// A transport address related to candidate, - /// which is useful for diagnostics and other purposes. - fn related_address(&self) -> Option; - - fn candidate_type(&self) -> CandidateType; - fn tcp_type(&self) -> TcpType; - - fn marshal(&self) -> String; - - fn addr(&self) -> SocketAddr; - - async fn close(&self) -> Result<()>; - fn seen(&self, outbound: bool); - - async fn write_to(&self, raw: &[u8], dst: &(dyn Candidate + Send + Sync)) -> Result; - fn equal(&self, other: &dyn Candidate) -> bool; - fn set_ip(&self, ip: &IpAddr) -> Result<()>; - fn get_conn(&self) -> Option<&Arc>; - fn get_closed_ch(&self) -> Arc>>>; -} - -/// Represents the type of candidate `CandidateType` enum. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CandidateType { - #[serde(rename = "unspecified")] - Unspecified, - #[serde(rename = "host")] - Host, - #[serde(rename = "srflx")] - ServerReflexive, - #[serde(rename = "prflx")] - PeerReflexive, - #[serde(rename = "relay")] - Relay, -} - -// String makes CandidateType printable -impl fmt::Display for CandidateType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - CandidateType::Host => "host", - CandidateType::ServerReflexive => "srflx", - CandidateType::PeerReflexive => "prflx", - CandidateType::Relay => "relay", - CandidateType::Unspecified => "Unknown candidate type", - }; - write!(f, "{s}") - } -} - -impl Default for CandidateType { - fn default() -> Self { - Self::Unspecified - } -} - -impl CandidateType { - /// Returns the preference weight of a `CandidateType`. - /// - /// 4.1.2.2. Guidelines for Choosing Type and Local Preferences - /// The RECOMMENDED values are 126 for host candidates, 100 - /// for server reflexive candidates, 110 for peer reflexive candidates, - /// and 0 for relayed candidates. - #[must_use] - pub const fn preference(self) -> u16 { - match self { - Self::Host => 126, - Self::PeerReflexive => 110, - Self::ServerReflexive => 100, - Self::Relay | CandidateType::Unspecified => 0, - } - } -} - -pub(crate) fn contains_candidate_type( - candidate_type: CandidateType, - candidate_type_list: &[CandidateType], -) -> bool { - if candidate_type_list.is_empty() { - return false; - } - for ct in candidate_type_list { - if *ct == candidate_type { - return true; - } - } - false -} - -/// Convey transport addresses related to the candidate, useful for diagnostics and other purposes. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct CandidateRelatedAddress { - pub address: String, - pub port: u16, -} - -// String makes CandidateRelatedAddress printable -impl fmt::Display for CandidateRelatedAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, " related {}:{}", self.address, self.port) - } -} - -/// Represent the ICE candidate pair state. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CandidatePairState { - #[serde(rename = "unspecified")] - Unspecified = 0, - - /// Means a check has not been performed for this pair. - #[serde(rename = "waiting")] - Waiting = 1, - - /// Means a check has been sent for this pair, but the transaction is in progress. - #[serde(rename = "in-progress")] - InProgress = 2, - - /// Means a check for this pair was already done and failed, either never producing any response - /// or producing an unrecoverable failure response. - #[serde(rename = "failed")] - Failed = 3, - - /// Means a check for this pair was already done and produced a successful result. - #[serde(rename = "succeeded")] - Succeeded = 4, -} - -impl From for CandidatePairState { - fn from(v: u8) -> Self { - match v { - 1 => Self::Waiting, - 2 => Self::InProgress, - 3 => Self::Failed, - 4 => Self::Succeeded, - _ => Self::Unspecified, - } - } -} - -impl Default for CandidatePairState { - fn default() -> Self { - Self::Unspecified - } -} - -impl fmt::Display for CandidatePairState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Waiting => "waiting", - Self::InProgress => "in-progress", - Self::Failed => "failed", - Self::Succeeded => "succeeded", - Self::Unspecified => "unspecified", - }; - - write!(f, "{s}") - } -} - -/// Represents a combination of a local and remote candidate. -pub struct CandidatePair { - pub(crate) ice_role_controlling: AtomicBool, - pub remote: Arc, - pub local: Arc, - pub(crate) binding_request_count: AtomicU16, - pub(crate) state: AtomicU8, // convert it to CandidatePairState, - pub(crate) nominated: AtomicBool, -} - -impl Default for CandidatePair { - fn default() -> Self { - Self { - ice_role_controlling: AtomicBool::new(false), - remote: Arc::new(CandidateBase::default()), - local: Arc::new(CandidateBase::default()), - state: AtomicU8::new(CandidatePairState::Waiting as u8), - binding_request_count: AtomicU16::new(0), - nominated: AtomicBool::new(false), - } - } -} - -impl fmt::Debug for CandidatePair { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "prio {} (local, prio {}) {} <-> {} (remote, prio {})", - self.priority(), - self.local.priority(), - self.local, - self.remote, - self.remote.priority() - ) - } -} - -impl fmt::Display for CandidatePair { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "prio {} (local, prio {}) {} <-> {} (remote, prio {})", - self.priority(), - self.local.priority(), - self.local, - self.remote, - self.remote.priority() - ) - } -} - -impl PartialEq for CandidatePair { - fn eq(&self, other: &Self) -> bool { - self.local.equal(&*other.local) && self.remote.equal(&*other.remote) - } -} - -impl CandidatePair { - #[must_use] - pub fn new( - local: Arc, - remote: Arc, - controlling: bool, - ) -> Self { - Self { - ice_role_controlling: AtomicBool::new(controlling), - remote, - local, - state: AtomicU8::new(CandidatePairState::Waiting as u8), - binding_request_count: AtomicU16::new(0), - nominated: AtomicBool::new(false), - } - } - - /// RFC 5245 - 5.7.2. Computing Pair Priority and Ordering Pairs - /// Let G be the priority for the candidate provided by the controlling - /// agent. Let D be the priority for the candidate provided by the - /// controlled agent. - /// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) - pub fn priority(&self) -> u64 { - let (g, d) = if self.ice_role_controlling.load(Ordering::SeqCst) { - (self.local.priority(), self.remote.priority()) - } else { - (self.remote.priority(), self.local.priority()) - }; - - // 1<<32 overflows uint32; and if both g && d are - // maxUint32, this result would overflow uint64 - ((1 << 32_u64) - 1) * u64::from(std::cmp::min(g, d)) - + 2 * u64::from(std::cmp::max(g, d)) - + u64::from(g > d) - } - - pub async fn write(&self, b: &[u8]) -> Result { - self.local.write_to(b, &*self.remote).await - } -} diff --git a/ice/src/control/control_test.rs b/ice/src/control/control_test.rs deleted file mode 100644 index 480c04555..000000000 --- a/ice/src/control/control_test.rs +++ /dev/null @@ -1,168 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_controlled_get_from() -> Result<()> { - let mut m = Message::new(); - let mut c = AttrControlled(4321); - let result = c.get_from(&m); - if let Err(err) = result { - assert_eq!(stun::Error::ErrAttributeNotFound, err, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - - m.build(&[Box::new(BINDING_REQUEST), Box::new(c)])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - let mut c1 = AttrControlled::default(); - c1.get_from(&m1)?; - - assert_eq!(c1, c, "not equal"); - - //"IncorrectSize" - { - let mut m3 = Message::new(); - m3.add(ATTR_ICE_CONTROLLED, &[0; 100]); - let mut c2 = AttrControlled::default(); - let result = c2.get_from(&m3); - if let Err(err) = result { - assert!(is_attr_size_invalid(&err), "should error"); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_controlling_get_from() -> Result<()> { - let mut m = Message::new(); - let mut c = AttrControlling(4321); - let result = c.get_from(&m); - if let Err(err) = result { - assert_eq!(stun::Error::ErrAttributeNotFound, err, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - - m.build(&[Box::new(BINDING_REQUEST), Box::new(c)])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - let mut c1 = AttrControlling::default(); - c1.get_from(&m1)?; - - assert_eq!(c1, c, "not equal"); - - //"IncorrectSize" - { - let mut m3 = Message::new(); - m3.add(ATTR_ICE_CONTROLLING, &[0; 100]); - let mut c2 = AttrControlling::default(); - let result = c2.get_from(&m3); - if let Err(err) = result { - assert!(is_attr_size_invalid(&err), "should error"); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_control_get_from() -> Result<()> { - //"Blank" - { - let m = Message::new(); - let mut c = AttrControl::default(); - let result = c.get_from(&m); - if let Err(err) = result { - assert_eq!(stun::Error::ErrAttributeNotFound, err, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - } - //"Controlling" - { - let mut m = Message::new(); - let mut c = AttrControl::default(); - let result = c.get_from(&m); - if let Err(err) = result { - assert_eq!(stun::Error::ErrAttributeNotFound, err, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - - c.role = Role::Controlling; - c.tie_breaker = TieBreaker(4321); - - m.build(&[Box::new(BINDING_REQUEST), Box::new(c)])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - let mut c1 = AttrControl::default(); - c1.get_from(&m1)?; - - assert_eq!(c1, c, "not equal"); - - //"IncorrectSize" - { - let mut m3 = Message::new(); - m3.add(ATTR_ICE_CONTROLLING, &[0; 100]); - let mut c2 = AttrControl::default(); - let result = c2.get_from(&m3); - if let Err(err) = result { - assert!(is_attr_size_invalid(&err), "should error"); - } else { - panic!("expected error, but got ok"); - } - } - } - - //"Controlled" - { - let mut m = Message::new(); - let mut c = AttrControl::default(); - let result = c.get_from(&m); - if let Err(err) = result { - assert_eq!(stun::Error::ErrAttributeNotFound, err, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - - c.role = Role::Controlled; - c.tie_breaker = TieBreaker(1234); - - m.build(&[Box::new(BINDING_REQUEST), Box::new(c)])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - let mut c1 = AttrControl::default(); - c1.get_from(&m1)?; - - assert_eq!(c1, c, "not equal"); - - //"IncorrectSize" - { - let mut m3 = Message::new(); - m3.add(ATTR_ICE_CONTROLLING, &[0; 100]); - let mut c2 = AttrControl::default(); - let result = c2.get_from(&m3); - if let Err(err) = result { - assert!(is_attr_size_invalid(&err), "should error"); - } else { - panic!("expected error, but got ok"); - } - } - } - - Ok(()) -} diff --git a/ice/src/control/mod.rs b/ice/src/control/mod.rs deleted file mode 100644 index a79e170c3..000000000 --- a/ice/src/control/mod.rs +++ /dev/null @@ -1,143 +0,0 @@ -#[cfg(test)] -mod control_test; - -use std::fmt; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -/// Common helper for ICE-{CONTROLLED,CONTROLLING} and represents the so-called Tiebreaker number. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct TieBreaker(pub u64); - -pub(crate) const TIE_BREAKER_SIZE: usize = 8; // 64 bit - -impl TieBreaker { - /// Adds Tiebreaker value to m as t attribute. - pub fn add_to_as(self, m: &mut Message, t: AttrType) -> Result<(), stun::Error> { - let mut v = vec![0; TIE_BREAKER_SIZE]; - v.copy_from_slice(&self.0.to_be_bytes()); - m.add(t, &v); - Ok(()) - } - - /// Decodes Tiebreaker value in message getting it as for t type. - pub fn get_from_as(&mut self, m: &Message, t: AttrType) -> Result<(), stun::Error> { - let v = m.get(t)?; - check_size(t, v.len(), TIE_BREAKER_SIZE)?; - self.0 = u64::from_be_bytes([v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]]); - Ok(()) - } -} -/// Represents ICE-CONTROLLED attribute. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct AttrControlled(pub u64); - -impl Setter for AttrControlled { - /// Adds ICE-CONTROLLED to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - TieBreaker(self.0).add_to_as(m, ATTR_ICE_CONTROLLED) - } -} - -impl Getter for AttrControlled { - /// Decodes ICE-CONTROLLED from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let mut t = TieBreaker::default(); - t.get_from_as(m, ATTR_ICE_CONTROLLED)?; - self.0 = t.0; - Ok(()) - } -} - -/// Represents ICE-CONTROLLING attribute. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct AttrControlling(pub u64); - -impl Setter for AttrControlling { - // add_to adds ICE-CONTROLLING to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - TieBreaker(self.0).add_to_as(m, ATTR_ICE_CONTROLLING) - } -} - -impl Getter for AttrControlling { - // get_from decodes ICE-CONTROLLING from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let mut t = TieBreaker::default(); - t.get_from_as(m, ATTR_ICE_CONTROLLING)?; - self.0 = t.0; - Ok(()) - } -} - -/// Helper that wraps ICE-{CONTROLLED,CONTROLLING}. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct AttrControl { - role: Role, - tie_breaker: TieBreaker, -} - -impl Setter for AttrControl { - // add_to adds ICE-CONTROLLED or ICE-CONTROLLING attribute depending on Role. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - if self.role == Role::Controlling { - self.tie_breaker.add_to_as(m, ATTR_ICE_CONTROLLING) - } else { - self.tie_breaker.add_to_as(m, ATTR_ICE_CONTROLLED) - } - } -} - -impl Getter for AttrControl { - // get_from decodes Role and Tiebreaker value from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - if m.contains(ATTR_ICE_CONTROLLING) { - self.role = Role::Controlling; - return self.tie_breaker.get_from_as(m, ATTR_ICE_CONTROLLING); - } - if m.contains(ATTR_ICE_CONTROLLED) { - self.role = Role::Controlled; - return self.tie_breaker.get_from_as(m, ATTR_ICE_CONTROLLED); - } - - Err(stun::Error::ErrAttributeNotFound) - } -} - -/// Represents ICE agent role, which can be controlling or controlled. -/// Possible ICE agent roles. -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub enum Role { - Controlling, - Controlled, - Unspecified, -} - -impl Default for Role { - fn default() -> Self { - Self::Controlling - } -} - -impl From<&str> for Role { - fn from(raw: &str) -> Self { - match raw { - "controlling" => Self::Controlling, - "controlled" => Self::Controlled, - _ => Self::Unspecified, - } - } -} - -impl fmt::Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Controlling => "controlling", - Self::Controlled => "controlled", - Self::Unspecified => "unspecified", - }; - write!(f, "{s}") - } -} diff --git a/ice/src/error.rs b/ice/src/error.rs deleted file mode 100644 index a3f6ff84e..000000000 --- a/ice/src/error.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::num::ParseIntError; -use std::time::SystemTimeError; -use std::{io, net}; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - /// Indicates an error with Unknown info. - #[error("Unknown type")] - ErrUnknownType, - - /// Indicates the scheme type could not be parsed. - #[error("unknown scheme type")] - ErrSchemeType, - - /// Indicates query arguments are provided in a STUN URL. - #[error("queries not supported in stun address")] - ErrStunQuery, - - /// Indicates an malformed query is provided. - #[error("invalid query")] - ErrInvalidQuery, - - /// Indicates malformed hostname is provided. - #[error("invalid hostname")] - ErrHost, - - /// Indicates malformed port is provided. - #[error("invalid port number")] - ErrPort, - - /// Indicates local username fragment insufficient bits are provided. - /// Have to be at least 24 bits long. - #[error("local username fragment is less than 24 bits long")] - ErrLocalUfragInsufficientBits, - - /// Indicates local passoword insufficient bits are provided. - /// Have to be at least 128 bits long. - #[error("local password is less than 128 bits long")] - ErrLocalPwdInsufficientBits, - - /// Indicates an unsupported transport type was provided. - #[error("invalid transport protocol type")] - ErrProtoType, - - /// Indicates the agent is closed. - #[error("the agent is closed")] - ErrClosed, - - /// Indicates agent does not have a valid candidate pair. - #[error("no candidate pairs available")] - ErrNoCandidatePairs, - - /// Indicates agent connection was canceled by the caller. - #[error("connecting canceled by caller")] - ErrCanceledByCaller, - - /// Indicates agent was started twice. - #[error("attempted to start agent twice")] - ErrMultipleStart, - - /// Indicates agent was started with an empty remote ufrag. - #[error("remote ufrag is empty")] - ErrRemoteUfragEmpty, - - /// Indicates agent was started with an empty remote pwd. - #[error("remote pwd is empty")] - ErrRemotePwdEmpty, - - /// Indicates agent was started without on_candidate. - #[error("no on_candidate provided")] - ErrNoOnCandidateHandler, - - /// Indicates GatherCandidates has been called multiple times. - #[error("attempting to gather candidates during gathering state")] - ErrMultipleGatherAttempted, - - /// Indicates agent was give TURN URL with an empty Username. - #[error("username is empty")] - ErrUsernameEmpty, - - /// Indicates agent was give TURN URL with an empty Password. - #[error("password is empty")] - ErrPasswordEmpty, - - /// Indicates we were unable to parse a candidate address. - #[error("failed to parse address")] - ErrAddressParseFailed, - - /// Indicates that non host candidates were selected for a lite agent. - #[error("lite agents must only use host candidates")] - ErrLiteUsingNonHostCandidates, - - /// Indicates that one or more URL was provided to the agent but no host candidate required them. - #[error("agent does not need URL with selected candidate types")] - ErrUselessUrlsProvided, - - /// Indicates that the specified NAT1To1IPCandidateType is unsupported. - #[error("unsupported 1:1 NAT IP candidate type")] - ErrUnsupportedNat1to1IpCandidateType, - - /// Indicates that the given 1:1 NAT IP mapping is invalid. - #[error("invalid 1:1 NAT IP mapping")] - ErrInvalidNat1to1IpMapping, - - /// IPNotFound in NAT1To1IPMapping. - #[error("external mapped IP not found")] - ErrExternalMappedIpNotFound, - - /// Indicates that the mDNS gathering cannot be used along with 1:1 NAT IP mapping for host - /// candidate. - #[error("mDNS gathering cannot be used with 1:1 NAT IP mapping for host candidate")] - ErrMulticastDnsWithNat1to1IpMapping, - - /// Indicates that 1:1 NAT IP mapping for host candidate is requested, but the host candidate - /// type is disabled. - #[error("1:1 NAT IP mapping for host candidate ineffective")] - ErrIneffectiveNat1to1IpMappingHost, - - /// Indicates that 1:1 NAT IP mapping for srflx candidate is requested, but the srflx candidate - /// type is disabled. - #[error("1:1 NAT IP mapping for srflx candidate ineffective")] - ErrIneffectiveNat1to1IpMappingSrflx, - - /// Indicates an invalid MulticastDNSHostName. - #[error("invalid mDNS HostName, must end with .local and can only contain a single '.'")] - ErrInvalidMulticastDnshostName, - - /// Indicates Restart was called when Agent is in GatheringStateGathering. - #[error("ICE Agent can not be restarted when gathering")] - ErrRestartWhenGathering, - - /// Indicates a run operation was canceled by its individual done. - #[error("run was canceled by done")] - ErrRunCanceled, - - /// Initialized Indicates TCPMux is not initialized and that invalidTCPMux is used. - #[error("TCPMux is not initialized")] - ErrTcpMuxNotInitialized, - - /// Indicates we already have the connection with same remote addr. - #[error("conn with same remote addr already exists")] - ErrTcpRemoteAddrAlreadyExists, - - #[error("failed to send packet")] - ErrSendPacket, - #[error("attribute not long enough to be ICE candidate")] - ErrAttributeTooShortIceCandidate, - #[error("could not parse component")] - ErrParseComponent, - #[error("could not parse priority")] - ErrParsePriority, - #[error("could not parse port")] - ErrParsePort, - #[error("could not parse related addresses")] - ErrParseRelatedAddr, - #[error("could not parse type")] - ErrParseType, - #[error("unknown candidate type")] - ErrUnknownCandidateType, - #[error("failed to get XOR-MAPPED-ADDRESS response")] - ErrGetXorMappedAddrResponse, - #[error("connection with same remote address already exists")] - ErrConnectionAddrAlreadyExist, - #[error("error reading streaming packet")] - ErrReadingStreamingPacket, - #[error("error writing to")] - ErrWriting, - #[error("error closing connection")] - ErrClosingConnection, - #[error("unable to determine networkType")] - ErrDetermineNetworkType, - #[error("missing protocol scheme")] - ErrMissingProtocolScheme, - #[error("too many colons in address")] - ErrTooManyColonsAddr, - #[error("unexpected error trying to read")] - ErrRead, - #[error("unknown role")] - ErrUnknownRole, - #[error("username mismatch")] - ErrMismatchUsername, - #[error("the ICE conn can't write STUN messages")] - ErrIceWriteStunMessage, - #[error("invalid url")] - ErrInvalidUrl, - #[error("relative URL without a base")] - ErrUrlParse, - #[error("Candidate IP could not be found")] - ErrCandidateIpNotFound, - - #[error("parse int: {0}")] - ParseInt(#[from] ParseIntError), - #[error("parse addr: {0}")] - ParseIp(#[from] net::AddrParseError), - #[error("{0}")] - Io(#[source] IoError), - #[error("{0}")] - Util(#[from] util::Error), - #[error("{0}")] - Stun(#[from] stun::Error), - #[error("{0}")] - ParseUrl(#[from] url::ParseError), - #[error("{0}")] - Mdns(#[from] mdns::Error), - #[error("{0}")] - Turn(#[from] turn::Error), - - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -impl From for Error { - fn from(e: SystemTimeError) -> Self { - Error::Other(e.to_string()) - } -} diff --git a/ice/src/external_ip_mapper/external_ip_mapper_test.rs b/ice/src/external_ip_mapper/external_ip_mapper_test.rs deleted file mode 100644 index d9f9e5d60..000000000 --- a/ice/src/external_ip_mapper/external_ip_mapper_test.rs +++ /dev/null @@ -1,251 +0,0 @@ -use super::*; - -#[test] -fn test_external_ip_mapper_validate_ip_string() -> Result<()> { - let ip = validate_ip_string("1.2.3.4")?; - assert!(ip.is_ipv4(), "should be true"); - assert_eq!("1.2.3.4", ip.to_string(), "should be true"); - - let ip = validate_ip_string("2601:4567::5678")?; - assert!(!ip.is_ipv4(), "should be false"); - assert_eq!("2601:4567::5678", ip.to_string(), "should be true"); - - let result = validate_ip_string("bad.6.6.6"); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_new_external_ip_mapper() -> Result<()> { - // ips being empty should succeed but mapper will still be nil - let m = ExternalIpMapper::new(CandidateType::Unspecified, &[])?; - assert!(m.is_none(), "should be none"); - - // IPv4 with no explicit local IP, defaults to CandidateTypeHost - let m = ExternalIpMapper::new(CandidateType::Unspecified, &["1.2.3.4".to_owned()])?.unwrap(); - assert_eq!(m.candidate_type, CandidateType::Host, "should match"); - assert!(m.ipv4_mapping.ip_sole.is_some()); - assert!(m.ipv6_mapping.ip_sole.is_none()); - assert_eq!(m.ipv4_mapping.ip_map.len(), 0, "should match"); - assert_eq!(m.ipv6_mapping.ip_map.len(), 0, "should match"); - - // IPv4 with no explicit local IP, using CandidateTypeServerReflexive - let m = - ExternalIpMapper::new(CandidateType::ServerReflexive, &["1.2.3.4".to_owned()])?.unwrap(); - assert_eq!( - CandidateType::ServerReflexive, - m.candidate_type, - "should match" - ); - assert!(m.ipv4_mapping.ip_sole.is_some()); - assert!(m.ipv6_mapping.ip_sole.is_none()); - assert_eq!(m.ipv4_mapping.ip_map.len(), 0, "should match"); - assert_eq!(m.ipv6_mapping.ip_map.len(), 0, "should match"); - - // IPv4 with no explicit local IP, defaults to CandidateTypeHost - let m = ExternalIpMapper::new(CandidateType::Unspecified, &["2601:4567::5678".to_owned()])? - .unwrap(); - assert_eq!(m.candidate_type, CandidateType::Host, "should match"); - assert!(m.ipv4_mapping.ip_sole.is_none()); - assert!(m.ipv6_mapping.ip_sole.is_some()); - assert_eq!(m.ipv4_mapping.ip_map.len(), 0, "should match"); - assert_eq!(m.ipv6_mapping.ip_map.len(), 0, "should match"); - - // IPv4 and IPv6 in the mix - let m = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.4".to_owned(), "2601:4567::5678".to_owned()], - )? - .unwrap(); - assert_eq!(m.candidate_type, CandidateType::Host, "should match"); - assert!(m.ipv4_mapping.ip_sole.is_some()); - assert!(m.ipv6_mapping.ip_sole.is_some()); - assert_eq!(m.ipv4_mapping.ip_map.len(), 0, "should match"); - assert_eq!(m.ipv6_mapping.ip_map.len(), 0, "should match"); - - // Unsupported candidate type - CandidateTypePeerReflexive - let result = ExternalIpMapper::new(CandidateType::PeerReflexive, &["1.2.3.4".to_owned()]); - assert!(result.is_err(), "should fail"); - - // Unsupported candidate type - CandidateTypeRelay - let result = ExternalIpMapper::new(CandidateType::PeerReflexive, &["1.2.3.4".to_owned()]); - assert!(result.is_err(), "should fail"); - - // Cannot duplicate mapping IPv4 family - let result = ExternalIpMapper::new( - CandidateType::ServerReflexive, - &["1.2.3.4".to_owned(), "5.6.7.8".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - // Cannot duplicate mapping IPv6 family - let result = ExternalIpMapper::new( - CandidateType::ServerReflexive, - &["2201::1".to_owned(), "2201::0002".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - // Invalid external IP string - let result = ExternalIpMapper::new(CandidateType::ServerReflexive, &["bad.2.3.4".to_owned()]); - assert!(result.is_err(), "should fail"); - - // Invalid local IP string - let result = ExternalIpMapper::new( - CandidateType::ServerReflexive, - &["1.2.3.4/10.0.0.bad".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_new_external_ip_mapper_with_explicit_local_ip() -> Result<()> { - // IPv4 with explicit local IP, defaults to CandidateTypeHost - let m = ExternalIpMapper::new(CandidateType::Unspecified, &["1.2.3.4/10.0.0.1".to_owned()])? - .unwrap(); - assert_eq!(m.candidate_type, CandidateType::Host, "should match"); - assert!(m.ipv4_mapping.ip_sole.is_none()); - assert!(m.ipv6_mapping.ip_sole.is_none()); - assert_eq!(m.ipv4_mapping.ip_map.len(), 1, "should match"); - assert_eq!(m.ipv6_mapping.ip_map.len(), 0, "should match"); - - // Cannot assign two ext IPs for one local IPv4 - let result = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.4/10.0.0.1".to_owned(), "1.2.3.5/10.0.0.1".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - // Cannot assign two ext IPs for one local IPv6 - let result = ExternalIpMapper::new( - CandidateType::Unspecified, - &[ - "2200::1/fe80::1".to_owned(), - "2200::0002/fe80::1".to_owned(), - ], - ); - assert!(result.is_err(), "should fail"); - - // Cannot mix different IP family in a pair (1) - let result = - ExternalIpMapper::new(CandidateType::Unspecified, &["2200::1/10.0.0.1".to_owned()]); - assert!(result.is_err(), "should fail"); - - // Cannot mix different IP family in a pair (2) - let result = ExternalIpMapper::new(CandidateType::Unspecified, &["1.2.3.4/fe80::1".to_owned()]); - assert!(result.is_err(), "should fail"); - - // Invalid pair - let result = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.4/192.168.0.2/10.0.0.1".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_new_external_ip_mapper_with_implicit_local_ip() -> Result<()> { - // Mixing inpicit and explicit local IPs not allowed - let result = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.4".to_owned(), "1.2.3.5/10.0.0.1".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - // Mixing inpicit and explicit local IPs not allowed - let result = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.5/10.0.0.1".to_owned(), "1.2.3.4".to_owned()], - ); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_find_external_ip_without_explicit_local_ip() -> Result<()> { - // IPv4 with explicit local IP, defaults to CandidateTypeHost - let m = ExternalIpMapper::new( - CandidateType::Unspecified, - &["1.2.3.4".to_owned(), "2200::1".to_owned()], - )? - .unwrap(); - assert!(m.ipv4_mapping.ip_sole.is_some()); - assert!(m.ipv6_mapping.ip_sole.is_some()); - - // find external IPv4 - let ext_ip = m.find_external_ip("10.0.0.1")?; - assert_eq!(ext_ip.to_string(), "1.2.3.4", "should match"); - - // find external IPv6 - let ext_ip = m.find_external_ip("fe80::0001")?; // use '0001' instead of '1' on purpose - assert_eq!(ext_ip.to_string(), "2200::1", "should match"); - - // Bad local IP string - let result = m.find_external_ip("really.bad"); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_find_external_ip_with_explicit_local_ip() -> Result<()> { - // IPv4 with explicit local IP, defaults to CandidateTypeHost - let m = ExternalIpMapper::new( - CandidateType::Unspecified, - &[ - "1.2.3.4/10.0.0.1".to_owned(), - "1.2.3.5/10.0.0.2".to_owned(), - "2200::1/fe80::1".to_owned(), - "2200::2/fe80::2".to_owned(), - ], - )? - .unwrap(); - - // find external IPv4 - let ext_ip = m.find_external_ip("10.0.0.1")?; - assert_eq!(ext_ip.to_string(), "1.2.3.4", "should match"); - - let ext_ip = m.find_external_ip("10.0.0.2")?; - assert_eq!(ext_ip.to_string(), "1.2.3.5", "should match"); - - let result = m.find_external_ip("10.0.0.3"); - assert!(result.is_err(), "should fail"); - - // find external IPv6 - let ext_ip = m.find_external_ip("fe80::0001")?; // use '0001' instead of '1' on purpose - assert_eq!(ext_ip.to_string(), "2200::1", "should match"); - - let ext_ip = m.find_external_ip("fe80::0002")?; // use '0002' instead of '2' on purpose - assert_eq!(ext_ip.to_string(), "2200::2", "should match"); - - let result = m.find_external_ip("fe80::3"); - assert!(result.is_err(), "should fail"); - - // Bad local IP string - let result = m.find_external_ip("really.bad"); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[test] -fn test_external_ip_mapper_find_external_ip_with_empty_map() -> Result<()> { - let m = ExternalIpMapper::new(CandidateType::Unspecified, &["1.2.3.4".to_owned()])?.unwrap(); - - // attempt to find IPv6 that does not exist in the map - let result = m.find_external_ip("fe80::1"); - assert!(result.is_err(), "should fail"); - - let m = ExternalIpMapper::new(CandidateType::Unspecified, &["2200::1".to_owned()])?.unwrap(); - - // attempt to find IPv4 that does not exist in the map - let result = m.find_external_ip("10.0.0.1"); - assert!(result.is_err(), "should fail"); - - Ok(()) -} diff --git a/ice/src/external_ip_mapper/mod.rs b/ice/src/external_ip_mapper/mod.rs deleted file mode 100644 index 0d968b83f..000000000 --- a/ice/src/external_ip_mapper/mod.rs +++ /dev/null @@ -1,133 +0,0 @@ -#[cfg(test)] -mod external_ip_mapper_test; - -use std::collections::HashMap; -use std::net::IpAddr; - -use crate::candidate::*; -use crate::error::*; - -pub(crate) fn validate_ip_string(ip_str: &str) -> Result { - match ip_str.parse() { - Ok(ip) => Ok(ip), - Err(_) => Err(Error::ErrInvalidNat1to1IpMapping), - } -} - -/// Holds the mapping of local and external IP address for a particular IP family. -#[derive(Default, PartialEq, Debug)] -pub(crate) struct IpMapping { - ip_sole: Option, // when non-nil, this is the sole external IP for one local IP assumed - ip_map: HashMap, // local-to-external IP mapping (k: local, v: external) -} - -impl IpMapping { - pub(crate) fn set_sole_ip(&mut self, ip: IpAddr) -> Result<()> { - if self.ip_sole.is_some() || !self.ip_map.is_empty() { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - self.ip_sole = Some(ip); - - Ok(()) - } - - pub(crate) fn add_ip_mapping(&mut self, loc_ip: IpAddr, ext_ip: IpAddr) -> Result<()> { - if self.ip_sole.is_some() { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - let loc_ip_str = loc_ip.to_string(); - - // check if dup of local IP - if self.ip_map.contains_key(&loc_ip_str) { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - self.ip_map.insert(loc_ip_str, ext_ip); - - Ok(()) - } - - pub(crate) fn find_external_ip(&self, loc_ip: IpAddr) -> Result { - if let Some(ip_sole) = &self.ip_sole { - return Ok(*ip_sole); - } - - self.ip_map.get(&loc_ip.to_string()).map_or_else( - || Err(Error::ErrExternalMappedIpNotFound), - |ext_ip| Ok(*ext_ip), - ) - } -} - -#[derive(Default)] -pub(crate) struct ExternalIpMapper { - pub(crate) ipv4_mapping: IpMapping, - pub(crate) ipv6_mapping: IpMapping, - pub(crate) candidate_type: CandidateType, -} - -impl ExternalIpMapper { - pub(crate) fn new(mut candidate_type: CandidateType, ips: &[String]) -> Result> { - if ips.is_empty() { - return Ok(None); - } - if candidate_type == CandidateType::Unspecified { - candidate_type = CandidateType::Host; // defaults to host - } else if candidate_type != CandidateType::Host - && candidate_type != CandidateType::ServerReflexive - { - return Err(Error::ErrUnsupportedNat1to1IpCandidateType); - } - - let mut m = Self { - ipv4_mapping: IpMapping::default(), - ipv6_mapping: IpMapping::default(), - candidate_type, - }; - - for ext_ip_str in ips { - let ip_pair: Vec<&str> = ext_ip_str.split('/').collect(); - if ip_pair.is_empty() || ip_pair.len() > 2 { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - let ext_ip = validate_ip_string(ip_pair[0])?; - if ip_pair.len() == 1 { - if ext_ip.is_ipv4() { - m.ipv4_mapping.set_sole_ip(ext_ip)?; - } else { - m.ipv6_mapping.set_sole_ip(ext_ip)?; - } - } else { - let loc_ip = validate_ip_string(ip_pair[1])?; - if ext_ip.is_ipv4() { - if !loc_ip.is_ipv4() { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - m.ipv4_mapping.add_ip_mapping(loc_ip, ext_ip)?; - } else { - if loc_ip.is_ipv4() { - return Err(Error::ErrInvalidNat1to1IpMapping); - } - - m.ipv6_mapping.add_ip_mapping(loc_ip, ext_ip)?; - } - } - } - - Ok(Some(m)) - } - - pub(crate) fn find_external_ip(&self, local_ip_str: &str) -> Result { - let loc_ip = validate_ip_string(local_ip_str)?; - - if loc_ip.is_ipv4() { - self.ipv4_mapping.find_external_ip(loc_ip) - } else { - self.ipv6_mapping.find_external_ip(loc_ip) - } - } -} diff --git a/ice/src/lib.rs b/ice/src/lib.rs deleted file mode 100644 index b2e0af1a2..000000000 --- a/ice/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod agent; -pub mod candidate; -pub mod control; -mod error; -pub mod external_ip_mapper; -pub mod mdns; -pub mod network_type; -pub mod priority; -pub mod rand; -pub mod state; -pub mod stats; -pub mod tcp_type; -pub mod udp_mux; -pub mod udp_network; -pub mod url; -pub mod use_candidate; -pub mod util; - -pub use error::Error; diff --git a/ice/src/mdns/mdns_test.rs b/ice/src/mdns/mdns_test.rs deleted file mode 100644 index 604010390..000000000 --- a/ice/src/mdns/mdns_test.rs +++ /dev/null @@ -1,151 +0,0 @@ -use regex::Regex; -use tokio::sync::{mpsc, Mutex}; - -use super::*; -use crate::agent::agent_config::*; -use crate::agent::agent_vnet_test::*; -use crate::agent::*; -use crate::candidate::*; -use crate::error::Error; -use crate::network_type::*; - -#[tokio::test] -// This test is disabled on Windows for now because it gets stuck and never finishes. -// This does not seem to have happened due to a code change. It started happening with -// `ce55c3a066ab461c3e74f0d5ac6f1209205e79bc` but was verified as happening on -// `92cc698a3dc6da459f3bf3789fd046c2dffdf107` too. -#[cfg(not(windows))] -async fn test_multicast_dns_only_connection() -> Result<()> { - let cfg0 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let (a_notifier, mut a_connected) = on_connected(); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - let (b_notifier, mut b_connected) = on_connected(); - b_agent.on_connection_state_change(b_notifier); - - connect_with_vnet(&a_agent, &b_agent).await?; - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_multicast_dns_mixed_connection() -> Result<()> { - let cfg0 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - ..Default::default() - }; - - let a_agent = Arc::new(Agent::new(cfg0).await?); - let (a_notifier, mut a_connected) = on_connected(); - a_agent.on_connection_state_change(a_notifier); - - let cfg1 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryOnly, - ..Default::default() - }; - - let b_agent = Arc::new(Agent::new(cfg1).await?); - let (b_notifier, mut b_connected) = on_connected(); - b_agent.on_connection_state_change(b_notifier); - - connect_with_vnet(&a_agent, &b_agent).await?; - let _ = a_connected.recv().await; - let _ = b_connected.recv().await; - - a_agent.close().await?; - b_agent.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_multicast_dns_static_host_name() -> Result<()> { - let cfg0 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - multicast_dns_host_name: "invalidHostName".to_owned(), - ..Default::default() - }; - if let Err(err) = Agent::new(cfg0).await { - assert_eq!(err, Error::ErrInvalidMulticastDnshostName); - } else { - panic!("expected error, but got ok"); - } - - let cfg1 = AgentConfig { - network_types: vec![NetworkType::Udp4], - candidate_types: vec![CandidateType::Host], - multicast_dns_mode: MulticastDnsMode::QueryAndGather, - multicast_dns_host_name: "validName.local".to_owned(), - ..Default::default() - }; - - let a = Agent::new(cfg1).await?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - a.on_candidate(Box::new( - move |c: Option>| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if c.is_none() { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }, - )); - - a.gather_candidates()?; - - log::debug!("wait for gathering is done..."); - let _ = done_rx.recv().await; - log::debug!("gathering is done"); - - Ok(()) -} - -#[test] -fn test_generate_multicast_dnsname() -> Result<()> { - let name = generate_multicast_dns_name(); - - let re = Regex::new( - r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}.local+$", - ); - - if let Ok(re) = re { - assert!( - re.is_match(&name), - "mDNS name must be UUID v4 + \".local\" suffix, got {name}" - ); - } else { - panic!("expected ok, but got err"); - } - - Ok(()) -} diff --git a/ice/src/mdns/mod.rs b/ice/src/mdns/mod.rs deleted file mode 100644 index 981d58fb4..000000000 --- a/ice/src/mdns/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -#[cfg(test)] -mod mdns_test; - -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use mdns::config::*; -use mdns::conn::*; -use uuid::Uuid; - -use crate::error::Result; - -/// Represents the different Multicast modes that ICE can run. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum MulticastDnsMode { - /// Means remote mDNS candidates will be discarded, and local host candidates will use IPs. - Disabled, - - /// Means remote mDNS candidates will be accepted, and local host candidates will use IPs. - QueryOnly, - - /// Means remote mDNS candidates will be accepted, and local host candidates will use mDNS. - QueryAndGather, -} - -impl Default for MulticastDnsMode { - fn default() -> Self { - Self::QueryOnly - } -} - -pub(crate) fn generate_multicast_dns_name() -> String { - // https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering - // The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”. - let u = Uuid::new_v4(); - format!("{u}.local") -} - -pub(crate) fn create_multicast_dns( - mdns_mode: MulticastDnsMode, - mdns_name: &str, - dest_addr: &str, -) -> Result>> { - let local_names = match mdns_mode { - MulticastDnsMode::QueryOnly => vec![], - MulticastDnsMode::QueryAndGather => vec![mdns_name.to_owned()], - MulticastDnsMode::Disabled => return Ok(None), - }; - - let addr = if dest_addr.is_empty() { - //TODO: why DEFAULT_DEST_ADDR doesn't work on Mac/Win? - if cfg!(target_os = "linux") { - SocketAddr::from_str(DEFAULT_DEST_ADDR)? - } else { - SocketAddr::from_str("0.0.0.0:5353")? - } - } else { - SocketAddr::from_str(dest_addr)? - }; - log::info!("mDNS is using {} as dest_addr", addr); - - let conn = DnsConn::server( - addr, - Config { - local_names, - ..Config::default() - }, - )?; - Ok(Some(Arc::new(conn))) -} diff --git a/ice/src/network_type/mod.rs b/ice/src/network_type/mod.rs deleted file mode 100644 index fcd50f9a2..000000000 --- a/ice/src/network_type/mod.rs +++ /dev/null @@ -1,148 +0,0 @@ -#[cfg(test)] -mod network_type_test; - -use std::fmt; -use std::net::IpAddr; - -use serde::{Deserialize, Serialize}; - -use crate::error::*; - -pub(crate) const UDP: &str = "udp"; -pub(crate) const TCP: &str = "tcp"; - -#[must_use] -pub fn supported_network_types() -> Vec { - vec![ - NetworkType::Udp4, - NetworkType::Udp6, - //NetworkType::TCP4, - //NetworkType::TCP6, - ] -} - -/// Represents the type of network. -#[derive(PartialEq, Debug, Copy, Clone, Eq, Hash, Serialize, Deserialize)] -pub enum NetworkType { - #[serde(rename = "unspecified")] - Unspecified, - - /// Indicates UDP over IPv4. - #[serde(rename = "udp4")] - Udp4, - - /// Indicates UDP over IPv6. - #[serde(rename = "udp6")] - Udp6, - - /// Indicates TCP over IPv4. - #[serde(rename = "tcp4")] - Tcp4, - - /// Indicates TCP over IPv6. - #[serde(rename = "tcp6")] - Tcp6, -} - -impl From for NetworkType { - fn from(v: u8) -> Self { - match v { - 1 => Self::Udp4, - 2 => Self::Udp6, - 3 => Self::Tcp4, - 4 => Self::Tcp6, - _ => Self::Unspecified, - } - } -} - -impl fmt::Display for NetworkType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Udp4 => "udp4", - Self::Udp6 => "udp6", - Self::Tcp4 => "tcp4", - Self::Tcp6 => "tcp6", - Self::Unspecified => "unspecified", - }; - write!(f, "{s}") - } -} - -impl Default for NetworkType { - fn default() -> Self { - Self::Unspecified - } -} - -impl NetworkType { - /// Returns true when network is UDP4 or UDP6. - #[must_use] - pub fn is_udp(self) -> bool { - self == Self::Udp4 || self == Self::Udp6 - } - - /// Returns true when network is TCP4 or TCP6. - #[must_use] - pub fn is_tcp(self) -> bool { - self == Self::Tcp4 || self == Self::Tcp6 - } - - /// Returns the short network description. - #[must_use] - pub fn network_short(self) -> String { - match self { - Self::Udp4 | Self::Udp6 => UDP.to_owned(), - Self::Tcp4 | Self::Tcp6 => TCP.to_owned(), - Self::Unspecified => "Unspecified".to_owned(), - } - } - - /// Returns true if the network is reliable. - #[must_use] - pub const fn is_reliable(self) -> bool { - match self { - Self::Tcp4 | Self::Tcp6 => true, - Self::Udp4 | Self::Udp6 | Self::Unspecified => false, - } - } - - /// Returns whether the network type is IPv4 or not. - #[must_use] - pub const fn is_ipv4(self) -> bool { - match self { - Self::Udp4 | Self::Tcp4 => true, - Self::Udp6 | Self::Tcp6 | Self::Unspecified => false, - } - } - - /// Returns whether the network type is IPv6 or not. - #[must_use] - pub const fn is_ipv6(self) -> bool { - match self { - Self::Udp6 | Self::Tcp6 => true, - Self::Udp4 | Self::Tcp4 | Self::Unspecified => false, - } - } -} - -/// Determines the type of network based on the short network string and an IP address. -pub(crate) fn determine_network_type(network: &str, ip: &IpAddr) -> Result { - let ipv4 = ip.is_ipv4(); - let net = network.to_lowercase(); - if net.starts_with(UDP) { - if ipv4 { - Ok(NetworkType::Udp4) - } else { - Ok(NetworkType::Udp6) - } - } else if net.starts_with(TCP) { - if ipv4 { - Ok(NetworkType::Tcp4) - } else { - Ok(NetworkType::Tcp6) - } - } else { - Err(Error::ErrDetermineNetworkType) - } -} diff --git a/ice/src/network_type/network_type_test.rs b/ice/src/network_type/network_type_test.rs deleted file mode 100644 index fa2a91daa..000000000 --- a/ice/src/network_type/network_type_test.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_network_type_parsing_success() -> Result<()> { - let ipv4: IpAddr = "192.168.0.1".parse().unwrap(); - let ipv6: IpAddr = "fe80::a3:6ff:fec4:5454".parse().unwrap(); - - let tests = vec![ - ("lowercase UDP4", "udp", ipv4, NetworkType::Udp4), - ("uppercase UDP4", "UDP", ipv4, NetworkType::Udp4), - ("lowercase UDP6", "udp", ipv6, NetworkType::Udp6), - ("uppercase UDP6", "UDP", ipv6, NetworkType::Udp6), - ]; - - for (name, in_network, in_ip, expected) in tests { - let actual = determine_network_type(in_network, &in_ip)?; - - assert_eq!( - actual, expected, - "NetworkTypeParsing: '{name}' -- input:{in_network} expected:{expected} actual:{actual}" - ); - } - - Ok(()) -} - -#[test] -fn test_network_type_parsing_failure() -> Result<()> { - let ipv6: IpAddr = "fe80::a3:6ff:fec4:5454".parse().unwrap(); - - let tests = vec![("invalid network", "junkNetwork", ipv6)]; - for (name, in_network, in_ip) in tests { - let result = determine_network_type(in_network, &in_ip); - assert!( - result.is_err(), - "NetworkTypeParsing should fail: '{name}' -- input:{in_network}", - ); - } - - Ok(()) -} - -#[test] -fn test_network_type_is_udp() -> Result<()> { - assert!(NetworkType::Udp4.is_udp()); - assert!(NetworkType::Udp6.is_udp()); - assert!(!NetworkType::Udp4.is_tcp()); - assert!(!NetworkType::Udp6.is_tcp()); - - Ok(()) -} - -#[test] -fn test_network_type_is_tcp() -> Result<()> { - assert!(NetworkType::Tcp4.is_tcp()); - assert!(NetworkType::Tcp6.is_tcp()); - assert!(!NetworkType::Tcp4.is_udp()); - assert!(!NetworkType::Tcp6.is_udp()); - - Ok(()) -} - -#[test] -fn test_network_type_serialization() { - let tests = vec![ - (NetworkType::Tcp4, "\"tcp4\""), - (NetworkType::Tcp6, "\"tcp6\""), - (NetworkType::Udp4, "\"udp4\""), - (NetworkType::Udp6, "\"udp6\""), - (NetworkType::Unspecified, "\"unspecified\""), - ]; - - for (network_type, expected_string) in tests { - assert_eq!( - expected_string.to_string(), - serde_json::to_string(&network_type).unwrap() - ); - } -} - -#[test] -fn test_network_type_to_string() { - let tests = vec![ - (NetworkType::Tcp4, "tcp4"), - (NetworkType::Tcp6, "tcp6"), - (NetworkType::Udp4, "udp4"), - (NetworkType::Udp6, "udp6"), - (NetworkType::Unspecified, "unspecified"), - ]; - - for (network_type, expected_string) in tests { - assert_eq!(network_type.to_string(), expected_string); - } -} diff --git a/ice/src/priority/mod.rs b/ice/src/priority/mod.rs deleted file mode 100644 index 8a00c81e9..000000000 --- a/ice/src/priority/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -#[cfg(test)] -mod priority_test; - -use stun::attributes::ATTR_PRIORITY; -use stun::checks::*; -use stun::message::*; - -/// Represents PRIORITY attribute. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct PriorityAttr(pub u32); - -const PRIORITY_SIZE: usize = 4; // 32 bit - -impl Setter for PriorityAttr { - // add_to adds PRIORITY attribute to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0_u8; PRIORITY_SIZE]; - v.copy_from_slice(&self.0.to_be_bytes()); - m.add(ATTR_PRIORITY, &v); - Ok(()) - } -} - -impl PriorityAttr { - /// Decodes PRIORITY attribute from message. - pub fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_PRIORITY)?; - - check_size(ATTR_PRIORITY, v.len(), PRIORITY_SIZE)?; - - let p = u32::from_be_bytes([v[0], v[1], v[2], v[3]]); - self.0 = p; - - Ok(()) - } -} diff --git a/ice/src/priority/priority_test.rs b/ice/src/priority/priority_test.rs deleted file mode 100644 index 231ca7c43..000000000 --- a/ice/src/priority/priority_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_priority_get_from() -> Result<()> { - let mut m = Message::new(); - let mut p = PriorityAttr::default(); - let result = p.get_from(&m); - if let Err(err) = result { - assert_eq!(err, stun::Error::ErrAttributeNotFound, "unexpected error"); - } else { - panic!("expected error, but got ok"); - } - - m.build(&[Box::new(BINDING_REQUEST), Box::new(p)])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - let mut p1 = PriorityAttr::default(); - p1.get_from(&m1)?; - - assert_eq!(p1, p, "not equal"); - - //"IncorrectSize" - { - let mut m3 = Message::new(); - m3.add(ATTR_PRIORITY, &[0; 100]); - let mut p2 = PriorityAttr::default(); - let result = p2.get_from(&m3); - if let Err(err) = result { - assert!(is_attr_size_invalid(&err), "should error"); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} diff --git a/ice/src/rand/mod.rs b/ice/src/rand/mod.rs deleted file mode 100644 index db041ef0b..000000000 --- a/ice/src/rand/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[cfg(test)] -mod rand_test; - -use rand::{thread_rng, Rng}; - -const RUNES_ALPHA: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const RUNES_CANDIDATE_ID_FOUNDATION: &[u8] = - b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+"; - -const LEN_UFRAG: usize = 16; -const LEN_PWD: usize = 32; - -// TODO: cryptographically strong random source -pub fn generate_crypto_random_string(n: usize, runes: &[u8]) -> String { - let mut rng = thread_rng(); - - let rand_string: String = (0..n) - .map(|_| { - let idx = rng.gen_range(0..runes.len()); - runes[idx] as char - }) - .collect(); - - rand_string -} - -/// -/// candidate-id = "candidate" ":" foundation -/// foundation = 1*32ice-char -/// ice-char = ALPHA / DIGIT / "+" / "/" -pub fn generate_cand_id() -> String { - format!( - "candidate:{}", - generate_crypto_random_string(32, RUNES_CANDIDATE_ID_FOUNDATION) - ) -} - -/// Generates ICE pwd. -/// This internally uses `generate_crypto_random_string`. -pub fn generate_pwd() -> String { - generate_crypto_random_string(LEN_PWD, RUNES_ALPHA) -} - -/// ICE user fragment. -/// This internally uses `generate_crypto_random_string`. -pub fn generate_ufrag() -> String { - generate_crypto_random_string(LEN_UFRAG, RUNES_ALPHA) -} diff --git a/ice/src/rand/rand_test.rs b/ice/src/rand/rand_test.rs deleted file mode 100644 index bf2fdcaa1..000000000 --- a/ice/src/rand/rand_test.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::sync::Arc; - -use tokio::sync::Mutex; -use waitgroup::WaitGroup; - -use super::*; -use crate::error::Result; - -#[tokio::test] -async fn test_random_generator_collision() -> Result<()> { - let test_cases = vec![ - ( - "CandidateID", - 0, /*||-> String { - generate_cand_id() - },*/ - ), - ( - "PWD", 1, /*||-> String { - generate_pwd() - },*/ - ), - ( - "Ufrag", 2, /*|| ->String { - generate_ufrag() - },*/ - ), - ]; - - const N: usize = 10; - const ITERATION: usize = 10; - - for (name, test_case) in test_cases { - for _ in 0..ITERATION { - let rands = Arc::new(Mutex::new(vec![])); - - // Create a new wait group. - let wg = WaitGroup::new(); - - for _ in 0..N { - let w = wg.worker(); - let rs = Arc::clone(&rands); - - tokio::spawn(async move { - let _d = w; - - let s = if test_case == 0 { - generate_cand_id() - } else if test_case == 1 { - generate_pwd() - } else { - generate_ufrag() - }; - - let mut r = rs.lock().await; - r.push(s); - }); - } - wg.wait().await; - - let rs = rands.lock().await; - assert_eq!(rs.len(), N, "{name} Failed to generate randoms"); - - for i in 0..N { - for j in i + 1..N { - assert_ne!( - rs[i], rs[j], - "{}: generateRandString caused collision: {} == {}", - name, rs[i], rs[j], - ); - } - } - } - } - - Ok(()) -} diff --git a/ice/src/state/mod.rs b/ice/src/state/mod.rs deleted file mode 100644 index bf21877c9..000000000 --- a/ice/src/state/mod.rs +++ /dev/null @@ -1,112 +0,0 @@ -#[cfg(test)] -mod state_test; - -use std::fmt; - -/// An enum showing the state of a ICE Connection List of supported States. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ConnectionState { - Unspecified, - - /// ICE agent is gathering addresses. - New, - - /// ICE agent has been given local and remote candidates, and is attempting to find a match. - Checking, - - /// ICE agent has a pairing, but is still checking other pairs. - Connected, - - /// ICE agent has finished. - Completed, - - /// ICE agent never could successfully connect. - Failed, - - /// ICE agent connected successfully, but has entered a failed state. - Disconnected, - - /// ICE agent has finished and is no longer handling requests. - Closed, -} - -impl Default for ConnectionState { - fn default() -> Self { - Self::Unspecified - } -} - -impl fmt::Display for ConnectionState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Unspecified => "Unspecified", - Self::New => "New", - Self::Checking => "Checking", - Self::Connected => "Connected", - Self::Completed => "Completed", - Self::Failed => "Failed", - Self::Disconnected => "Disconnected", - Self::Closed => "Closed", - }; - write!(f, "{s}") - } -} - -impl From for ConnectionState { - fn from(v: u8) -> Self { - match v { - 1 => Self::New, - 2 => Self::Checking, - 3 => Self::Connected, - 4 => Self::Completed, - 5 => Self::Failed, - 6 => Self::Disconnected, - 7 => Self::Closed, - _ => Self::Unspecified, - } - } -} - -/// Describes the state of the candidate gathering process. -#[derive(PartialEq, Eq, Copy, Clone)] -pub enum GatheringState { - Unspecified, - - /// Indicates candidate gathering is not yet started. - New, - - /// Indicates candidate gathering is ongoing. - Gathering, - - /// Indicates candidate gathering has been completed. - Complete, -} - -impl From for GatheringState { - fn from(v: u8) -> Self { - match v { - 1 => Self::New, - 2 => Self::Gathering, - 3 => Self::Complete, - _ => Self::Unspecified, - } - } -} - -impl Default for GatheringState { - fn default() -> Self { - Self::Unspecified - } -} - -impl fmt::Display for GatheringState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::New => "new", - Self::Gathering => "gathering", - Self::Complete => "complete", - Self::Unspecified => "unspecified", - }; - write!(f, "{s}") - } -} diff --git a/ice/src/state/state_test.rs b/ice/src/state/state_test.rs deleted file mode 100644 index 9e9382039..000000000 --- a/ice/src/state/state_test.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_connected_state_string() -> Result<()> { - let tests = vec![ - (ConnectionState::Unspecified, "Unspecified"), - (ConnectionState::New, "New"), - (ConnectionState::Checking, "Checking"), - (ConnectionState::Connected, "Connected"), - (ConnectionState::Completed, "Completed"), - (ConnectionState::Failed, "Failed"), - (ConnectionState::Disconnected, "Disconnected"), - (ConnectionState::Closed, "Closed"), - ]; - - for (connection_state, expected_string) in tests { - assert_eq!( - connection_state.to_string(), - expected_string, - "testCase: {expected_string} vs {connection_state}", - ) - } - - Ok(()) -} - -#[test] -fn test_gathering_state_string() -> Result<()> { - let tests = vec![ - (GatheringState::Unspecified, "unspecified"), - (GatheringState::New, "new"), - (GatheringState::Gathering, "gathering"), - (GatheringState::Complete, "complete"), - ]; - - for (gathering_state, expected_string) in tests { - assert_eq!( - gathering_state.to_string(), - expected_string, - "testCase: {expected_string} vs {gathering_state}", - ) - } - - Ok(()) -} diff --git a/ice/src/stats/mod.rs b/ice/src/stats/mod.rs deleted file mode 100644 index e3fc40676..000000000 --- a/ice/src/stats/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -use tokio::time::Instant; - -use crate::candidate::*; -use crate::network_type::*; - -// CandidatePairStats contains ICE candidate pair statistics -#[derive(Debug, Clone)] -pub struct CandidatePairStats { - // timestamp is the timestamp associated with this object. - pub timestamp: Instant, - - // local_candidate_id is the id of the local candidate - pub local_candidate_id: String, - - // remote_candidate_id is the id of the remote candidate - pub remote_candidate_id: String, - - // state represents the state of the checklist for the local and remote - // candidates in a pair. - pub state: CandidatePairState, - - // nominated is true when this valid pair that should be used for media - // if it is the highest-priority one amongst those whose nominated flag is set - pub nominated: bool, - - // packets_sent represents the total number of packets sent on this candidate pair. - pub packets_sent: u32, - - // packets_received represents the total number of packets received on this candidate pair. - pub packets_received: u32, - - // bytes_sent represents the total number of payload bytes sent on this candidate pair - // not including headers or padding. - pub bytes_sent: u64, - - // bytes_received represents the total number of payload bytes received on this candidate pair - // not including headers or padding. - pub bytes_received: u64, - - // last_packet_sent_timestamp represents the timestamp at which the last packet was - // sent on this particular candidate pair, excluding STUN packets. - pub last_packet_sent_timestamp: Instant, - - // last_packet_received_timestamp represents the timestamp at which the last packet - // was received on this particular candidate pair, excluding STUN packets. - pub last_packet_received_timestamp: Instant, - - // first_request_timestamp represents the timestamp at which the first STUN request - // was sent on this particular candidate pair. - pub first_request_timestamp: Instant, - - // last_request_timestamp represents the timestamp at which the last STUN request - // was sent on this particular candidate pair. The average interval between two - // consecutive connectivity checks sent can be calculated with - // (last_request_timestamp - first_request_timestamp) / requests_sent. - pub last_request_timestamp: Instant, - - // last_response_timestamp represents the timestamp at which the last STUN response - // was received on this particular candidate pair. - pub last_response_timestamp: Instant, - - // total_round_trip_time represents the sum of all round trip time measurements - // in seconds since the beginning of the session, based on STUN connectivity - // check responses (responses_received), including those that reply to requests - // that are sent in order to verify consent. The average round trip time can - // be computed from total_round_trip_time by dividing it by responses_received. - pub total_round_trip_time: f64, - - // current_round_trip_time represents the latest round trip time measured in seconds, - // computed from both STUN connectivity checks, including those that are sent - // for consent verification. - pub current_round_trip_time: f64, - - // available_outgoing_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the outgoing RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated - // over a 1 second window. - pub available_outgoing_bitrate: f64, - - // available_incoming_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the incoming RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is - // calculated over a 1 second window. - pub available_incoming_bitrate: f64, - - // circuit_breaker_trigger_count represents the number of times the circuit breaker - // is triggered for this particular 5-tuple, ceasing transmission. - pub circuit_breaker_trigger_count: u32, - - // requests_received represents the total number of connectivity check requests - // received (including retransmissions). It is impossible for the receiver to - // tell whether the request was sent in order to check connectivity or check - // consent, so all connectivity checks requests are counted here. - pub requests_received: u64, - - // requests_sent represents the total number of connectivity check requests - // sent (not including retransmissions). - pub requests_sent: u64, - - // responses_received represents the total number of connectivity check responses received. - pub responses_received: u64, - - // responses_sent epresents the total number of connectivity check responses sent. - // Since we cannot distinguish connectivity check requests and consent requests, - // all responses are counted. - pub responses_sent: u64, - - // retransmissions_received represents the total number of connectivity check - // request retransmissions received. - pub retransmissions_received: u64, - - // retransmissions_sent represents the total number of connectivity check - // request retransmissions sent. - pub retransmissions_sent: u64, - - // consent_requests_sent represents the total number of consent requests sent. - pub consent_requests_sent: u64, - - // consent_expired_timestamp represents the timestamp at which the latest valid - // STUN binding response expired. - pub consent_expired_timestamp: Instant, -} - -// CandidateStats contains ICE candidate statistics related to the ICETransport objects. -#[derive(Debug, Clone)] -pub struct CandidateStats { - // timestamp is the timestamp associated with this object. - pub timestamp: Instant, - - // id is the candidate id - pub id: String, - - // network_type represents the type of network interface used by the base of a - // local candidate (the address the ICE agent sends from). Only present for - // local candidates; it's not possible to know what type of network interface - // a remote candidate is using. - // - // Note: - // This stat only tells you about the network interface used by the first "hop"; - // it's possible that a connection will be bottlenecked by another type of network. - // For example, when using Wi-Fi tethering, the networkType of the relevant candidate - // would be "wifi", even when the next hop is over a cellular connection. - pub network_type: NetworkType, - - // ip is the ip address of the candidate, allowing for IPv4 addresses and - // IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. - pub ip: String, - - // port is the port number of the candidate. - pub port: u16, - - // candidate_type is the "Type" field of the ICECandidate. - pub candidate_type: CandidateType, - - // priority is the "priority" field of the ICECandidate. - pub priority: u32, - - // url is the url of the TURN or STUN server indicated in the that translated - // this ip address. It is the url address surfaced in an PeerConnectionICEEvent. - pub url: String, - - // relay_protocol is the protocol used by the endpoint to communicate with the - // TURN server. This is only present for local candidates. Valid values for - // the TURN url protocol is one of udp, tcp, or tls. - pub relay_protocol: String, - - // deleted is true if the candidate has been deleted/freed. For host candidates, - // this means that any network resources (typically a socket) associated with the - // candidate have been released. For TURN candidates, this means the TURN allocation - // is no longer active. - // - // Only defined for local candidates. For remote candidates, this property is not applicable. - pub deleted: bool, -} diff --git a/ice/src/tcp_type/mod.rs b/ice/src/tcp_type/mod.rs deleted file mode 100644 index 11f7bdbcb..000000000 --- a/ice/src/tcp_type/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[cfg(test)] -mod tcp_type_test; - -use std::fmt; - -// TCPType is the type of ICE TCP candidate as described in -// ttps://tools.ietf.org/html/rfc6544#section-4.5 -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum TcpType { - /// The default value. For example UDP candidates do not need this field. - Unspecified, - /// Active TCP candidate, which initiates TCP connections. - Active, - /// Passive TCP candidate, only accepts TCP connections. - Passive, - /// Like `Active` and `Passive` at the same time. - SimultaneousOpen, -} - -// from creates a new TCPType from string. -impl From<&str> for TcpType { - fn from(raw: &str) -> Self { - match raw { - "active" => Self::Active, - "passive" => Self::Passive, - "so" => Self::SimultaneousOpen, - _ => Self::Unspecified, - } - } -} - -impl fmt::Display for TcpType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Active => "active", - Self::Passive => "passive", - Self::SimultaneousOpen => "so", - Self::Unspecified => "unspecified", - }; - write!(f, "{s}") - } -} - -impl Default for TcpType { - fn default() -> Self { - Self::Unspecified - } -} diff --git a/ice/src/tcp_type/tcp_type_test.rs b/ice/src/tcp_type/tcp_type_test.rs deleted file mode 100644 index 26a189e00..000000000 --- a/ice/src/tcp_type/tcp_type_test.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_tcp_type() -> Result<()> { - //assert_eq!(tcpType, TCPType::Unspecified) - assert_eq!(TcpType::from("active"), TcpType::Active); - assert_eq!(TcpType::from("passive"), TcpType::Passive); - assert_eq!(TcpType::from("so"), TcpType::SimultaneousOpen); - assert_eq!(TcpType::from("something else"), TcpType::Unspecified); - - assert_eq!(TcpType::Unspecified.to_string(), "unspecified"); - assert_eq!(TcpType::Active.to_string(), "active"); - assert_eq!(TcpType::Passive.to_string(), "passive"); - assert_eq!(TcpType::SimultaneousOpen.to_string(), "so"); - - Ok(()) -} diff --git a/ice/src/udp_mux/mod.rs b/ice/src/udp_mux/mod.rs deleted file mode 100644 index 59560ca43..000000000 --- a/ice/src/udp_mux/mod.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::collections::HashMap; -use std::io::ErrorKind; -use std::net::SocketAddr; -use std::sync::{Arc, Weak}; - -use async_trait::async_trait; -use tokio::sync::{watch, Mutex}; -use util::sync::RwLock; -use util::{Conn, Error}; - -mod udp_mux_conn; -pub use udp_mux_conn::{UDPMuxConn, UDPMuxConnParams, UDPMuxWriter}; - -#[cfg(test)] -mod udp_mux_test; - -mod socket_addr_ext; - -use stun::attributes::ATTR_USERNAME; -use stun::message::{is_message as is_stun_message, Message as STUNMessage}; - -use crate::candidate::RECEIVE_MTU; - -/// Normalize a target socket addr for sending over a given local socket addr. This is useful when -/// a dual stack socket is used, in which case an IPv4 target needs to be mapped to an IPv6 -/// address. -fn normalize_socket_addr(target: &SocketAddr, socket_addr: &SocketAddr) -> SocketAddr { - match (target, socket_addr) { - (SocketAddr::V4(target_ipv4), SocketAddr::V6(_)) => { - let ipv6_mapped = target_ipv4.ip().to_ipv6_mapped(); - - SocketAddr::new(std::net::IpAddr::V6(ipv6_mapped), target_ipv4.port()) - } - // This will fail later if target is IPv6 and socket is IPv4, we ignore it here - (_, _) => *target, - } -} - -#[async_trait] -pub trait UDPMux { - /// Close the muxing. - async fn close(&self) -> Result<(), Error>; - - /// Get the underlying connection for a given ufrag. - async fn get_conn(self: Arc, ufrag: &str) -> Result, Error>; - - /// Remove the underlying connection for a given ufrag. - async fn remove_conn_by_ufrag(&self, ufrag: &str); -} - -pub struct UDPMuxParams { - conn: Box, -} - -impl UDPMuxParams { - pub fn new(conn: C) -> Self - where - C: Conn + Send + Sync + 'static, - { - Self { - conn: Box::new(conn), - } - } -} - -pub struct UDPMuxDefault { - /// The params this instance is configured with. - /// Contains the underlying UDP socket in use - params: UDPMuxParams, - - /// Maps from ufrag to the underlying connection. - conns: Mutex>, - - /// Maps from ip address to the underlying connection. - address_map: RwLock>, - - // Close sender - closed_watch_tx: Mutex>>, - - /// Close receiver - closed_watch_rx: watch::Receiver<()>, -} - -impl UDPMuxDefault { - pub fn new(params: UDPMuxParams) -> Arc { - let (closed_watch_tx, closed_watch_rx) = watch::channel(()); - - let mux = Arc::new(Self { - params, - conns: Mutex::default(), - address_map: RwLock::default(), - closed_watch_tx: Mutex::new(Some(closed_watch_tx)), - closed_watch_rx: closed_watch_rx.clone(), - }); - - let cloned_mux = Arc::clone(&mux); - cloned_mux.start_conn_worker(closed_watch_rx); - - mux - } - - pub async fn is_closed(&self) -> bool { - self.closed_watch_tx.lock().await.is_none() - } - - /// Create a muxed connection for a given ufrag. - fn create_muxed_conn(self: &Arc, ufrag: &str) -> Result { - let local_addr = self.params.conn.local_addr()?; - - let params = UDPMuxConnParams { - local_addr, - key: ufrag.into(), - udp_mux: Arc::downgrade(self) as Weak, - }; - - Ok(UDPMuxConn::new(params)) - } - - async fn conn_from_stun_message(&self, buffer: &[u8], addr: &SocketAddr) -> Option { - let (result, message) = { - let mut m = STUNMessage::new(); - - (m.unmarshal_binary(buffer), m) - }; - - match result { - Err(err) => { - log::warn!("Failed to handle decode ICE from {}: {}", addr, err); - None - } - Ok(_) => { - let (attr, found) = message.attributes.get(ATTR_USERNAME); - if !found { - log::warn!("No username attribute in STUN message from {}", &addr); - return None; - } - - let s = match String::from_utf8(attr.value) { - // Per the RFC this shouldn't happen - // https://datatracker.ietf.org/doc/html/rfc5389#section-15.3 - Err(err) => { - log::warn!( - "Failed to decode USERNAME from STUN message as UTF-8: {}", - err - ); - return None; - } - Ok(s) => s, - }; - - let conns = self.conns.lock().await; - let conn = s - .split(':') - .next() - .and_then(|ufrag| conns.get(ufrag)) - .cloned(); - - conn - } - } - } - - fn start_conn_worker(self: Arc, mut closed_watch_rx: watch::Receiver<()>) { - tokio::spawn(async move { - let mut buffer = [0u8; RECEIVE_MTU]; - - loop { - let loop_self = Arc::clone(&self); - let conn = &loop_self.params.conn; - - tokio::select! { - res = conn.recv_from(&mut buffer) => { - match res { - Ok((len, addr)) => { - // Find connection based on previously having seen this source address - let conn = { - let address_map = loop_self - .address_map - .read(); - - address_map.get(&addr).cloned() - }; - - let conn = match conn { - // If we couldn't find the connection based on source address, see if - // this is a STUN message and if so if we can find the connection based on ufrag. - None if is_stun_message(&buffer) => { - loop_self.conn_from_stun_message(&buffer, &addr).await - } - s @ Some(_) => s, - _ => None, - }; - - match conn { - None => { - log::trace!("Dropping packet from {}", &addr); - } - Some(conn) => { - if let Err(err) = conn.write_packet(&buffer[..len], addr).await { - log::error!("Failed to write packet: {}", err); - } - } - } - } - Err(Error::Io(err)) if err.0.kind() == ErrorKind::TimedOut => continue, - Err(err) => { - log::error!("Could not read udp packet: {}", err); - break; - } - } - } - _ = closed_watch_rx.changed() => { - return; - } - } - } - }); - } -} - -#[async_trait] -impl UDPMux for UDPMuxDefault { - async fn close(&self) -> Result<(), Error> { - if self.is_closed().await { - return Err(Error::ErrAlreadyClosed); - } - - let mut closed_tx = self.closed_watch_tx.lock().await; - - if let Some(tx) = closed_tx.take() { - let _ = tx.send(()); - drop(closed_tx); - - let old_conns = { - let mut conns = self.conns.lock().await; - - std::mem::take(&mut (*conns)) - }; - - // NOTE: We don't wait for these closure to complete - for (_, conn) in old_conns { - conn.close(); - } - - { - let mut address_map = self.address_map.write(); - - // NOTE: This is important, we need to drop all instances of `UDPMuxConn` to - // avoid a retain cycle due to the use of [`std::sync::Arc`] on both sides. - let _ = std::mem::take(&mut (*address_map)); - } - } - - Ok(()) - } - - async fn get_conn(self: Arc, ufrag: &str) -> Result, Error> { - if self.is_closed().await { - return Err(Error::ErrUseClosedNetworkConn); - } - - { - let mut conns = self.conns.lock().await; - if let Some(conn) = conns.get(ufrag) { - // UDPMuxConn uses `Arc` internally so it's cheap to clone, but because - // we implement `Conn` we need to further wrap it in an `Arc` here. - return Ok(Arc::new(conn.clone()) as Arc); - } - - let muxed_conn = self.create_muxed_conn(ufrag)?; - let mut close_rx = muxed_conn.close_rx(); - let cloned_self = Arc::clone(&self); - let cloned_ufrag = ufrag.to_string(); - tokio::spawn(async move { - let _ = close_rx.changed().await; - - // Arc needed - cloned_self.remove_conn_by_ufrag(&cloned_ufrag).await; - }); - - conns.insert(ufrag.into(), muxed_conn.clone()); - - Ok(Arc::new(muxed_conn) as Arc) - } - } - - async fn remove_conn_by_ufrag(&self, ufrag: &str) { - // Pion's ice implementation has both `RemoveConnByFrag` and `RemoveConn`, but since `conns` - // is keyed on `ufrag` their implementation is equivalent. - - let removed_conn = { - let mut conns = self.conns.lock().await; - conns.remove(ufrag) - }; - - if let Some(conn) = removed_conn { - let mut address_map = self.address_map.write(); - - for address in conn.get_addresses() { - address_map.remove(&address); - } - } - } -} - -#[async_trait] -impl UDPMuxWriter for UDPMuxDefault { - async fn register_conn_for_address(&self, conn: &UDPMuxConn, addr: SocketAddr) { - if self.is_closed().await { - return; - } - - let key = conn.key(); - { - let mut addresses = self.address_map.write(); - - addresses - .entry(addr) - .and_modify(|e| { - if e.key() != key { - e.remove_address(&addr); - *e = conn.clone(); - } - }) - .or_insert_with(|| conn.clone()); - } - - log::debug!("Registered {} for {}", addr, key); - } - - async fn send_to(&self, buf: &[u8], target: &SocketAddr) -> Result { - self.params - .conn - .send_to(buf, *target) - .await - .map_err(Into::into) - } -} diff --git a/ice/src/udp_mux/socket_addr_ext.rs b/ice/src/udp_mux/socket_addr_ext.rs deleted file mode 100644 index 7290b1b00..000000000 --- a/ice/src/udp_mux/socket_addr_ext.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::array::TryFromSliceError; -use std::convert::TryInto; -use std::net::SocketAddr; - -use util::Error; - -pub(super) trait SocketAddrExt { - ///Encode a representation of `self` into the buffer and return the length of this encoded - ///version. - /// - /// The buffer needs to be at least 27 bytes in length. - fn encode(&self, buffer: &mut [u8]) -> Result; - - /// Decode a `SocketAddr` from a buffer. The encoding should have previously been done with - /// [`SocketAddrExt::encode`]. - fn decode(buffer: &[u8]) -> Result; -} - -const IPV4_MARKER: u8 = 4; -const IPV4_ADDRESS_SIZE: usize = 7; -const IPV6_MARKER: u8 = 6; -const IPV6_ADDRESS_SIZE: usize = 27; - -pub(super) const MAX_ADDR_SIZE: usize = IPV6_ADDRESS_SIZE; - -impl SocketAddrExt for SocketAddr { - fn encode(&self, buffer: &mut [u8]) -> Result { - use std::net::SocketAddr::{V4, V6}; - - if buffer.len() < MAX_ADDR_SIZE { - return Err(Error::ErrBufferShort); - } - - match self { - V4(addr) => { - let marker = IPV4_MARKER; - let ip: [u8; 4] = addr.ip().octets(); - let port: u16 = addr.port(); - - buffer[0] = marker; - buffer[1..5].copy_from_slice(&ip); - buffer[5..7].copy_from_slice(&port.to_le_bytes()); - - Ok(7) - } - V6(addr) => { - let marker = IPV6_MARKER; - let ip: [u8; 16] = addr.ip().octets(); - let port: u16 = addr.port(); - let flowinfo = addr.flowinfo(); - let scope_id = addr.scope_id(); - - buffer[0] = marker; - buffer[1..17].copy_from_slice(&ip); - buffer[17..19].copy_from_slice(&port.to_le_bytes()); - buffer[19..23].copy_from_slice(&flowinfo.to_le_bytes()); - buffer[23..27].copy_from_slice(&scope_id.to_le_bytes()); - - Ok(MAX_ADDR_SIZE) - } - } - } - - fn decode(buffer: &[u8]) -> Result { - use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; - - match buffer[0] { - IPV4_MARKER => { - if buffer.len() < IPV4_ADDRESS_SIZE { - return Err(Error::ErrBufferShort); - } - - let ip_parts = &buffer[1..5]; - let port = match &buffer[5..7].try_into() { - Err(_) => return Err(Error::ErrFailedToParseIpaddr), - Ok(input) => u16::from_le_bytes(*input), - }; - - let ip = Ipv4Addr::new(ip_parts[0], ip_parts[1], ip_parts[2], ip_parts[3]); - - Ok(SocketAddr::V4(SocketAddrV4::new(ip, port))) - } - IPV6_MARKER => { - if buffer.len() < IPV6_ADDRESS_SIZE { - return Err(Error::ErrBufferShort); - } - - // Just to help the type system infer correctly - fn helper(b: &[u8]) -> Result<&[u8; 16], TryFromSliceError> { - b.try_into() - } - - let ip = match helper(&buffer[1..17]) { - Err(_) => return Err(Error::ErrFailedToParseIpaddr), - Ok(input) => Ipv6Addr::from(*input), - }; - let port = match &buffer[17..19].try_into() { - Err(_) => return Err(Error::ErrFailedToParseIpaddr), - Ok(input) => u16::from_le_bytes(*input), - }; - - let flowinfo = match &buffer[19..23].try_into() { - Err(_) => return Err(Error::ErrFailedToParseIpaddr), - Ok(input) => u32::from_le_bytes(*input), - }; - - let scope_id = match &buffer[23..27].try_into() { - Err(_) => return Err(Error::ErrFailedToParseIpaddr), - Ok(input) => u32::from_le_bytes(*input), - }; - - Ok(SocketAddr::V6(SocketAddrV6::new( - ip, port, flowinfo, scope_id, - ))) - } - _ => Err(Error::ErrFailedToParseIpaddr), - } - } -} - -#[cfg(test)] -mod test { - use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; - - use super::*; - - #[test] - fn test_ipv4() { - let ip = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([56, 128, 35, 5]), 0x1234)); - - let mut buffer = [0_u8; MAX_ADDR_SIZE]; - let encoded_len = ip.encode(&mut buffer); - - assert_eq!(encoded_len, Ok(7)); - assert_eq!( - &buffer[0..7], - &[IPV4_MARKER, 56, 128, 35, 5, 0x34, 0x12][..] - ); - - let decoded = SocketAddr::decode(&buffer); - - assert_eq!(decoded, Ok(ip)); - } - - #[test] - fn test_ipv6() { - let ip = SocketAddr::V6(SocketAddrV6::new( - Ipv6Addr::from([ - 92, 114, 235, 3, 244, 64, 38, 111, 20, 100, 199, 241, 19, 174, 220, 123, - ]), - 0x1234, - 0x12345678, - 0x87654321, - )); - - let mut buffer = [0_u8; MAX_ADDR_SIZE]; - let encoded_len = ip.encode(&mut buffer); - - assert_eq!(encoded_len, Ok(27)); - assert_eq!( - &buffer[0..27], - &[ - IPV6_MARKER, // marker - // Start of ipv6 address - 92, - 114, - 235, - 3, - 244, - 64, - 38, - 111, - 20, - 100, - 199, - 241, - 19, - 174, - 220, - 123, - // LE port - 0x34, - 0x12, - // LE flowinfo - 0x78, - 0x56, - 0x34, - 0x12, - // LE scope_id - 0x21, - 0x43, - 0x65, - 0x87, - ][..] - ); - - let decoded = SocketAddr::decode(&buffer); - - assert_eq!(decoded, Ok(ip)); - } - - #[test] - fn test_encode_ipv4_with_short_buffer() { - let mut buffer = vec![0u8; IPV4_ADDRESS_SIZE - 1]; - let ip = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([56, 128, 35, 5]), 0x1234)); - - let result = ip.encode(&mut buffer); - - assert_eq!(result, Err(Error::ErrBufferShort)); - } - - #[test] - fn test_encode_ipv6_with_short_buffer() { - let mut buffer = vec![0u8; MAX_ADDR_SIZE - 1]; - let ip = SocketAddr::V6(SocketAddrV6::new( - Ipv6Addr::from([ - 92, 114, 235, 3, 244, 64, 38, 111, 20, 100, 199, 241, 19, 174, 220, 123, - ]), - 0x1234, - 0x12345678, - 0x87654321, - )); - - let result = ip.encode(&mut buffer); - - assert_eq!(result, Err(Error::ErrBufferShort)); - } - - #[test] - fn test_decode_ipv4_with_short_buffer() { - let buffer = vec![IPV4_MARKER, 0]; - - let result = SocketAddr::decode(&buffer); - - assert_eq!(result, Err(Error::ErrBufferShort)); - } - - #[test] - fn test_decode_ipv6_with_short_buffer() { - let buffer = vec![IPV6_MARKER, 0]; - - let result = SocketAddr::decode(&buffer); - - assert_eq!(result, Err(Error::ErrBufferShort)); - } -} diff --git a/ice/src/udp_mux/udp_mux_conn.rs b/ice/src/udp_mux/udp_mux_conn.rs deleted file mode 100644 index b97183abf..000000000 --- a/ice/src/udp_mux/udp_mux_conn.rs +++ /dev/null @@ -1,320 +0,0 @@ -use std::collections::HashSet; -use std::convert::TryInto; -use std::io; -use std::net::SocketAddr; -use std::sync::{Arc, Weak}; - -use async_trait::async_trait; -use tokio::sync::watch; -use util::sync::Mutex; -use util::{Buffer, Conn, Error}; - -use super::socket_addr_ext::{SocketAddrExt, MAX_ADDR_SIZE}; -use super::{normalize_socket_addr, RECEIVE_MTU}; - -/// A trait for a [`UDPMuxConn`] to communicate with an UDP mux. -#[async_trait] -pub trait UDPMuxWriter { - /// Registers an address for the given connection. - async fn register_conn_for_address(&self, conn: &UDPMuxConn, addr: SocketAddr); - /// Sends the content of the buffer to the given target. - /// - /// Returns the number of bytes sent or an error, if any. - async fn send_to(&self, buf: &[u8], target: &SocketAddr) -> Result; -} - -/// Parameters for a [`UDPMuxConn`]. -pub struct UDPMuxConnParams { - /// Local socket address. - pub local_addr: SocketAddr, - /// Static key identifying the connection. - pub key: String, - /// A `std::sync::Weak` reference to the UDP mux. - /// - /// NOTE: a non-owning reference should be used to prevent possible cycles. - pub udp_mux: Weak, -} - -type ConnResult = Result; - -/// A UDP mux connection. -#[derive(Clone)] -pub struct UDPMuxConn { - /// Close Receiver. A copy of this can be obtained via [`close_tx`]. - closed_watch_rx: watch::Receiver, - - inner: Arc, -} - -impl UDPMuxConn { - /// Creates a new [`UDPMuxConn`]. - pub fn new(params: UDPMuxConnParams) -> Self { - let (closed_watch_tx, closed_watch_rx) = watch::channel(false); - - Self { - closed_watch_rx, - inner: Arc::new(UDPMuxConnInner { - params, - closed_watch_tx: Mutex::new(Some(closed_watch_tx)), - addresses: Default::default(), - buffer: Buffer::new(0, 0), - }), - } - } - - /// Returns a key identifying this connection. - pub fn key(&self) -> &str { - &self.inner.params.key - } - - /// Writes data to the given address. Returns an error if the buffer is too short or there's an - /// encoding error. - pub async fn write_packet(&self, data: &[u8], addr: SocketAddr) -> ConnResult<()> { - // NOTE: Pion/ice uses Sync.Pool to optimise this. - let mut buffer = make_buffer(); - let mut offset = 0; - - if (data.len() + MAX_ADDR_SIZE) > (RECEIVE_MTU + MAX_ADDR_SIZE) { - return Err(Error::ErrBufferShort); - } - - // Format of buffer: | data len(2) | data bytes(dn) | addr len(2) | addr bytes(an) | - // Where the number in parenthesis indicate the number of bytes used - // `dn` and `an` are the length in bytes of data and addr respectively. - - // SAFETY: `data.len()` is at most RECEIVE_MTU(8192) - MAX_ADDR_SIZE(27) - buffer[0..2].copy_from_slice(&(data.len() as u16).to_le_bytes()[..]); - offset += 2; - - buffer[offset..offset + data.len()].copy_from_slice(data); - offset += data.len(); - - let len = addr.encode(&mut buffer[offset + 2..])?; - buffer[offset..offset + 2].copy_from_slice(&(len as u16).to_le_bytes()[..]); - offset += 2 + len; - - self.inner.buffer.write(&buffer[..offset]).await?; - - Ok(()) - } - - /// Returns true if this connection is closed. - pub fn is_closed(&self) -> bool { - self.inner.is_closed() - } - - /// Gets a copy of the close [`tokio::sync::watch::Receiver`] that fires when this - /// connection is closed. - pub fn close_rx(&self) -> watch::Receiver { - self.closed_watch_rx.clone() - } - - /// Closes this connection. - pub fn close(&self) { - self.inner.close(); - } - - /// Gets the list of the addresses associated with this connection. - pub fn get_addresses(&self) -> Vec { - self.inner.get_addresses() - } - - /// Registers a new address for this connection. - pub async fn add_address(&self, addr: SocketAddr) { - self.inner.add_address(addr); - if let Some(mux) = self.inner.params.udp_mux.upgrade() { - mux.register_conn_for_address(self, addr).await; - } - } - - /// Deregisters an address. - pub fn remove_address(&self, addr: &SocketAddr) { - self.inner.remove_address(addr) - } - - /// Returns true if the given address is associated with this connection. - pub fn contains_address(&self, addr: &SocketAddr) -> bool { - self.inner.contains_address(addr) - } -} - -struct UDPMuxConnInner { - params: UDPMuxConnParams, - - /// Close Sender. We'll send a value on this channel when we close - closed_watch_tx: Mutex>>, - - /// Remote addresses we've seen on this connection. - addresses: Mutex>, - - buffer: Buffer, -} - -impl UDPMuxConnInner { - // Sending/Recieving - async fn recv_from(&self, buf: &mut [u8]) -> ConnResult<(usize, SocketAddr)> { - // NOTE: Pion/ice uses Sync.Pool to optimise this. - let mut buffer = make_buffer(); - let mut offset = 0; - - let len = self.buffer.read(&mut buffer, None).await?; - // We always have at least. - // - // * 2 bytes for data len - // * 2 bytes for addr len - // * 7 bytes for an Ipv4 addr - if len < 11 { - return Err(Error::ErrBufferShort); - } - - let data_len: usize = buffer[..2] - .try_into() - .map(u16::from_le_bytes) - .map(From::from) - .unwrap(); - offset += 2; - - let total = 2 + data_len + 2 + 7; - if data_len > buf.len() || total > len { - return Err(Error::ErrBufferShort); - } - - buf[..data_len].copy_from_slice(&buffer[offset..offset + data_len]); - offset += data_len; - - let address_len: usize = buffer[offset..offset + 2] - .try_into() - .map(u16::from_le_bytes) - .map(From::from) - .unwrap(); - offset += 2; - - let addr = SocketAddr::decode(&buffer[offset..offset + address_len])?; - - Ok((data_len, addr)) - } - - async fn send_to(&self, buf: &[u8], target: &SocketAddr) -> ConnResult { - if let Some(mux) = self.params.udp_mux.upgrade() { - mux.send_to(buf, target).await - } else { - Err(Error::Other(format!( - "wanted to send {} bytes to {}, but UDP mux is gone", - buf.len(), - target - ))) - } - } - - fn is_closed(&self) -> bool { - self.closed_watch_tx.lock().is_none() - } - - fn close(self: &Arc) { - let mut closed_tx = self.closed_watch_tx.lock(); - - if let Some(tx) = closed_tx.take() { - let _ = tx.send(true); - drop(closed_tx); - - let cloned_self = Arc::clone(self); - - { - let mut addresses = self.addresses.lock(); - *addresses = Default::default(); - } - - // NOTE: Alternatively we could wait on the buffer closing here so that - // our caller can wait for things to fully settle down - tokio::spawn(async move { - cloned_self.buffer.close().await; - }); - } - } - - fn local_addr(&self) -> SocketAddr { - self.params.local_addr - } - - // Address related methods - pub(super) fn get_addresses(&self) -> Vec { - let addresses = self.addresses.lock(); - - addresses.iter().copied().collect() - } - - pub(super) fn add_address(self: &Arc, addr: SocketAddr) { - { - let mut addresses = self.addresses.lock(); - addresses.insert(addr); - } - } - - pub(super) fn remove_address(&self, addr: &SocketAddr) { - { - let mut addresses = self.addresses.lock(); - addresses.remove(addr); - } - } - - pub(super) fn contains_address(&self, addr: &SocketAddr) -> bool { - let addresses = self.addresses.lock(); - - addresses.contains(addr) - } -} - -#[async_trait] -impl Conn for UDPMuxConn { - async fn connect(&self, _addr: SocketAddr) -> ConnResult<()> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, _buf: &mut [u8]) -> ConnResult { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv_from(&self, buf: &mut [u8]) -> ConnResult<(usize, SocketAddr)> { - self.inner.recv_from(buf).await - } - - async fn send(&self, _buf: &[u8]) -> ConnResult { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> ConnResult { - let normalized_target = normalize_socket_addr(&target, &self.inner.params.local_addr); - - if !self.contains_address(&normalized_target) { - self.add_address(normalized_target).await; - } - - self.inner.send_to(buf, &normalized_target).await - } - - fn local_addr(&self) -> ConnResult { - Ok(self.inner.local_addr()) - } - - fn remote_addr(&self) -> Option { - None - } - async fn close(&self) -> ConnResult<()> { - self.inner.close(); - - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -#[inline(always)] -/// Create a buffer of appropriate size to fit both a packet with max RECEIVE_MTU and the -/// additional metadata used for muxing. -fn make_buffer() -> Vec { - // The 4 extra bytes are used to encode the length of the data and address respectively. - // See [`write_packet`] for details. - vec![0u8; RECEIVE_MTU + MAX_ADDR_SIZE + 2 + 2] -} diff --git a/ice/src/udp_mux/udp_mux_test.rs b/ice/src/udp_mux/udp_mux_test.rs deleted file mode 100644 index 10e7701b7..000000000 --- a/ice/src/udp_mux/udp_mux_test.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::convert::TryInto; -use std::io; -use std::time::Duration; - -use rand::{thread_rng, Rng}; -use sha1::{Digest, Sha1}; -use stun::message::{Message, BINDING_REQUEST}; -use tokio::net::UdpSocket; -use tokio::time::{sleep, timeout}; - -use super::*; -use crate::error::Result; - -#[derive(Debug, Copy, Clone)] -enum Network { - Ipv4, - Ipv6, -} - -impl Network { - /// Bind the UDP socket for the "remote". - async fn bind(self) -> io::Result { - match self { - Network::Ipv4 => UdpSocket::bind("0.0.0.0:0").await, - Network::Ipv6 => UdpSocket::bind("[::]:0").await, - } - } - - /// Connect ip from the "remote". - fn connect_ip(self, port: u16) -> String { - match self { - Network::Ipv4 => format!("127.0.0.1:{port}"), - Network::Ipv6 => format!("[::1]:{port}"), - } - } -} - -const TIMEOUT: Duration = Duration::from_secs(60); - -#[tokio::test] -async fn test_udp_mux() -> Result<()> { - use std::io::Write; - env_logger::Builder::from_default_env() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .init(); - - // TODO: Support IPv6 dual stack. This works Linux and macOS, but not Windows. - #[cfg(all(unix, target_pointer_width = "64"))] - let udp_socket = UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0)).await?; - - #[cfg(any(not(unix), not(target_pointer_width = "64")))] - let udp_socket = UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)).await?; - - let addr = udp_socket.local_addr()?; - log::info!("Listening on {}", addr); - - let udp_mux = UDPMuxDefault::new(UDPMuxParams::new(udp_socket)); - let udp_mux_dyn = Arc::clone(&udp_mux) as Arc; - - let udp_mux_dyn_1 = Arc::clone(&udp_mux_dyn); - let h1 = tokio::spawn(async move { - timeout( - TIMEOUT, - test_mux_connection(Arc::clone(&udp_mux_dyn_1), "ufrag1", addr, Network::Ipv4), - ) - .await - }); - - let udp_mux_dyn_2 = Arc::clone(&udp_mux_dyn); - let h2 = tokio::spawn(async move { - timeout( - TIMEOUT, - test_mux_connection(Arc::clone(&udp_mux_dyn_2), "ufrag2", addr, Network::Ipv4), - ) - .await - }); - - let all_results; - - #[cfg(all(unix, target_pointer_width = "64"))] - { - // TODO: Support IPv6 dual stack. This works Linux and macOS, but not Windows. - let udp_mux_dyn_3 = Arc::clone(&udp_mux_dyn); - let h3 = tokio::spawn(async move { - timeout( - TIMEOUT, - test_mux_connection(Arc::clone(&udp_mux_dyn_3), "ufrag3", addr, Network::Ipv6), - ) - .await - }); - - let (r1, r2, r3) = tokio::join!(h1, h2, h3); - all_results = [r1, r2, r3]; - } - - #[cfg(any(not(unix), not(target_pointer_width = "64")))] - { - let (r1, r2) = tokio::join!(h1, h2); - all_results = [r1, r2]; - } - - for timeout_result in &all_results { - // Timeout error - match timeout_result { - Err(timeout_err) => { - panic!("Mux test timedout: {timeout_err:?}"); - } - - // Join error - Ok(join_result) => match join_result { - Err(err) => { - panic!("Mux test failed with join error: {err:?}"); - } - // Actual error - Ok(mux_result) => { - if let Err(err) = mux_result { - panic!("Mux test failed with error: {err:?}"); - } - } - }, - } - } - - let timeout = all_results.iter().find_map(|r| r.as_ref().err()); - assert!( - timeout.is_none(), - "At least one of the muxed tasks timedout {all_results:?}" - ); - - let res = udp_mux.close().await; - assert!(res.is_ok()); - let res = udp_mux.get_conn("failurefrag").await; - - assert!( - res.is_err(), - "Getting connections after UDPMuxDefault is closed should fail" - ); - - Ok(()) -} - -async fn test_mux_connection( - mux: Arc, - ufrag: &str, - listener_addr: SocketAddr, - network: Network, -) -> Result<()> { - let conn = mux.get_conn(ufrag).await?; - // FIXME: Cleanup - - let connect_addr = network - .connect_ip(listener_addr.port()) - .parse::() - .unwrap(); - - let remote_connection = Arc::new(network.bind().await?); - log::info!("Bound for ufrag: {}", ufrag); - remote_connection.connect(connect_addr).await?; - log::info!("Connected to {} for ufrag: {}", connect_addr, ufrag); - log::info!( - "Testing muxing from {} over {}", - remote_connection.local_addr().unwrap(), - listener_addr - ); - - // These bytes should be dropped - remote_connection.send("Dropped bytes".as_bytes()).await?; - - sleep(Duration::from_millis(1)).await; - - let stun_msg = { - let mut m = Message { - typ: BINDING_REQUEST, - ..Message::default() - }; - - m.add(ATTR_USERNAME, format!("{ufrag}:otherufrag").as_bytes()); - - m.marshal_binary().unwrap() - }; - - let remote_connection_addr = remote_connection.local_addr()?; - - conn.send_to(&stun_msg, remote_connection_addr).await?; - - let mut buffer = vec![0u8; RECEIVE_MTU]; - let len = remote_connection.recv(&mut buffer).await?; - assert_eq!(buffer[..len], stun_msg); - - const TARGET_SIZE: usize = 1024 * 1024; - - // Read on the muxed side - let conn_2 = Arc::clone(&conn); - let mux_handle = tokio::spawn(async move { - let conn = conn_2; - - let mut buffer = vec![0u8; RECEIVE_MTU]; - let mut next_sequence = 0; - let mut read = 0; - - while read < TARGET_SIZE { - let (n, _) = conn - .recv_from(&mut buffer) - .await - .expect("recv_from should not error"); - assert_eq!(n, RECEIVE_MTU); - - verify_packet(&buffer[..n], next_sequence); - - conn.send_to(&buffer[..n], remote_connection_addr) - .await - .expect("Failed to write to muxxed connection"); - - read += n; - log::debug!("Muxxed read {}, sequence: {}", read, next_sequence); - next_sequence += 1; - } - }); - - let remote_connection_2 = Arc::clone(&remote_connection); - let remote_handle = tokio::spawn(async move { - let remote_connection = remote_connection_2; - let mut buffer = vec![0u8; RECEIVE_MTU]; - let mut next_sequence = 0; - let mut read = 0; - - while read < TARGET_SIZE { - let n = remote_connection - .recv(&mut buffer) - .await - .expect("recv_from should not error"); - assert_eq!(n, RECEIVE_MTU); - - verify_packet(&buffer[..n], next_sequence); - read += n; - log::debug!("Remote read {}, sequence: {}", read, next_sequence); - next_sequence += 1; - } - }); - - let mut sequence: u32 = 0; - let mut written = 0; - let mut buffer = vec![0u8; RECEIVE_MTU]; - while written < TARGET_SIZE { - thread_rng().fill(&mut buffer[24..]); - - let hash = sha1_hash(&buffer[24..]); - buffer[4..24].copy_from_slice(&hash); - buffer[0..4].copy_from_slice(&sequence.to_le_bytes()); - - let len = remote_connection.send(&buffer).await?; - - written += len; - log::debug!("Data written {}, sequence: {}", written, sequence); - sequence += 1; - - sleep(Duration::from_millis(1)).await; - } - - let (r1, r2) = tokio::join!(mux_handle, remote_handle); - assert!(r1.is_ok() && r2.is_ok()); - - let res = conn.close().await; - assert!(res.is_ok(), "Failed to close Conn: {res:?}"); - - Ok(()) -} - -fn verify_packet(buffer: &[u8], next_sequence: u32) { - let read_sequence = u32::from_le_bytes(buffer[0..4].try_into().unwrap()); - assert_eq!(read_sequence, next_sequence); - - let hash = sha1_hash(&buffer[24..]); - assert_eq!(hash, buffer[4..24]); -} - -fn sha1_hash(buffer: &[u8]) -> Vec { - let mut hasher = Sha1::new(); - hasher.update(&buffer[24..]); - - hasher.finalize().to_vec() -} diff --git a/ice/src/udp_network.rs b/ice/src/udp_network.rs deleted file mode 100644 index e2077bdf7..000000000 --- a/ice/src/udp_network.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::sync::Arc; - -use super::udp_mux::UDPMux; -use super::Error; - -#[derive(Default, Clone)] -pub struct EphemeralUDP { - port_min: u16, - port_max: u16, -} - -impl EphemeralUDP { - pub fn new(port_min: u16, port_max: u16) -> Result { - let mut s = Self::default(); - s.set_ports(port_min, port_max)?; - - Ok(s) - } - - pub fn port_min(&self) -> u16 { - self.port_min - } - - pub fn port_max(&self) -> u16 { - self.port_max - } - - pub fn set_ports(&mut self, port_min: u16, port_max: u16) -> Result<(), Error> { - if port_max < port_min { - return Err(Error::ErrPort); - } - - self.port_min = port_min; - self.port_max = port_max; - - Ok(()) - } -} - -/// Configuration for the underlying UDP network stack. -/// There are two ways to configure this Ephemeral and Muxed. -/// -/// **Ephemeral mode** -/// -/// In Ephemeral mode sockets are created and bound to random ports during ICE -/// gathering. The ports to use can be restricted by setting [`EphemeralUDP::port_min`] and -/// [`EphemeralUDP::port_max`] in which case only ports in this range will be used. -/// -/// **Muxed** -/// -/// In muxed mode a single UDP socket is used and all connections are muxed over this single socket. -/// -#[derive(Clone)] -pub enum UDPNetwork { - Ephemeral(EphemeralUDP), - Muxed(Arc), -} - -impl Default for UDPNetwork { - fn default() -> Self { - Self::Ephemeral(Default::default()) - } -} - -impl UDPNetwork { - fn is_ephemeral(&self) -> bool { - matches!(self, Self::Ephemeral(_)) - } - - fn is_muxed(&self) -> bool { - matches!(self, Self::Muxed(_)) - } -} - -#[cfg(test)] -mod test { - use super::EphemeralUDP; - - #[test] - fn test_ephemeral_udp_constructor() { - assert!( - EphemeralUDP::new(3000, 2999).is_err(), - "EphemeralUDP should not allow invalid port range" - ); - - let e = EphemeralUDP::default(); - assert_eq!(e.port_min(), 0, "EphemeralUDP should default port_min to 0"); - assert_eq!(e.port_max(), 0, "EphemeralUDP should default port_max to 0"); - } - - #[test] - fn test_ephemeral_udp_set_ports() { - let mut e = EphemeralUDP::default(); - - assert!( - e.set_ports(3000, 2999).is_err(), - "EphemeralUDP should not allow invalid port range" - ); - - assert!( - e.set_ports(6000, 6001).is_ok(), - "EphemeralUDP::set_ports should allow valid port range" - ); - - assert_eq!( - e.port_min(), - 6000, - "Ports set with `EphemeralUDP::set_ports` should be reflected" - ); - assert_eq!( - e.port_max(), - 6001, - "Ports set with `EphemeralUDP::set_ports` should be reflected" - ); - } -} diff --git a/ice/src/url/mod.rs b/ice/src/url/mod.rs deleted file mode 100644 index d55fd48f1..000000000 --- a/ice/src/url/mod.rs +++ /dev/null @@ -1,266 +0,0 @@ -#[cfg(test)] -mod url_test; - -use std::borrow::Cow; -use std::convert::From; -use std::fmt; - -use crate::error::*; - -/// The type of server used in the ice.URL structure. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum SchemeType { - /// The URL represents a STUN server. - Stun, - - /// The URL represents a STUNS (secure) server. - Stuns, - - /// The URL represents a TURN server. - Turn, - - /// The URL represents a TURNS (secure) server. - Turns, - - /// Default public constant to use for "enum" like struct comparisons when no value was defined. - Unknown, -} - -impl Default for SchemeType { - fn default() -> Self { - Self::Unknown - } -} - -impl From<&str> for SchemeType { - /// Defines a procedure for creating a new `SchemeType` from a raw - /// string naming the scheme type. - fn from(raw: &str) -> Self { - match raw { - "stun" => Self::Stun, - "stuns" => Self::Stuns, - "turn" => Self::Turn, - "turns" => Self::Turns, - _ => Self::Unknown, - } - } -} - -impl fmt::Display for SchemeType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - SchemeType::Stun => "stun", - SchemeType::Stuns => "stuns", - SchemeType::Turn => "turn", - SchemeType::Turns => "turns", - SchemeType::Unknown => "unknown", - }; - write!(f, "{s}") - } -} - -/// The transport protocol type that is used in the `ice::url::Url` structure. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum ProtoType { - /// The URL uses a UDP transport. - Udp, - - /// The URL uses a TCP transport. - Tcp, - - Unknown, -} - -impl Default for ProtoType { - fn default() -> Self { - Self::Udp - } -} - -// defines a procedure for creating a new ProtoType from a raw -// string naming the transport protocol type. -impl From<&str> for ProtoType { - // NewSchemeType defines a procedure for creating a new SchemeType from a raw - // string naming the scheme type. - fn from(raw: &str) -> Self { - match raw { - "udp" => Self::Udp, - "tcp" => Self::Tcp, - _ => Self::Unknown, - } - } -} - -impl fmt::Display for ProtoType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Udp => "udp", - Self::Tcp => "tcp", - Self::Unknown => "unknown", - }; - write!(f, "{s}") - } -} - -/// Represents a STUN (rfc7064) or TURN (rfc7065) URL. -#[derive(Debug, Clone, Default)] -pub struct Url { - pub scheme: SchemeType, - pub host: String, - pub port: u16, - pub username: String, - pub password: String, - pub proto: ProtoType, -} - -impl fmt::Display for Url { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let host = if self.host.contains("::") { - "[".to_owned() + self.host.as_str() + "]" - } else { - self.host.clone() - }; - if self.scheme == SchemeType::Turn || self.scheme == SchemeType::Turns { - write!( - f, - "{}:{}:{}?transport={}", - self.scheme, host, self.port, self.proto - ) - } else { - write!(f, "{}:{}:{}", self.scheme, host, self.port) - } - } -} - -impl Url { - /// Parses a STUN or TURN urls following the ABNF syntax described in - /// [IETF rfc-7064](https://tools.ietf.org/html/rfc7064) and - /// [IETF rfc-7065](https://tools.ietf.org/html/rfc7065) respectively. - pub fn parse_url(raw: &str) -> Result { - // work around for url crate - if raw.contains("//") { - return Err(Error::ErrInvalidUrl); - } - - let mut s = raw.to_string(); - let pos = raw.find(':'); - if let Some(p) = pos { - s.replace_range(p..=p, "://"); - } else { - return Err(Error::ErrSchemeType); - } - - let raw_parts = url::Url::parse(&s)?; - - let scheme = raw_parts.scheme().into(); - - let host = if let Some(host) = raw_parts.host_str() { - host.trim() - .trim_start_matches('[') - .trim_end_matches(']') - .to_owned() - } else { - return Err(Error::ErrHost); - }; - - let port = if let Some(port) = raw_parts.port() { - port - } else if scheme == SchemeType::Stun || scheme == SchemeType::Turn { - 3478 - } else { - 5349 - }; - - let mut q_args = raw_parts.query_pairs(); - let proto = match scheme { - SchemeType::Stun => { - if q_args.count() > 0 { - return Err(Error::ErrStunQuery); - } - ProtoType::Udp - } - SchemeType::Stuns => { - if q_args.count() > 0 { - return Err(Error::ErrStunQuery); - } - ProtoType::Tcp - } - SchemeType::Turn => { - if q_args.count() > 1 { - return Err(Error::ErrInvalidQuery); - } - if let Some((key, value)) = q_args.next() { - if key == Cow::Borrowed("transport") { - let proto: ProtoType = value.as_ref().into(); - if proto == ProtoType::Unknown { - return Err(Error::ErrProtoType); - } - proto - } else { - return Err(Error::ErrInvalidQuery); - } - } else { - ProtoType::Udp - } - } - SchemeType::Turns => { - if q_args.count() > 1 { - return Err(Error::ErrInvalidQuery); - } - if let Some((key, value)) = q_args.next() { - if key == Cow::Borrowed("transport") { - let proto: ProtoType = value.as_ref().into(); - if proto == ProtoType::Unknown { - return Err(Error::ErrProtoType); - } - proto - } else { - return Err(Error::ErrInvalidQuery); - } - } else { - ProtoType::Tcp - } - } - SchemeType::Unknown => { - return Err(Error::ErrSchemeType); - } - }; - - Ok(Self { - scheme, - host, - port, - username: "".to_owned(), - password: "".to_owned(), - proto, - }) - } - - /* - fn parse_proto(raw:&str) ->Result { - let qArgs= raw.split('='); - if qArgs.len() != 2 { - return Err(Error::ErrInvalidQuery.into()); - } - - var proto ProtoType - if rawProto := qArgs.Get("transport"); rawProto != "" { - if proto = NewProtoType(rawProto); proto == ProtoType(0) { - return ProtoType(Unknown), ErrProtoType - } - return proto, nil - } - - if len(qArgs) > 0 { - return ProtoType(Unknown), ErrInvalidQuery - } - - return proto, nil - }*/ - - /// Returns whether the this URL's scheme describes secure scheme or not. - #[must_use] - pub fn is_secure(&self) -> bool { - self.scheme == SchemeType::Stuns || self.scheme == SchemeType::Turns - } -} diff --git a/ice/src/url/url_test.rs b/ice/src/url/url_test.rs deleted file mode 100644 index acbf72789..000000000 --- a/ice/src/url/url_test.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::*; - -#[test] -fn test_parse_url_success() -> Result<()> { - let tests = vec![ - ( - "stun:google.de", - "stun:google.de:3478", - SchemeType::Stun, - false, - "google.de", - 3478, - ProtoType::Udp, - ), - ( - "stun:google.de:1234", - "stun:google.de:1234", - SchemeType::Stun, - false, - "google.de", - 1234, - ProtoType::Udp, - ), - ( - "stuns:google.de", - "stuns:google.de:5349", - SchemeType::Stuns, - true, - "google.de", - 5349, - ProtoType::Tcp, - ), - ( - "stun:[::1]:123", - "stun:[::1]:123", - SchemeType::Stun, - false, - "::1", - 123, - ProtoType::Udp, - ), - ( - "turn:google.de", - "turn:google.de:3478?transport=udp", - SchemeType::Turn, - false, - "google.de", - 3478, - ProtoType::Udp, - ), - ( - "turns:google.de", - "turns:google.de:5349?transport=tcp", - SchemeType::Turns, - true, - "google.de", - 5349, - ProtoType::Tcp, - ), - ( - "turn:google.de?transport=udp", - "turn:google.de:3478?transport=udp", - SchemeType::Turn, - false, - "google.de", - 3478, - ProtoType::Udp, - ), - ( - "turns:google.de?transport=tcp", - "turns:google.de:5349?transport=tcp", - SchemeType::Turns, - true, - "google.de", - 5349, - ProtoType::Tcp, - ), - ]; - - for ( - raw_url, - expected_url_string, - expected_scheme, - expected_secure, - expected_host, - expected_port, - expected_proto, - ) in tests - { - let url = Url::parse_url(raw_url)?; - - assert_eq!(url.scheme, expected_scheme, "testCase: {raw_url:?}"); - assert_eq!( - expected_url_string, - url.to_string(), - "testCase: {raw_url:?}" - ); - assert_eq!(url.is_secure(), expected_secure, "testCase: {raw_url:?}"); - assert_eq!(url.host, expected_host, "testCase: {raw_url:?}"); - assert_eq!(url.port, expected_port, "testCase: {raw_url:?}"); - assert_eq!(url.proto, expected_proto, "testCase: {raw_url:?}"); - } - - Ok(()) -} - -#[test] -fn test_parse_url_failure() -> Result<()> { - let tests = vec![ - ("", Error::ErrSchemeType), - (":::", Error::ErrUrlParse), - ("stun:[::1]:123:", Error::ErrPort), - ("stun:[::1]:123a", Error::ErrPort), - ("google.de", Error::ErrSchemeType), - ("stun:", Error::ErrHost), - ("stun:google.de:abc", Error::ErrPort), - ("stun:google.de?transport=udp", Error::ErrStunQuery), - ("stuns:google.de?transport=udp", Error::ErrStunQuery), - ("turn:google.de?trans=udp", Error::ErrInvalidQuery), - ("turns:google.de?trans=udp", Error::ErrInvalidQuery), - ( - "turns:google.de?transport=udp&another=1", - Error::ErrInvalidQuery, - ), - ("turn:google.de?transport=ip", Error::ErrProtoType), - ]; - - for (raw_url, expected_err) in tests { - let result = Url::parse_url(raw_url); - if let Err(err) = result { - assert_eq!( - err.to_string(), - expected_err.to_string(), - "testCase: '{raw_url}', expected err '{expected_err}', but got err '{err}'" - ); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} diff --git a/ice/src/use_candidate/mod.rs b/ice/src/use_candidate/mod.rs deleted file mode 100644 index 8bb0d47ca..000000000 --- a/ice/src/use_candidate/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[cfg(test)] -mod use_candidate_test; - -use stun::attributes::ATTR_USE_CANDIDATE; -use stun::message::*; - -/// Represents USE-CANDIDATE attribute. -#[derive(Default)] -pub struct UseCandidateAttr; - -impl Setter for UseCandidateAttr { - /// Adds USE-CANDIDATE attribute to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - m.add(ATTR_USE_CANDIDATE, &[]); - Ok(()) - } -} - -impl UseCandidateAttr { - #[must_use] - pub const fn new() -> Self { - Self - } - - /// Returns true if USE-CANDIDATE attribute is set. - #[must_use] - pub fn is_set(m: &Message) -> bool { - let result = m.get(ATTR_USE_CANDIDATE); - result.is_ok() - } -} diff --git a/ice/src/use_candidate/use_candidate_test.rs b/ice/src/use_candidate/use_candidate_test.rs deleted file mode 100644 index 671a7544c..000000000 --- a/ice/src/use_candidate/use_candidate_test.rs +++ /dev/null @@ -1,19 +0,0 @@ -use stun::message::BINDING_REQUEST; - -use super::*; -use crate::error::Result; - -#[test] -fn test_use_candidate_attr_add_to() -> Result<()> { - let mut m = Message::new(); - assert!(!UseCandidateAttr::is_set(&m), "should not be set"); - - m.build(&[Box::new(BINDING_REQUEST), Box::new(UseCandidateAttr::new())])?; - - let mut m1 = Message::new(); - m1.write(&m.raw)?; - - assert!(UseCandidateAttr::is_set(&m1), "should be set"); - - Ok(()) -} diff --git a/ice/src/util/mod.rs b/ice/src/util/mod.rs deleted file mode 100644 index a44cb09f2..000000000 --- a/ice/src/util/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -#[cfg(test)] -mod util_test; - -use std::collections::HashSet; -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; - -use stun::agent::*; -use stun::attributes::*; -use stun::integrity::*; -use stun::message::*; -use stun::textattrs::*; -use stun::xoraddr::*; -use tokio::time::Duration; -use util::vnet::net::*; -use util::Conn; - -use crate::agent::agent_config::{InterfaceFilterFn, IpFilterFn}; -use crate::error::*; -use crate::network_type::*; - -pub fn create_addr(_network: NetworkType, ip: IpAddr, port: u16) -> SocketAddr { - /*if network.is_tcp(){ - return &net.TCPAddr{IP: ip, Port: port} - default: - return &net.UDPAddr{IP: ip, Port: port} - }*/ - SocketAddr::new(ip, port) -} - -pub fn assert_inbound_username(m: &Message, expected_username: &str) -> Result<()> { - let mut username = Username::new(ATTR_USERNAME, String::new()); - username.get_from(m)?; - - if username.to_string() != expected_username { - return Err(Error::Other(format!( - "{:?} expected({}) actual({})", - Error::ErrMismatchUsername, - expected_username, - username, - ))); - } - - Ok(()) -} - -pub fn assert_inbound_message_integrity(m: &mut Message, key: &[u8]) -> Result<()> { - let message_integrity_attr = MessageIntegrity(key.to_vec()); - Ok(message_integrity_attr.check(m)?) -} - -/// Initiates a stun requests to `server_addr` using conn, reads the response and returns the -/// `XORMappedAddress` returned by the stun server. -/// Adapted from stun v0.2. -pub async fn get_xormapped_addr( - conn: &Arc, - server_addr: SocketAddr, - deadline: Duration, -) -> Result { - let resp = stun_request(conn, server_addr, deadline).await?; - let mut addr = XorMappedAddress::default(); - addr.get_from(&resp)?; - Ok(addr) -} - -const MAX_MESSAGE_SIZE: usize = 1280; - -pub async fn stun_request( - conn: &Arc, - server_addr: SocketAddr, - deadline: Duration, -) -> Result { - let mut request = Message::new(); - request.build(&[Box::new(BINDING_REQUEST), Box::new(TransactionId::new())])?; - - conn.send_to(&request.raw, server_addr).await?; - let mut bs = vec![0_u8; MAX_MESSAGE_SIZE]; - let (n, _) = if deadline > Duration::from_secs(0) { - match tokio::time::timeout(deadline, conn.recv_from(&mut bs)).await { - Ok(result) => match result { - Ok((n, addr)) => (n, addr), - Err(err) => return Err(Error::Other(err.to_string())), - }, - Err(err) => return Err(Error::Other(err.to_string())), - } - } else { - conn.recv_from(&mut bs).await? - }; - - let mut res = Message::new(); - res.raw = bs[..n].to_vec(); - res.decode()?; - - Ok(res) -} - -pub async fn local_interfaces( - vnet: &Arc, - interface_filter: &Option, - ip_filter: &Option, - network_types: &[NetworkType], -) -> HashSet { - let mut ips = HashSet::new(); - let interfaces = vnet.get_interfaces().await; - - let (mut ipv4requested, mut ipv6requested) = (false, false); - for typ in network_types { - if typ.is_ipv4() { - ipv4requested = true; - } - if typ.is_ipv6() { - ipv6requested = true; - } - } - - for iface in interfaces { - if let Some(filter) = interface_filter { - if !filter(iface.name()) { - continue; - } - } - - for ipnet in iface.addrs() { - let ipaddr = ipnet.addr(); - - if !ipaddr.is_loopback() - && ((ipv4requested && ipaddr.is_ipv4()) || (ipv6requested && ipaddr.is_ipv6())) - && ip_filter - .as_ref() - .map(|filter| filter(ipaddr)) - .unwrap_or(true) - { - ips.insert(ipaddr); - } - } - } - - ips -} - -pub async fn listen_udp_in_port_range( - vnet: &Arc, - port_max: u16, - port_min: u16, - laddr: SocketAddr, -) -> Result> { - if laddr.port() != 0 || (port_min == 0 && port_max == 0) { - return Ok(vnet.bind(laddr).await?); - } - let i = if port_min == 0 { 1 } else { port_min }; - let j = if port_max == 0 { 0xFFFF } else { port_max }; - if i > j { - return Err(Error::ErrPort); - } - - let port_start = rand::random::() % (j - i + 1) + i; - let mut port_current = port_start; - loop { - let laddr = SocketAddr::new(laddr.ip(), port_current); - match vnet.bind(laddr).await { - Ok(c) => return Ok(c), - Err(err) => log::debug!("failed to listen {}: {}", laddr, err), - }; - - port_current += 1; - if port_current > j { - port_current = i; - } - if port_current == port_start { - break; - } - } - - Err(Error::ErrPort) -} diff --git a/ice/src/util/util_test.rs b/ice/src/util/util_test.rs deleted file mode 100644 index ab0faf94f..000000000 --- a/ice/src/util/util_test.rs +++ /dev/null @@ -1,10 +0,0 @@ -use super::*; - -#[tokio::test] -async fn test_local_interfaces() -> Result<()> { - let vnet = Arc::new(Net::new(None)); - let interfaces = vnet.get_interfaces().await; - let ips = local_interfaces(&vnet, &None, &None, &[NetworkType::Udp4, NetworkType::Udp6]).await; - log::info!("interfaces: {:?}, ips: {:?}", interfaces, ips); - Ok(()) -} diff --git a/interceptor/.gitignore b/interceptor/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/interceptor/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/interceptor/CHANGELOG.md b/interceptor/CHANGELOG.md deleted file mode 100644 index 5394a2e78..000000000 --- a/interceptor/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# interceptor changelog - -## Unreleased - -## v0.9.0 - -* Fix over-NACK due not resetting lost_packets bitmask [\#372](https://github.com/webrtc-rs/webrtc/pull/372/). -* Further extended stats interceptors to collect stats for `RemoteOutoundRTPStats` and improve `RemoteInboundRTPStats` collection. [#282](https://github.com/webrtc-rs/webrtc/pull/282) by [@k0nserv](https://github.com/k0nserv). -* When generating periodic TWCC feedback packets we no longer burst several packets in a row to catch up, i.e., we now use `MissedTickBehavior::Skip` instead of the default `MissedTickBehavior::Burst` for the ticker in question. [#323](https://github.com/webrtc-rs/webrtc/pull/323) by [@k0nserv](https://github.com/k0nserv). -* Don't generate empty TWCC packets that libWebRTC will ignore. [#324](https://github.com/webrtc-rs/webrtc/pull/324) by [@k0nserv](https://github.com/k0nserv). -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - -## v0.8.0 - -* [#14 Don't panic on seqnum rollover](https://github.com/webrtc-rs/interceptor/pull/14) contributed by by [@pthatcher](https://github.com/pthatcher). -* Add stats interceptor. Contributed by [@k0nserv](https://github.com/k0nserv) in [#277](https://github.com/webrtc-rs/webrtc/pull/277/) and [#225](https://github.com/webrtc-rs/webrtc/pull/225). -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). - -## Prior to 0.8.0 - -Before 0.8.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/interceptor/releases). - diff --git a/interceptor/Cargo.toml b/interceptor/Cargo.toml deleted file mode 100644 index 3edbcd3d0..000000000 --- a/interceptor/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "interceptor" -version = "0.12.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of Pluggable RTP/RTCP processors" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/interceptor" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/interceptor" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["marshal", "sync"] } -rtp = { version = "0.11.0", path = "../rtp" } -rtcp = { version = "0.11.0", path = "../rtcp" } -srtp = { version = "0.13.0", path = "../srtp", package = "webrtc-srtp" } - -tokio = { version = "1.32.0", features = ["sync", "time"] } -async-trait = "0.1" -bytes = "1" -thiserror = "1" -rand = "0.8" -waitgroup = "0.1" -log = "0.4" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -chrono = "0.4.28" diff --git a/interceptor/LICENSE-APACHE b/interceptor/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/interceptor/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/interceptor/LICENSE-MIT b/interceptor/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/interceptor/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/interceptor/README.md b/interceptor/README.md deleted file mode 100644 index a35123ec3..000000000 --- a/interceptor/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of Pluggable RTP/RTCP processors. Rewrite Pion Interceptor in Rust -

diff --git a/interceptor/codecov.yml b/interceptor/codecov.yml deleted file mode 100644 index 2103309ff..000000000 --- a/interceptor/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: a1ee2aa3-5623-4b41-8ba8-446a6b6792df - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/interceptor/doc/webrtc.rs.png b/interceptor/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/interceptor/src/chain.rs b/interceptor/src/chain.rs deleted file mode 100644 index 12f3bb161..000000000 --- a/interceptor/src/chain.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::sync::Arc; - -use crate::error::*; -use crate::stream_info::StreamInfo; -use crate::*; - -/// Chain is an interceptor that runs all child interceptors in order. -#[derive(Default)] -pub struct Chain { - interceptors: Vec>, -} - -impl Chain { - /// new returns a new Chain interceptor. - pub fn new(interceptors: Vec>) -> Self { - Chain { interceptors } - } - - pub fn add(&mut self, icpr: Arc) { - self.interceptors.push(icpr); - } -} - -#[async_trait] -impl Interceptor for Chain { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - mut reader: Arc, - ) -> Arc { - for icpr in &self.interceptors { - reader = icpr.bind_rtcp_reader(reader).await; - } - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - mut writer: Arc, - ) -> Arc { - for icpr in &self.interceptors { - writer = icpr.bind_rtcp_writer(writer).await; - } - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - mut writer: Arc, - ) -> Arc { - for icpr in &self.interceptors { - writer = icpr.bind_local_stream(info, writer).await; - } - writer - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - for icpr in &self.interceptors { - icpr.unbind_local_stream(info).await; - } - } - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - mut reader: Arc, - ) -> Arc { - for icpr in &self.interceptors { - reader = icpr.bind_remote_stream(info, reader).await; - } - reader - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - for icpr in &self.interceptors { - icpr.unbind_remote_stream(info).await; - } - } - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - let mut errs = vec![]; - for icpr in &self.interceptors { - if let Err(err) = icpr.close().await { - errs.push(err); - } - } - flatten_errs(errs) - } -} diff --git a/interceptor/src/error.rs b/interceptor/src/error.rs deleted file mode 100644 index 1ee4d227e..000000000 --- a/interceptor/src/error.rs +++ /dev/null @@ -1,46 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("Invalid Parent RTCP Reader")] - ErrInvalidParentRtcpReader, - #[error("Invalid Parent RTP Reader")] - ErrInvalidParentRtpReader, - #[error("Invalid Next RTP Writer")] - ErrInvalidNextRtpWriter, - #[error("Invalid CloseRx Channel")] - ErrInvalidCloseRx, - #[error("Invalid PacketRx Channel")] - ErrInvalidPacketRx, - #[error("IO EOF")] - ErrIoEOF, - #[error("Buffer is too short")] - ErrShortBuffer, - #[error("Invalid buffer size")] - ErrInvalidSize, - - #[error("{0}")] - Srtp(#[from] srtp::Error), - #[error("{0}")] - Rtcp(#[from] rtcp::Error), - #[error("{0}")] - Rtp(#[from] rtp::Error), - #[error("{0}")] - Util(#[from] util::Error), - - #[error("{0}")] - Other(String), -} - -/// flatten_errs flattens multiple errors into one -pub fn flatten_errs(errs: Vec) -> Result<()> { - if errs.is_empty() { - Ok(()) - } else { - let errs_strs: Vec = errs.into_iter().map(|e| e.to_string()).collect(); - Err(Error::Other(errs_strs.join("\n"))) - } -} diff --git a/interceptor/src/lib.rs b/interceptor/src/lib.rs deleted file mode 100644 index 98008d405..000000000 --- a/interceptor/src/lib.rs +++ /dev/null @@ -1,228 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use async_trait::async_trait; -use error::Result; -use stream_info::StreamInfo; - -pub mod chain; -mod error; -pub mod mock; -pub mod nack; -pub mod noop; -pub mod registry; -pub mod report; -pub mod stats; -pub mod stream_info; -pub mod stream_reader; -pub mod twcc; - -pub use error::Error; - -/// Attributes are a generic key/value store used by interceptors -pub type Attributes = HashMap; - -/// InterceptorBuilder provides an interface for constructing interceptors -pub trait InterceptorBuilder { - fn build(&self, id: &str) -> Result>; -} - -/// Interceptor can be used to add functionality to you PeerConnections by modifying any incoming/outgoing rtp/rtcp -/// packets, or sending your own packets as needed. -#[async_trait] -pub trait Interceptor { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc; - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc; - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc; - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo); - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc; - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo); - - async fn close(&self) -> Result<()>; -} - -/// RTPWriter is used by Interceptor.bind_local_stream. -#[async_trait] -pub trait RTPWriter { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, attributes: &Attributes) -> Result; -} - -pub type RTPWriterBoxFn = Box< - dyn (Fn( - &rtp::packet::Packet, - &Attributes, - ) -> Pin> + Send + Sync>>) - + Send - + Sync, ->; -pub struct RTPWriterFn(pub RTPWriterBoxFn); - -#[async_trait] -impl RTPWriter for RTPWriterFn { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, attributes: &Attributes) -> Result { - self.0(pkt, attributes).await - } -} - -/// RTPReader is used by Interceptor.bind_remote_stream. -#[async_trait] -pub trait RTPReader { - /// read a rtp packet - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)>; -} - -pub type RTPReaderBoxFn = Box< - dyn (Fn( - &mut [u8], - &Attributes, - ) - -> Pin> + Send + Sync>>) - + Send - + Sync, ->; -pub struct RTPReaderFn(pub RTPReaderBoxFn); - -#[async_trait] -impl RTPReader for RTPReaderFn { - /// read a rtp packet - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - self.0(buf, attributes).await - } -} - -/// RTCPWriter is used by Interceptor.bind_rtcpwriter. -#[async_trait] -pub trait RTCPWriter { - /// write a batch of rtcp packets - async fn write( - &self, - pkts: &[Box], - attributes: &Attributes, - ) -> Result; -} - -pub type RTCPWriterBoxFn = Box< - dyn (Fn( - &[Box], - &Attributes, - ) -> Pin> + Send + Sync>>) - + Send - + Sync, ->; - -pub struct RTCPWriterFn(pub RTCPWriterBoxFn); - -#[async_trait] -impl RTCPWriter for RTCPWriterFn { - /// write a batch of rtcp packets - async fn write( - &self, - pkts: &[Box], - attributes: &Attributes, - ) -> Result { - self.0(pkts, attributes).await - } -} - -/// RTCPReader is used by Interceptor.bind_rtcpreader. -#[async_trait] -pub trait RTCPReader { - /// read a batch of rtcp packets - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(Vec>, Attributes)>; -} - -pub type RTCPReaderBoxFn = Box< - dyn (Fn( - &mut [u8], - &Attributes, - ) -> Pin< - Box< - dyn Future< - Output = Result<( - Vec>, - Attributes, - )>, - > + Send - + Sync, - >, - >) + Send - + Sync, ->; - -pub struct RTCPReaderFn(pub RTCPReaderBoxFn); - -#[async_trait] -impl RTCPReader for RTCPReaderFn { - /// read a batch of rtcp packets - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(Vec>, Attributes)> { - self.0(buf, attributes).await - } -} - -/// Helper for the tests. -#[cfg(test)] -mod test { - use std::future::Future; - use std::time::Duration; - - pub async fn timeout_or_fail(duration: Duration, future: T) -> T::Output - where - T: Future, - { - tokio::time::timeout(duration, future) - .await - .expect("should not time out") - } -} diff --git a/interceptor/src/mock/mock_builder.rs b/interceptor/src/mock/mock_builder.rs deleted file mode 100644 index d6faabbca..000000000 --- a/interceptor/src/mock/mock_builder.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::Arc; - -use crate::error::Result; -use crate::{Interceptor, InterceptorBuilder}; - -pub type MockBuilderResult = Result>; - -/// MockBuilder is a mock Builder for testing. -pub struct MockBuilder { - pub build: Box MockBuilderResult) + Send + Sync + 'static>, -} - -impl MockBuilder { - pub fn new MockBuilderResult) + Send + Sync + 'static>(f: F) -> Self { - MockBuilder { build: Box::new(f) } - } -} - -impl InterceptorBuilder for MockBuilder { - fn build(&self, id: &str) -> MockBuilderResult { - (self.build)(id) - } -} diff --git a/interceptor/src/mock/mock_interceptor.rs b/interceptor/src/mock/mock_interceptor.rs deleted file mode 100644 index 7b5088efb..000000000 --- a/interceptor/src/mock/mock_interceptor.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::future::Future; -use std::pin::Pin; - -use crate::*; - -pub type BindRtcpReaderFn = Box< - dyn (Fn( - Arc, - ) - -> Pin> + Send + Sync>>) - + Send - + Sync, ->; - -pub type BindRtcpWriterFn = Box< - dyn (Fn( - Arc, - ) - -> Pin> + Send + Sync>>) - + Send - + Sync, ->; -pub type BindLocalStreamFn = Box< - dyn (Fn( - &StreamInfo, - Arc, - ) -> Pin> + Send + Sync>>) - + Send - + Sync, ->; -pub type UnbindLocalStreamFn = - Box Pin + Send + Sync>>) + Send + Sync>; -pub type BindRemoteStreamFn = Box< - dyn (Fn( - &StreamInfo, - Arc, - ) -> Pin> + Send + Sync>>) - + Send - + Sync, ->; -pub type UnbindRemoteStreamFn = - Box Pin + Send + Sync>>) + Send + Sync>; -pub type CloseFn = - Box Pin> + Send + Sync>>) + Send + Sync>; - -/// MockInterceptor is an mock Interceptor for testing. -#[derive(Default)] -pub struct MockInterceptor { - pub bind_rtcp_reader_fn: Option, - pub bind_rtcp_writer_fn: Option, - pub bind_local_stream_fn: Option, - pub unbind_local_stream_fn: Option, - pub bind_remote_stream_fn: Option, - pub unbind_remote_stream_fn: Option, - pub close_fn: Option, -} - -#[async_trait] -impl Interceptor for MockInterceptor { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - if let Some(f) = &self.bind_rtcp_reader_fn { - f(reader).await - } else { - reader - } - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - if let Some(f) = &self.bind_rtcp_writer_fn { - f(writer).await - } else { - writer - } - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc { - if let Some(f) = &self.bind_local_stream_fn { - f(info, writer).await - } else { - writer - } - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - if let Some(f) = &self.unbind_local_stream_fn { - f(info).await - } - } - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc { - if let Some(f) = &self.bind_remote_stream_fn { - f(info, reader).await - } else { - reader - } - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - if let Some(f) = &self.unbind_remote_stream_fn { - f(info).await - } - } - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - if let Some(f) = &self.close_fn { - f().await - } else { - Ok(()) - } - } -} diff --git a/interceptor/src/mock/mock_stream.rs b/interceptor/src/mock/mock_stream.rs deleted file mode 100644 index 8183197a3..000000000 --- a/interceptor/src/mock/mock_stream.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::sync::{mpsc, Mutex}; -use util::Marshal; - -use crate::error::{Error, Result}; -use crate::stream_info::StreamInfo; -use crate::{Attributes, Interceptor, RTCPReader, RTCPWriter, RTPReader, RTPWriter}; - -type RTCPPackets = Vec>; - -/// MockStream is a helper struct for testing interceptors. -pub struct MockStream { - interceptor: Arc, - - rtcp_writer: Mutex>>, - rtp_writer: Mutex>>, - - rtcp_out_modified_tx: mpsc::Sender, - rtp_out_modified_tx: mpsc::Sender, - rtcp_in_rx: Mutex>, - rtp_in_rx: Mutex>, - - rtcp_out_modified_rx: Mutex>, - rtp_out_modified_rx: Mutex>, - rtcp_in_tx: Mutex>>, - rtp_in_tx: Mutex>>, - - rtcp_in_modified_rx: Mutex>>, - rtp_in_modified_rx: Mutex>>, -} - -impl MockStream { - /// new creates a new MockStream - pub async fn new( - info: &StreamInfo, - interceptor: Arc, - ) -> Arc { - let (rtcp_in_tx, rtcp_in_rx) = mpsc::channel(1000); - let (rtp_in_tx, rtp_in_rx) = mpsc::channel(1000); - let (rtcp_out_modified_tx, rtcp_out_modified_rx) = mpsc::channel(1000); - let (rtp_out_modified_tx, rtp_out_modified_rx) = mpsc::channel(1000); - let (rtcp_in_modified_tx, rtcp_in_modified_rx) = mpsc::channel(1000); - let (rtp_in_modified_tx, rtp_in_modified_rx) = mpsc::channel(1000); - - let stream = Arc::new(MockStream { - interceptor: Arc::clone(&interceptor), - - rtcp_writer: Mutex::new(None), - rtp_writer: Mutex::new(None), - - rtcp_in_tx: Mutex::new(Some(rtcp_in_tx)), - rtp_in_tx: Mutex::new(Some(rtp_in_tx)), - rtcp_in_rx: Mutex::new(rtcp_in_rx), - rtp_in_rx: Mutex::new(rtp_in_rx), - - rtcp_out_modified_tx, - rtp_out_modified_tx, - rtcp_out_modified_rx: Mutex::new(rtcp_out_modified_rx), - rtp_out_modified_rx: Mutex::new(rtp_out_modified_rx), - - rtcp_in_modified_rx: Mutex::new(rtcp_in_modified_rx), - rtp_in_modified_rx: Mutex::new(rtp_in_modified_rx), - }); - - let rtcp_writer = interceptor - .bind_rtcp_writer(Arc::clone(&stream) as Arc) - .await; - { - let mut rw = stream.rtcp_writer.lock().await; - *rw = Some(rtcp_writer); - } - let rtp_writer = interceptor - .bind_local_stream( - info, - Arc::clone(&stream) as Arc, - ) - .await; - { - let mut rw = stream.rtp_writer.lock().await; - *rw = Some(rtp_writer); - } - - let rtcp_reader = interceptor - .bind_rtcp_reader(Arc::clone(&stream) as Arc) - .await; - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - let a = Attributes::new(); - loop { - let pkts = match rtcp_reader.read(&mut buf, &a).await { - Ok((n, _)) => n, - Err(err) => { - let _ = rtcp_in_modified_tx.send(Err(err)).await; - break; - } - }; - - let _ = rtcp_in_modified_tx.send(Ok(pkts)).await; - } - }); - - let rtp_reader = interceptor - .bind_remote_stream( - info, - Arc::clone(&stream) as Arc, - ) - .await; - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - let a = Attributes::new(); - loop { - let pkt = match rtp_reader.read(&mut buf, &a).await { - Ok((pkt, _)) => pkt, - Err(err) => { - let _ = rtp_in_modified_tx.send(Err(err)).await; - break; - } - }; - - let _ = rtp_in_modified_tx.send(Ok(pkt)).await; - } - }); - - stream - } - - /// write_rtcp writes a batch of rtcp packet to the stream, using the interceptor - pub async fn write_rtcp( - &self, - pkt: &[Box], - ) -> Result { - let a = Attributes::new(); - let rtcp_writer = self.rtcp_writer.lock().await; - if let Some(writer) = &*rtcp_writer { - writer.write(pkt, &a).await - } else { - Err(Error::Other("invalid rtcp_writer".to_owned())) - } - } - - /// write_rtp writes an rtp packet to the stream, using the interceptor - pub async fn write_rtp(&self, pkt: &rtp::packet::Packet) -> Result { - let a = Attributes::new(); - let rtp_writer = self.rtp_writer.lock().await; - if let Some(writer) = &*rtp_writer { - writer.write(pkt, &a).await - } else { - Err(Error::Other("invalid rtp_writer".to_owned())) - } - } - - /// receive_rtcp schedules a new rtcp batch, so it can be read be the stream - pub async fn receive_rtcp(&self, pkts: Vec>) { - let rtcp_in_tx = self.rtcp_in_tx.lock().await; - if let Some(tx) = &*rtcp_in_tx { - let _ = tx.send(pkts).await; - } - } - - /// receive_rtp schedules a rtp packet, so it can be read be the stream - pub async fn receive_rtp(&self, pkt: rtp::packet::Packet) { - let rtp_in_tx = self.rtp_in_tx.lock().await; - if let Some(tx) = &*rtp_in_tx { - let _ = tx.send(pkt).await; - } - } - - /// written_rtcp returns a channel containing the rtcp batches written, modified by the interceptor - pub async fn written_rtcp(&self) -> Option>> { - let mut rtcp_out_modified_rx = self.rtcp_out_modified_rx.lock().await; - rtcp_out_modified_rx.recv().await - } - - /// Returns the last rtcp packet bacth that was written, modified by the interceptor. - /// - /// NB: This method discards all other previously recoreded packet batches. - pub async fn last_written_rtcp( - &self, - ) -> Option>> { - let mut last = None; - let mut rtcp_out_modified_rx = self.rtcp_out_modified_rx.lock().await; - - while let Ok(v) = rtcp_out_modified_rx.try_recv() { - last = Some(v); - } - - last - } - - /// written_rtp returns a channel containing rtp packets written, modified by the interceptor - pub async fn written_rtp(&self) -> Option { - let mut rtp_out_modified_rx = self.rtp_out_modified_rx.lock().await; - rtp_out_modified_rx.recv().await - } - - /// read_rtcp returns a channel containing the rtcp batched read, modified by the interceptor - pub async fn read_rtcp( - &self, - ) -> Option>>> { - let mut rtcp_in_modified_rx = self.rtcp_in_modified_rx.lock().await; - rtcp_in_modified_rx.recv().await - } - - /// read_rtp returns a channel containing the rtp packets read, modified by the interceptor - pub async fn read_rtp(&self) -> Option> { - let mut rtp_in_modified_rx = self.rtp_in_modified_rx.lock().await; - rtp_in_modified_rx.recv().await - } - - /// close closes the stream and the underlying interceptor - pub async fn close(&self) -> Result<()> { - { - let mut rtcp_in_tx = self.rtcp_in_tx.lock().await; - rtcp_in_tx.take(); - } - { - let mut rtp_in_tx = self.rtp_in_tx.lock().await; - rtp_in_tx.take(); - } - self.interceptor.close().await - } -} - -#[async_trait] -impl RTCPWriter for MockStream { - async fn write( - &self, - pkts: &[Box], - _attributes: &Attributes, - ) -> Result { - let _ = self.rtcp_out_modified_tx.send(pkts.to_vec()).await; - - Ok(0) - } -} - -#[async_trait] -impl RTCPReader for MockStream { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(Vec>, Attributes)> { - let pkts = { - let mut rtcp_in = self.rtcp_in_rx.lock().await; - rtcp_in.recv().await.ok_or(Error::ErrIoEOF)? - }; - - let marshaled = rtcp::packet::marshal(&pkts)?; - let n = marshaled.len(); - if n > buf.len() { - return Err(Error::ErrShortBuffer); - } - - buf[..n].copy_from_slice(&marshaled); - Ok((pkts, a.clone())) - } -} - -#[async_trait] -impl RTPWriter for MockStream { - async fn write(&self, pkt: &rtp::packet::Packet, _a: &Attributes) -> Result { - let _ = self.rtp_out_modified_tx.send(pkt.clone()).await; - Ok(0) - } -} - -#[async_trait] -impl RTPReader for MockStream { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let pkt = { - let mut rtp_in = self.rtp_in_rx.lock().await; - rtp_in.recv().await.ok_or(Error::ErrIoEOF)? - }; - - let marshaled = pkt.marshal()?; - let n = marshaled.len(); - if n > buf.len() { - return Err(Error::ErrShortBuffer); - } - - buf[..n].copy_from_slice(&marshaled); - Ok((pkt, a.clone())) - } -} - -#[cfg(test)] -mod test { - use tokio::time::Duration; - - use super::*; - use crate::noop::NoOp; - use crate::test::timeout_or_fail; - - #[tokio::test] - async fn test_mock_stream() -> Result<()> { - use rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; - - let s = MockStream::new(&StreamInfo::default(), Arc::new(NoOp)).await; - - s.write_rtcp(&[Box::::default()]) - .await?; - timeout_or_fail(Duration::from_millis(10), s.written_rtcp()).await; - let result = tokio::time::timeout(Duration::from_millis(10), s.written_rtcp()).await; - assert!( - result.is_err(), - "single rtcp packet written, but multiple found" - ); - - s.write_rtp(&rtp::packet::Packet::default()).await?; - timeout_or_fail(Duration::from_millis(10), s.written_rtp()).await; - let result = tokio::time::timeout(Duration::from_millis(10), s.written_rtp()).await; - assert!( - result.is_err(), - "single rtp packet written, but multiple found" - ); - - s.receive_rtcp(vec![Box::::default()]) - .await; - assert!( - timeout_or_fail(Duration::from_millis(10), s.read_rtcp()) - .await - .is_some(), - "read rtcp returned error", - ); - let result = tokio::time::timeout(Duration::from_millis(10), s.read_rtcp()).await; - assert!( - result.is_err(), - "single rtcp packet written, but multiple found" - ); - - s.receive_rtp(rtp::packet::Packet::default()).await; - assert!( - timeout_or_fail(Duration::from_millis(10), s.read_rtp()) - .await - .is_some(), - "read rtp returned error", - ); - let result = tokio::time::timeout(Duration::from_millis(10), s.read_rtp()).await; - assert!( - result.is_err(), - "single rtp packet written, but multiple found" - ); - - s.close().await?; - - Ok(()) - } -} diff --git a/interceptor/src/mock/mock_time.rs b/interceptor/src/mock/mock_time.rs deleted file mode 100644 index 566ae436e..000000000 --- a/interceptor/src/mock/mock_time.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use util::sync::Mutex; - -/// MockTime is a helper to replace SystemTime::now() for testing purposes. -pub struct MockTime { - cur_now: Mutex, -} - -impl Default for MockTime { - fn default() -> Self { - MockTime { - cur_now: Mutex::new(SystemTime::UNIX_EPOCH), - } - } -} - -impl MockTime { - /// set_now sets the current time. - pub fn set_now(&self, now: SystemTime) { - let mut cur_now = self.cur_now.lock(); - *cur_now = now; - } - - /// now returns the current time. - pub fn now(&self) -> SystemTime { - let cur_now = self.cur_now.lock(); - *cur_now - } - - /// advance advances duration d - pub fn advance(&mut self, d: Duration) { - let mut cur_now = self.cur_now.lock(); - *cur_now = cur_now.checked_add(d).unwrap_or(*cur_now); - } -} diff --git a/interceptor/src/mock/mod.rs b/interceptor/src/mock/mod.rs deleted file mode 100644 index 0a561b8ff..000000000 --- a/interceptor/src/mock/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod mock_builder; -pub mod mock_interceptor; -pub mod mock_stream; -pub mod mock_time; diff --git a/interceptor/src/nack/generator/generator_stream.rs b/interceptor/src/nack/generator/generator_stream.rs deleted file mode 100644 index 8cd5a108c..000000000 --- a/interceptor/src/nack/generator/generator_stream.rs +++ /dev/null @@ -1,314 +0,0 @@ -use util::sync::Mutex; - -use super::*; -use crate::nack::UINT16SIZE_HALF; - -struct GeneratorStreamInternal { - packets: Vec, - size: u16, - end: u16, - started: bool, - last_consecutive: u16, -} - -impl GeneratorStreamInternal { - fn new(log2_size_minus_6: u8) -> Self { - GeneratorStreamInternal { - packets: vec![0u64; 1 << log2_size_minus_6], - size: 1 << (log2_size_minus_6 + 6), - end: 0, - started: false, - last_consecutive: 0, - } - } - - fn add(&mut self, seq: u16) { - if !self.started { - self.set_received(seq); - self.end = seq; - self.started = true; - self.last_consecutive = seq; - return; - } - - let last_consecutive_plus1 = self.last_consecutive.wrapping_add(1); - let diff = seq.wrapping_sub(self.end); - if diff == 0 { - return; - } else if diff < UINT16SIZE_HALF { - // this means a positive diff, in other words seq > end (with counting for rollovers) - let mut i = self.end.wrapping_add(1); - while i != seq { - // clear packets between end and seq (these may contain packets from a "size" ago) - self.del_received(i); - i = i.wrapping_add(1); - } - self.end = seq; - - let seq_sub_last_consecutive = seq.wrapping_sub(self.last_consecutive); - if last_consecutive_plus1 == seq { - self.last_consecutive = seq; - } else if seq_sub_last_consecutive > self.size { - let diff = seq.wrapping_sub(self.size); - self.last_consecutive = diff; - self.fix_last_consecutive(); // there might be valid packets at the beginning of the buffer now - } - } else if last_consecutive_plus1 == seq { - // negative diff, seq < end (with counting for rollovers) - self.last_consecutive = seq; - self.fix_last_consecutive(); // there might be other valid packets after seq - } - - self.set_received(seq); - } - - fn get(&self, seq: u16) -> bool { - let diff = self.end.wrapping_sub(seq); - if diff >= UINT16SIZE_HALF { - return false; - } - - if diff >= self.size { - return false; - } - - self.get_received(seq) - } - - fn missing_seq_numbers(&self, skip_last_n: u16) -> Vec { - let until = self.end.wrapping_sub(skip_last_n); - let diff = until.wrapping_sub(self.last_consecutive); - if diff >= UINT16SIZE_HALF { - // until < s.last_consecutive (counting for rollover) - return vec![]; - } - - let mut missing_packet_seq_nums = vec![]; - let mut i = self.last_consecutive.wrapping_add(1); - let util_plus1 = until.wrapping_add(1); - while i != util_plus1 { - if !self.get_received(i) { - missing_packet_seq_nums.push(i); - } - i = i.wrapping_add(1); - } - - missing_packet_seq_nums - } - - fn set_received(&mut self, seq: u16) { - let pos = (seq % self.size) as usize; - self.packets[pos / 64] |= 1u64 << (pos % 64); - } - - fn del_received(&mut self, seq: u16) { - let pos = (seq % self.size) as usize; - self.packets[pos / 64] &= u64::MAX ^ (1u64 << (pos % 64)); - } - - fn get_received(&self, seq: u16) -> bool { - let pos = (seq % self.size) as usize; - (self.packets[pos / 64] & (1u64 << (pos % 64))) != 0 - } - - fn fix_last_consecutive(&mut self) { - let mut i = self.last_consecutive.wrapping_add(1); - while i != self.end.wrapping_add(1) && self.get_received(i) { - // find all consecutive packets - i = i.wrapping_add(1); - } - self.last_consecutive = i.wrapping_sub(1); - } -} - -pub(super) struct GeneratorStream { - parent_rtp_reader: Arc, - - internal: Mutex, -} - -impl GeneratorStream { - pub(super) fn new(log2_size_minus_6: u8, reader: Arc) -> Self { - GeneratorStream { - parent_rtp_reader: reader, - internal: Mutex::new(GeneratorStreamInternal::new(log2_size_minus_6)), - } - } - - pub(super) fn missing_seq_numbers(&self, skip_last_n: u16) -> Vec { - let internal = self.internal.lock(); - internal.missing_seq_numbers(skip_last_n) - } - - pub(super) fn add(&self, seq: u16) { - let mut internal = self.internal.lock(); - internal.add(seq); - } -} - -/// RTPReader is used by Interceptor.bind_remote_stream. -#[async_trait] -impl RTPReader for GeneratorStream { - /// read a rtp packet - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let (pkt, attr) = self.parent_rtp_reader.read(buf, a).await?; - - self.add(pkt.header.sequence_number); - - Ok((pkt, attr)) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_generator_stream() -> Result<()> { - let tests: Vec = vec![ - 0, 1, 127, 128, 129, 511, 512, 513, 32767, 32768, 32769, 65407, 65408, 65409, 65534, - 65535, - ]; - for start in tests { - let mut rl = GeneratorStreamInternal::new(1); - - let all = |min: u16, max: u16| -> Vec { - let mut result = vec![]; - let mut i = min; - let max_plus_1 = max.wrapping_add(1); - while i != max_plus_1 { - result.push(i); - i = i.wrapping_add(1); - } - result - }; - - let join = |parts: &[&[u16]]| -> Vec { - let mut result = vec![]; - for p in parts { - result.extend_from_slice(p); - } - result - }; - - let add = |rl: &mut GeneratorStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - rl.add(seq); - } - }; - - let assert_get = |rl: &GeneratorStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - assert!(rl.get(seq), "not found: {seq}"); - } - }; - - let assert_not_get = |rl: &GeneratorStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - assert!( - !rl.get(seq), - "packet found: start {}, n {}, seq {}", - start, - *n, - seq - ); - } - }; - - let assert_missing = |rl: &GeneratorStreamInternal, skip_last_n: u16, nums: &[u16]| { - let missing = rl.missing_seq_numbers(skip_last_n); - let mut want = vec![]; - for n in nums { - let seq = start.wrapping_add(*n); - want.push(seq); - } - assert_eq!(want, missing, "missing want/got, "); - }; - - let assert_last_consecutive = |rl: &GeneratorStreamInternal, last_consecutive: u16| { - let want = last_consecutive.wrapping_add(start); - assert_eq!(rl.last_consecutive, want, "invalid last_consecutive want"); - }; - - add(&mut rl, &[0]); - assert_get(&rl, &[0]); - assert_missing(&rl, 0, &[]); - assert_last_consecutive(&rl, 0); // first element added - - add(&mut rl, &all(1, 127)); - assert_get(&rl, &all(1, 127)); - assert_missing(&rl, 0, &[]); - assert_last_consecutive(&rl, 127); - - add(&mut rl, &[128]); - assert_get(&rl, &[128]); - assert_not_get(&rl, &[0]); - assert_missing(&rl, 0, &[]); - assert_last_consecutive(&rl, 128); - - add(&mut rl, &[130]); - assert_get(&rl, &[130]); - assert_not_get(&rl, &[1, 2, 129]); - assert_missing(&rl, 0, &[129]); - assert_last_consecutive(&rl, 128); - - add(&mut rl, &[333]); - assert_get(&rl, &[333]); - assert_not_get(&rl, &all(0, 332)); - assert_missing(&rl, 0, &all(206, 332)); // all 127 elements missing before 333 - assert_missing(&rl, 10, &all(206, 323)); // skip last 10 packets (324-333) from check - assert_last_consecutive(&rl, 205); // lastConsecutive is still out of the buffer - - add(&mut rl, &[329]); - assert_get(&rl, &[329]); - assert_missing(&rl, 0, &join(&[&all(206, 328), &all(330, 332)])); - assert_missing(&rl, 5, &join(&[&all(206, 328)])); // skip last 5 packets (329-333) from check - assert_last_consecutive(&rl, 205); - - add(&mut rl, &all(207, 320)); - assert_get(&rl, &all(207, 320)); - assert_missing(&rl, 0, &join(&[&[206], &all(321, 328), &all(330, 332)])); - assert_last_consecutive(&rl, 205); - - add(&mut rl, &[334]); - assert_get(&rl, &[334]); - assert_not_get(&rl, &[206]); - assert_missing(&rl, 0, &join(&[&all(321, 328), &all(330, 332)])); - assert_last_consecutive(&rl, 320); // head of buffer is full of consecutive packages - - add(&mut rl, &all(322, 328)); - assert_get(&rl, &all(322, 328)); - assert_missing(&rl, 0, &join(&[&[321], &all(330, 332)])); - assert_last_consecutive(&rl, 320); - - add(&mut rl, &[321]); - assert_get(&rl, &[321]); - assert_missing(&rl, 0, &all(330, 332)); - assert_last_consecutive(&rl, 329); // after adding a single missing packet, lastConsecutive should jump forward - } - - Ok(()) - } - - #[test] - fn test_generator_stream_rollover() { - let mut rl = GeneratorStreamInternal::new(1); - // Make sure it doesn't panic. - rl.add(65533); - rl.add(65535); - rl.add(65534); - - let mut rl = GeneratorStreamInternal::new(1); - // Make sure it doesn't panic. - rl.add(65534); - rl.add(0); - rl.add(65535); - } -} diff --git a/interceptor/src/nack/generator/generator_test.rs b/interceptor/src/nack/generator/generator_test.rs deleted file mode 100644 index b0ed1805e..000000000 --- a/interceptor/src/nack/generator/generator_test.rs +++ /dev/null @@ -1,66 +0,0 @@ -use rtcp::transport_feedbacks::transport_layer_nack::TransportLayerNack; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::stream_info::RTCPFeedback; -use crate::test::timeout_or_fail; - -#[tokio::test] -async fn test_generator_interceptor() -> Result<()> { - const INTERVAL: Duration = Duration::from_millis(10); - let icpr: Arc = Generator::builder() - .with_log2_size_minus_6(0) - .with_skip_last_n(2) - .with_interval(INTERVAL) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtcp_feedback: vec![RTCPFeedback { - typ: "nack".to_owned(), - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - for seq_num in [10, 11, 12, 14, 16, 18] { - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq_num, - ..Default::default() - }, - ..Default::default() - }) - .await; - - let r = timeout_or_fail(Duration::from_millis(10), stream.read_rtp()) - .await - .expect("A read packet") - .expect("Not an error"); - assert_eq!(r.header.sequence_number, seq_num); - } - - tokio::time::sleep(INTERVAL * 2).await; // wait for at least 2 nack packets - - // ignore the first nack, it might only contain the sequence id 13 as missing - let _ = stream.written_rtcp().await; - - let r = timeout_or_fail(Duration::from_millis(10), stream.written_rtcp()) - .await - .expect("Write rtcp"); - if let Some(p) = r[0].as_any().downcast_ref::() { - assert_eq!(p.nacks[0].packet_id, 13); - assert_eq!(p.nacks[0].lost_packets, 0b10); // we want packets: 13, 15 (not packet 17, because skipLastN is setReceived to 2) - } else { - panic!("single packet RTCP Compound Packet expected"); - } - - stream.close().await?; - - Ok(()) -} diff --git a/interceptor/src/nack/generator/mod.rs b/interceptor/src/nack/generator/mod.rs deleted file mode 100644 index 93d13dac4..000000000 --- a/interceptor/src/nack/generator/mod.rs +++ /dev/null @@ -1,255 +0,0 @@ -mod generator_stream; -#[cfg(test)] -mod generator_test; - -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; - -use async_trait::async_trait; -use generator_stream::GeneratorStream; -use rtcp::transport_feedbacks::transport_layer_nack::{ - nack_pairs_from_sequence_numbers, TransportLayerNack, -}; -use tokio::sync::{mpsc, Mutex}; -use waitgroup::WaitGroup; - -use crate::error::{Error, Result}; -use crate::nack::stream_support_nack; -use crate::stream_info::StreamInfo; -use crate::{ - Attributes, Interceptor, InterceptorBuilder, RTCPReader, RTCPWriter, RTPReader, RTPWriter, -}; - -/// GeneratorBuilder can be used to configure Generator Interceptor -#[derive(Default)] -pub struct GeneratorBuilder { - log2_size_minus_6: Option, - skip_last_n: Option, - interval: Option, -} - -impl GeneratorBuilder { - /// with_size sets the size of the interceptor. - /// Size must be one of: 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 - pub fn with_log2_size_minus_6(mut self, log2_size_minus_6: u8) -> GeneratorBuilder { - self.log2_size_minus_6 = Some(log2_size_minus_6); - self - } - - /// with_skip_last_n sets the number of packets (n-1 packets before the last received packets) to ignore when generating - /// nack requests. - pub fn with_skip_last_n(mut self, skip_last_n: u16) -> GeneratorBuilder { - self.skip_last_n = Some(skip_last_n); - self - } - - /// with_interval sets the nack send interval for the interceptor - pub fn with_interval(mut self, interval: Duration) -> GeneratorBuilder { - self.interval = Some(interval); - self - } -} - -impl InterceptorBuilder for GeneratorBuilder { - fn build(&self, _id: &str) -> Result> { - let (close_tx, close_rx) = mpsc::channel(1); - Ok(Arc::new(Generator { - internal: Arc::new(GeneratorInternal { - log2_size_minus_6: if let Some(log2_size_minus_6) = self.log2_size_minus_6 { - log2_size_minus_6 - } else { - 13 - 6 // 8192 = 1 << 13 - }, - skip_last_n: self.skip_last_n.unwrap_or_default(), - interval: if let Some(interval) = self.interval { - interval - } else { - Duration::from_millis(100) - }, - - streams: Mutex::new(HashMap::new()), - close_rx: Mutex::new(Some(close_rx)), - }), - - wg: Mutex::new(Some(WaitGroup::new())), - close_tx: Mutex::new(Some(close_tx)), - })) - } -} - -struct GeneratorInternal { - log2_size_minus_6: u8, - skip_last_n: u16, - interval: Duration, - - streams: Mutex>>, - close_rx: Mutex>>, -} - -/// Generator interceptor generates nack feedback messages. -pub struct Generator { - internal: Arc, - - pub(crate) wg: Mutex>, - pub(crate) close_tx: Mutex>>, -} - -impl Generator { - /// builder returns a new GeneratorBuilder. - pub fn builder() -> GeneratorBuilder { - GeneratorBuilder::default() - } - - async fn is_closed(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_none() - } - - async fn run( - rtcp_writer: Arc, - internal: Arc, - ) -> Result<()> { - let mut ticker = tokio::time::interval(internal.interval); - let mut close_rx = { - let mut close_rx = internal.close_rx.lock().await; - if let Some(close) = close_rx.take() { - close - } else { - return Err(Error::ErrInvalidCloseRx); - } - }; - - let sender_ssrc = rand::random::(); - loop { - tokio::select! { - _ = ticker.tick() =>{ - let nacks = { - let mut nacks = vec![]; - let streams = internal.streams.lock().await; - for (ssrc, stream) in streams.iter() { - let missing = stream.missing_seq_numbers(internal.skip_last_n); - if missing.is_empty(){ - continue; - } - - nacks.push(TransportLayerNack{ - sender_ssrc, - media_ssrc: *ssrc, - nacks: nack_pairs_from_sequence_numbers(&missing), - }); - } - nacks - }; - - let a = Attributes::new(); - for nack in nacks{ - if let Err(err) = rtcp_writer.write(&[Box::new(nack)], &a).await{ - log::warn!("failed sending nack: {}", err); - } - } - } - _ = close_rx.recv() =>{ - return Ok(()); - } - } - } - } -} - -#[async_trait] -impl Interceptor for Generator { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - if self.is_closed().await { - return writer; - } - - let mut w = { - let wait_group = self.wg.lock().await; - wait_group.as_ref().map(|wg| wg.worker()) - }; - let writer2 = Arc::clone(&writer); - let internal = Arc::clone(&self.internal); - tokio::spawn(async move { - let _d = w.take(); - if let Err(err) = Generator::run(writer2, internal).await { - log::warn!("bind_rtcp_writer NACK Generator::run got error: {}", err); - } - }); - - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - _info: &StreamInfo, - writer: Arc, - ) -> Arc { - writer - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, _info: &StreamInfo) {} - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc { - if !stream_support_nack(info) { - return reader; - } - - let stream = Arc::new(GeneratorStream::new( - self.internal.log2_size_minus_6, - reader, - )); - { - let mut streams = self.internal.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - let mut receive_logs = self.internal.streams.lock().await; - receive_logs.remove(&info.ssrc); - } - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - { - let mut wait_group = self.wg.lock().await; - if let Some(wg) = wait_group.take() { - wg.wait().await; - } - } - - Ok(()) - } -} diff --git a/interceptor/src/nack/mod.rs b/interceptor/src/nack/mod.rs deleted file mode 100644 index 87abe5039..000000000 --- a/interceptor/src/nack/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::stream_info::StreamInfo; - -pub mod generator; -pub mod responder; - -const UINT16SIZE_HALF: u16 = 1 << 15; - -fn stream_support_nack(info: &StreamInfo) -> bool { - for fb in &info.rtcp_feedback { - if fb.typ == "nack" && fb.parameter.is_empty() { - return true; - } - } - - false -} diff --git a/interceptor/src/nack/responder/mod.rs b/interceptor/src/nack/responder/mod.rs deleted file mode 100644 index 934a948a2..000000000 --- a/interceptor/src/nack/responder/mod.rs +++ /dev/null @@ -1,203 +0,0 @@ -mod responder_stream; -#[cfg(test)] -mod responder_test; - -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use async_trait::async_trait; -use responder_stream::ResponderStream; -use rtcp::transport_feedbacks::transport_layer_nack::TransportLayerNack; -use tokio::sync::Mutex; - -use crate::error::Result; -use crate::nack::stream_support_nack; -use crate::stream_info::StreamInfo; -use crate::{ - Attributes, Interceptor, InterceptorBuilder, RTCPReader, RTCPWriter, RTPReader, RTPWriter, -}; - -/// GeneratorBuilder can be used to configure Responder Interceptor -#[derive(Default)] -pub struct ResponderBuilder { - log2_size: Option, -} - -impl ResponderBuilder { - /// with_log2_size sets the size of the interceptor. - /// Size must be one of: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 - pub fn with_log2_size(mut self, log2_size: u8) -> ResponderBuilder { - self.log2_size = Some(log2_size); - self - } -} - -impl InterceptorBuilder for ResponderBuilder { - fn build(&self, _id: &str) -> Result> { - Ok(Arc::new(Responder { - internal: Arc::new(ResponderInternal { - log2_size: if let Some(log2_size) = self.log2_size { - log2_size - } else { - 13 // 8192 = 1 << 13 - }, - streams: Arc::new(Mutex::new(HashMap::new())), - }), - })) - } -} - -pub struct ResponderInternal { - log2_size: u8, - streams: Arc>>>, -} - -impl ResponderInternal { - async fn resend_packets( - streams: Arc>>>, - nack: TransportLayerNack, - ) { - let stream = { - let m = streams.lock().await; - if let Some(stream) = m.get(&nack.media_ssrc) { - stream.clone() - } else { - return; - } - }; - - for n in &nack.nacks { - // can't use n.range() since this callback is async fn, - // instead, use NackPair into_iter() - let stream2 = Arc::clone(&stream); - let f = Box::new( - move |seq: u16| -> Pin + Send + 'static>> { - let stream3 = Arc::clone(&stream2); - Box::pin(async move { - if let Some(p) = stream3.get(seq).await { - let a = Attributes::new(); - if let Err(err) = stream3.next_rtp_writer.write(&p, &a).await { - log::warn!("failed resending nacked packet: {}", err); - } - } - true - }) - }, - ); - for packet_id in n.into_iter() { - if !f(packet_id).await { - return; - } - } - } - } -} - -pub struct ResponderRtcpReader { - parent_rtcp_reader: Arc, - internal: Arc, -} - -#[async_trait] -impl RTCPReader for ResponderRtcpReader { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(Vec>, Attributes)> { - let (pkts, attr) = { self.parent_rtcp_reader.read(buf, a).await? }; - for p in &pkts { - if let Some(nack) = p.as_any().downcast_ref::() { - let nack = nack.clone(); - let streams = Arc::clone(&self.internal.streams); - tokio::spawn(async move { - ResponderInternal::resend_packets(streams, nack).await; - }); - } - } - - Ok((pkts, attr)) - } -} - -/// Responder responds to nack feedback messages -pub struct Responder { - internal: Arc, -} - -impl Responder { - /// builder returns a new ResponderBuilder. - pub fn builder() -> ResponderBuilder { - ResponderBuilder::default() - } -} - -#[async_trait] -impl Interceptor for Responder { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - Arc::new(ResponderRtcpReader { - internal: Arc::clone(&self.internal), - parent_rtcp_reader: reader, - }) as Arc - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc { - if !stream_support_nack(info) { - return writer; - } - - let stream = Arc::new(ResponderStream::new(self.internal.log2_size, writer)); - { - let mut streams = self.internal.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - let mut streams = self.internal.streams.lock().await; - streams.remove(&info.ssrc); - } - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - _info: &StreamInfo, - reader: Arc, - ) -> Arc { - reader - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, _info: &StreamInfo) {} - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - Ok(()) - } -} diff --git a/interceptor/src/nack/responder/responder_stream.rs b/interceptor/src/nack/responder/responder_stream.rs deleted file mode 100644 index ec714da6d..000000000 --- a/interceptor/src/nack/responder/responder_stream.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::sync::Mutex; - -use crate::error::Result; -use crate::nack::UINT16SIZE_HALF; -use crate::{Attributes, RTPWriter}; - -struct ResponderStreamInternal { - packets: Vec>, - size: u16, - last_added: u16, - started: bool, -} - -impl ResponderStreamInternal { - fn new(log2_size: u8) -> Self { - ResponderStreamInternal { - packets: vec![None; 1 << log2_size], - size: 1 << log2_size, - last_added: 0, - started: false, - } - } - - fn add(&mut self, packet: &rtp::packet::Packet) { - let seq = packet.header.sequence_number; - if !self.started { - self.packets[(seq % self.size) as usize] = Some(packet.clone()); - self.last_added = seq; - self.started = true; - return; - } - - let diff = seq.wrapping_sub(self.last_added); - if diff == 0 { - return; - } else if diff < UINT16SIZE_HALF { - let mut i = self.last_added.wrapping_add(1); - while i != seq { - self.packets[(i % self.size) as usize] = None; - i = i.wrapping_add(1); - } - } - - self.packets[(seq % self.size) as usize] = Some(packet.clone()); - self.last_added = seq; - } - - fn get(&self, seq: u16) -> Option<&rtp::packet::Packet> { - let diff = self.last_added.wrapping_sub(seq); - if diff >= UINT16SIZE_HALF { - return None; - } - - if diff >= self.size { - return None; - } - - self.packets[(seq % self.size) as usize].as_ref() - } -} - -pub(super) struct ResponderStream { - internal: Mutex, - pub(super) next_rtp_writer: Arc, -} - -impl ResponderStream { - pub(super) fn new(log2_size: u8, writer: Arc) -> Self { - ResponderStream { - internal: Mutex::new(ResponderStreamInternal::new(log2_size)), - next_rtp_writer: writer, - } - } - - async fn add(&self, pkt: &rtp::packet::Packet) { - let mut internal = self.internal.lock().await; - internal.add(pkt); - } - - pub(super) async fn get(&self, seq: u16) -> Option { - let internal = self.internal.lock().await; - internal.get(seq).cloned() - } -} - -/// RTPWriter is used by Interceptor.bind_local_stream. -#[async_trait] -impl RTPWriter for ResponderStream { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, a: &Attributes) -> Result { - self.add(pkt).await; - - self.next_rtp_writer.write(pkt, a).await - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_responder_stream() -> Result<()> { - let tests: Vec = vec![ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 511, 512, 513, 32767, 32768, 32769, 65527, 65528, 65529, - 65530, 65531, 65532, 65533, 65534, 65535, - ]; - for start in tests { - let mut sb = ResponderStreamInternal::new(3); - - let add = |sb: &mut ResponderStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - sb.add(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq, - ..Default::default() - }, - ..Default::default() - }); - } - }; - - let assert_get = |sb: &ResponderStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - if let Some(packet) = sb.get(seq) { - assert_eq!( - packet.header.sequence_number, seq, - "packet for {} returned with incorrect SequenceNumber: {}", - seq, packet.header.sequence_number - ); - } else { - panic!("packet not found: {seq}"); - } - } - }; - - let assert_not_get = |sb: &ResponderStreamInternal, nums: &[u16]| { - for n in nums { - let seq = start.wrapping_add(*n); - if let Some(packet) = sb.get(seq) { - panic!( - "packet found for {}: {}", - seq, packet.header.sequence_number - ); - } - } - }; - - add(&mut sb, &[0, 1, 2, 3, 4, 5, 6, 7]); - assert_get(&sb, &[0, 1, 2, 3, 4, 5, 6, 7]); - - add(&mut sb, &[8]); - assert_get(&sb, &[8]); - assert_not_get(&sb, &[0]); - - add(&mut sb, &[10]); - assert_get(&sb, &[10]); - assert_not_get(&sb, &[1, 2, 9]); - - add(&mut sb, &[22]); - assert_get(&sb, &[22]); - assert_not_get( - &sb, - &[ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - ], - ); - } - - Ok(()) - } -} diff --git a/interceptor/src/nack/responder/responder_test.rs b/interceptor/src/nack/responder/responder_test.rs deleted file mode 100644 index e61cf0ea6..000000000 --- a/interceptor/src/nack/responder/responder_test.rs +++ /dev/null @@ -1,76 +0,0 @@ -use rtcp::transport_feedbacks::transport_layer_nack::{NackPair, TransportLayerNack}; -use tokio::time::Duration; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::stream_info::RTCPFeedback; -use crate::test::timeout_or_fail; - -#[tokio::test] -async fn test_responder_interceptor() -> Result<()> { - let icpr: Arc = - Responder::builder().with_log2_size(3).build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtcp_feedback: vec![RTCPFeedback { - typ: "nack".to_owned(), - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - for seq_num in [10, 11, 12, 14, 15] { - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq_num, - ..Default::default() - }, - ..Default::default() - }) - .await?; - - let p = timeout_or_fail(Duration::from_millis(10), stream.written_rtp()) - .await - .expect("A packet"); - assert_eq!(p.header.sequence_number, seq_num); - } - - stream - .receive_rtcp(vec![Box::new(TransportLayerNack { - media_ssrc: 1, - sender_ssrc: 2, - nacks: vec![ - NackPair { - packet_id: 11, - lost_packets: 0b1011, - }, // sequence numbers: 11, 12, 13, 15 - ], - })]) - .await; - - // seq number 13 was never sent, so it can't be resent - for seq_num in [11, 12, 15] { - if let Ok(r) = tokio::time::timeout(Duration::from_millis(50), stream.written_rtp()).await { - if let Some(p) = r { - assert_eq!(p.header.sequence_number, seq_num); - } else { - panic!("seq_num {seq_num} is not sent due to channel closed"); - } - } else { - panic!("seq_num {seq_num} is not sent yet"); - } - } - - let result = tokio::time::timeout(Duration::from_millis(10), stream.written_rtp()).await; - assert!(result.is_err(), "no more rtp packets expected"); - - stream.close().await?; - - Ok(()) -} diff --git a/interceptor/src/noop.rs b/interceptor/src/noop.rs deleted file mode 100644 index 597c57a8f..000000000 --- a/interceptor/src/noop.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::*; -use crate::error::Result; - -/// NoOp is an Interceptor that does not modify any packets. It can embedded in other interceptors, so it's -/// possible to implement only a subset of the methods. -pub struct NoOp; - -#[async_trait] -impl Interceptor for NoOp { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - _info: &StreamInfo, - writer: Arc, - ) -> Arc { - writer - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, _info: &StreamInfo) {} - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - _info: &StreamInfo, - reader: Arc, - ) -> Arc { - reader - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, _info: &StreamInfo) {} - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - Ok(()) - } -} - -#[async_trait] -impl RTPReader for NoOp { - async fn read( - &self, - _buf: &mut [u8], - a: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - Ok((rtp::packet::Packet::default(), a.clone())) - } -} - -#[async_trait] -impl RTCPReader for NoOp { - async fn read( - &self, - _buf: &mut [u8], - a: &Attributes, - ) -> Result<(Vec>, Attributes)> { - Ok((vec![], a.clone())) - } -} diff --git a/interceptor/src/registry.rs b/interceptor/src/registry.rs deleted file mode 100644 index 6a3ed6c98..000000000 --- a/interceptor/src/registry.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::sync::Arc; - -use crate::chain::Chain; -use crate::error::Result; -use crate::noop::NoOp; -use crate::{Interceptor, InterceptorBuilder}; - -/// Registry is a collector for interceptors. -#[derive(Default)] -pub struct Registry { - builders: Vec>, -} - -impl Registry { - pub fn new() -> Self { - Registry { builders: vec![] } - } - - /// add adds a new InterceptorBuilder to the registry. - pub fn add(&mut self, builder: Box) { - self.builders.push(builder); - } - - /// build constructs a single Interceptor from an InterceptorRegistry - pub fn build(&self, id: &str) -> Result> { - if self.builders.is_empty() { - return Ok(Arc::new(NoOp {})); - } - - self.build_chain(id) - .map(|c| Arc::new(c) as Arc) - } - - /// build_chain constructs a non-type erased Chain from an Interceptor registry. - pub fn build_chain(&self, id: &str) -> Result { - if self.builders.is_empty() { - return Ok(Chain::new(vec![Arc::new(NoOp {})])); - } - - let interceptors: Result> = self.builders.iter().map(|b| b.build(id)).collect(); - - Ok(Chain::new(interceptors?)) - } -} diff --git a/interceptor/src/report/mod.rs b/interceptor/src/report/mod.rs deleted file mode 100644 index bb366a2a0..000000000 --- a/interceptor/src/report/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use tokio::sync::{mpsc, Mutex}; -use waitgroup::WaitGroup; - -pub mod receiver; -pub mod sender; - -use receiver::{ReceiverReport, ReceiverReportInternal}; -use sender::{SenderReport, SenderReportInternal}; - -use crate::error::Result; -use crate::{Interceptor, InterceptorBuilder}; - -type FnTimeGen = Arc SystemTime + Sync + 'static + Send>; - -/// ReceiverBuilder can be used to configure ReceiverReport Interceptor. -#[derive(Default)] -pub struct ReportBuilder { - is_rr: bool, - interval: Option, - now: Option, -} - -impl ReportBuilder { - /// with_interval sets send interval for the interceptor. - pub fn with_interval(mut self, interval: Duration) -> ReportBuilder { - self.interval = Some(interval); - self - } - - /// with_now_fn sets an alternative for the time.Now function. - pub fn with_now_fn(mut self, now: FnTimeGen) -> ReportBuilder { - self.now = Some(now); - self - } - - fn build_rr(&self) -> ReceiverReport { - let (close_tx, close_rx) = mpsc::channel(1); - ReceiverReport { - internal: Arc::new(ReceiverReportInternal { - interval: if let Some(interval) = &self.interval { - *interval - } else { - Duration::from_secs(1) - }, - now: self.now.clone(), - streams: Mutex::new(HashMap::new()), - close_rx: Mutex::new(Some(close_rx)), - }), - - wg: Mutex::new(Some(WaitGroup::new())), - close_tx: Mutex::new(Some(close_tx)), - } - } - - fn build_sr(&self) -> SenderReport { - let (close_tx, close_rx) = mpsc::channel(1); - SenderReport { - internal: Arc::new(SenderReportInternal { - interval: if let Some(interval) = &self.interval { - *interval - } else { - Duration::from_secs(1) - }, - now: self.now.clone(), - streams: Mutex::new(HashMap::new()), - close_rx: Mutex::new(Some(close_rx)), - }), - - wg: Mutex::new(Some(WaitGroup::new())), - close_tx: Mutex::new(Some(close_tx)), - } - } -} - -impl InterceptorBuilder for ReportBuilder { - fn build(&self, _id: &str) -> Result> { - if self.is_rr { - Ok(Arc::new(self.build_rr())) - } else { - Ok(Arc::new(self.build_sr())) - } - } -} diff --git a/interceptor/src/report/receiver/mod.rs b/interceptor/src/report/receiver/mod.rs deleted file mode 100644 index ef3381949..000000000 --- a/interceptor/src/report/receiver/mod.rs +++ /dev/null @@ -1,225 +0,0 @@ -mod receiver_stream; -#[cfg(test)] -mod receiver_test; - -use std::collections::HashMap; -use std::time::{Duration, SystemTime}; - -use receiver_stream::ReceiverStream; -use tokio::sync::{mpsc, Mutex}; -use waitgroup::WaitGroup; - -use super::*; -use crate::error::Error; -use crate::*; - -pub(crate) struct ReceiverReportInternal { - pub(crate) interval: Duration, - pub(crate) now: Option, - pub(crate) streams: Mutex>>, - pub(crate) close_rx: Mutex>>, -} - -pub(crate) struct ReceiverReportRtcpReader { - pub(crate) internal: Arc, - pub(crate) parent_rtcp_reader: Arc, -} - -#[async_trait] -impl RTCPReader for ReceiverReportRtcpReader { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(Vec>, Attributes)> { - let (pkts, attr) = self.parent_rtcp_reader.read(buf, a).await?; - - let now = if let Some(f) = &self.internal.now { - f() - } else { - SystemTime::now() - }; - - for p in &pkts { - if let Some(sr) = p - .as_any() - .downcast_ref::() - { - let stream = { - let m = self.internal.streams.lock().await; - m.get(&sr.ssrc).cloned() - }; - if let Some(stream) = stream { - stream.process_sender_report(now, sr); - } - } - } - - Ok((pkts, attr)) - } -} - -/// ReceiverReport interceptor generates receiver reports. -pub struct ReceiverReport { - pub(crate) internal: Arc, - - pub(crate) wg: Mutex>, - pub(crate) close_tx: Mutex>>, -} - -impl ReceiverReport { - /// builder returns a new ReportBuilder. - pub fn builder() -> ReportBuilder { - ReportBuilder { - is_rr: true, - ..Default::default() - } - } - - async fn is_closed(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_none() - } - - async fn run( - rtcp_writer: Arc, - internal: Arc, - ) -> Result<()> { - let mut ticker = tokio::time::interval(internal.interval); - let mut close_rx = { - let mut close_rx = internal.close_rx.lock().await; - if let Some(close) = close_rx.take() { - close - } else { - return Err(Error::ErrInvalidCloseRx); - } - }; - - loop { - tokio::select! { - _ = ticker.tick() =>{ - // TODO(cancel safety): This branch isn't cancel safe - - let now = if let Some(f) = &internal.now { - f() - } else { - SystemTime::now() - }; - let streams:Vec> = { - let m = internal.streams.lock().await; - m.values().cloned().collect() - }; - for stream in streams { - let pkt = stream.generate_report(now); - - let a = Attributes::new(); - if let Err(err) = rtcp_writer.write(&[Box::new(pkt)], &a).await{ - log::warn!("failed sending: {}", err); - } - } - } - _ = close_rx.recv() =>{ - return Ok(()); - } - } - } - } -} - -#[async_trait] -impl Interceptor for ReceiverReport { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - Arc::new(ReceiverReportRtcpReader { - internal: Arc::clone(&self.internal), - parent_rtcp_reader: reader, - }) - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - if self.is_closed().await { - return writer; - } - - let mut w = { - let wait_group = self.wg.lock().await; - wait_group.as_ref().map(|wg| wg.worker()) - }; - let writer2 = Arc::clone(&writer); - let internal = Arc::clone(&self.internal); - tokio::spawn(async move { - let _d = w.take(); - if let Err(err) = ReceiverReport::run(writer2, internal).await { - log::warn!("bind_rtcp_writer ReceiverReport::run got error: {}", err); - } - }); - - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - _info: &StreamInfo, - writer: Arc, - ) -> Arc { - writer - } - - /// UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, _info: &StreamInfo) {} - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc { - let stream = Arc::new(ReceiverStream::new( - info.ssrc, - info.clock_rate, - reader, - self.internal.now.clone(), - )); - { - let mut streams = self.internal.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - let mut streams = self.internal.streams.lock().await; - streams.remove(&info.ssrc); - } - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - { - let mut wait_group = self.wg.lock().await; - if let Some(wg) = wait_group.take() { - wg.wait().await; - } - } - - Ok(()) - } -} diff --git a/interceptor/src/report/receiver/receiver_stream.rs b/interceptor/src/report/receiver/receiver_stream.rs deleted file mode 100644 index d170922e8..000000000 --- a/interceptor/src/report/receiver/receiver_stream.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::time::SystemTime; - -use async_trait::async_trait; -use util::sync::Mutex; - -use super::*; -use crate::{Attributes, RTPReader}; - -struct ReceiverStreamInternal { - ssrc: u32, - receiver_ssrc: u32, - clock_rate: f64, - - packets: Vec, - started: bool, - seq_num_cycles: u16, - last_seq_num: i32, - last_report_seq_num: i32, - last_rtp_time_rtp: u32, - last_rtp_time_time: SystemTime, - jitter: f64, - last_sender_report: u32, - last_sender_report_time: SystemTime, - total_lost: u32, -} - -impl ReceiverStreamInternal { - fn set_received(&mut self, seq: u16) { - let pos = (seq as usize) % self.packets.len(); - self.packets[pos / 64] |= 1 << (pos % 64); - } - - fn del_received(&mut self, seq: u16) { - let pos = (seq as usize) % self.packets.len(); - self.packets[pos / 64] &= u64::MAX ^ (1u64 << (pos % 64)); - } - - fn get_received(&self, seq: u16) -> bool { - let pos = (seq as usize) % self.packets.len(); - (self.packets[pos / 64] & (1 << (pos % 64))) != 0 - } - - fn process_rtp(&mut self, now: SystemTime, pkt: &rtp::packet::Packet) { - if !self.started { - // first frame - self.started = true; - self.set_received(pkt.header.sequence_number); - self.last_seq_num = pkt.header.sequence_number as i32; - self.last_report_seq_num = pkt.header.sequence_number as i32 - 1; - } else { - // following frames - self.set_received(pkt.header.sequence_number); - - let diff = pkt.header.sequence_number as i32 - self.last_seq_num; - if !(-0x0FFF..=0).contains(&diff) { - // overflow - if diff < -0x0FFF { - self.seq_num_cycles += 1; - } - - // set missing packets as missing - for i in self.last_seq_num + 1..pkt.header.sequence_number as i32 { - self.del_received(i as u16); - } - - self.last_seq_num = pkt.header.sequence_number as i32; - } - - // compute jitter - // https://tools.ietf.org/html/rfc3550#page-39 - let d = now - .duration_since(self.last_rtp_time_time) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs_f64() - * self.clock_rate - - (pkt.header.timestamp as f64 - self.last_rtp_time_rtp as f64); - self.jitter += (d.abs() - self.jitter) / 16.0; - } - - self.last_rtp_time_rtp = pkt.header.timestamp; - self.last_rtp_time_time = now; - } - - fn process_sender_report(&mut self, now: SystemTime, sr: &rtcp::sender_report::SenderReport) { - self.last_sender_report = (sr.ntp_time >> 16) as u32; - self.last_sender_report_time = now; - } - - fn generate_report(&mut self, now: SystemTime) -> rtcp::receiver_report::ReceiverReport { - let total_since_report = (self.last_seq_num - self.last_report_seq_num) as u16; - let mut total_lost_since_report = { - if self.last_seq_num == self.last_report_seq_num { - 0 - } else { - let mut ret = 0u32; - let mut i = (self.last_report_seq_num + 1) as u16; - while i != self.last_seq_num as u16 { - if !self.get_received(i) { - ret += 1; - } - i = i.wrapping_add(1); - } - ret - } - }; - - self.total_lost += total_lost_since_report; - - // allow up to 24 bits - if total_lost_since_report > 0xFFFFFF { - total_lost_since_report = 0xFFFFFF; - } - if self.total_lost > 0xFFFFFF { - self.total_lost = 0xFFFFFF - } - - let r = rtcp::receiver_report::ReceiverReport { - ssrc: self.receiver_ssrc, - reports: vec![rtcp::reception_report::ReceptionReport { - ssrc: self.ssrc, - last_sequence_number: (self.seq_num_cycles as u32) << 16 - | (self.last_seq_num as u32), - last_sender_report: self.last_sender_report, - fraction_lost: ((total_lost_since_report * 256) as f64 / total_since_report as f64) - as u8, - total_lost: self.total_lost, - delay: { - if self.last_sender_report_time == SystemTime::UNIX_EPOCH { - 0 - } else { - match now.duration_since(self.last_sender_report_time) { - Ok(d) => (d.as_secs_f64() * 65536.0) as u32, - Err(_) => 0, - } - } - }, - jitter: self.jitter as u32, - }], - ..Default::default() - }; - - self.last_report_seq_num = self.last_seq_num; - - r - } -} - -pub(crate) struct ReceiverStream { - parent_rtp_reader: Arc, - now: Option, - - internal: Mutex, -} - -impl ReceiverStream { - pub(crate) fn new( - ssrc: u32, - clock_rate: u32, - reader: Arc, - now: Option, - ) -> Self { - let receiver_ssrc = rand::random::(); - ReceiverStream { - parent_rtp_reader: reader, - now, - - internal: Mutex::new(ReceiverStreamInternal { - ssrc, - receiver_ssrc, - clock_rate: clock_rate as f64, - - packets: vec![0u64; 128], - started: false, - seq_num_cycles: 0, - last_seq_num: 0, - last_report_seq_num: 0, - last_rtp_time_rtp: 0, - last_rtp_time_time: SystemTime::UNIX_EPOCH, - jitter: 0.0, - last_sender_report: 0, - last_sender_report_time: SystemTime::UNIX_EPOCH, - total_lost: 0, - }), - } - } - - pub(crate) fn process_rtp(&self, now: SystemTime, pkt: &rtp::packet::Packet) { - let mut internal = self.internal.lock(); - internal.process_rtp(now, pkt); - } - - pub(crate) fn process_sender_report( - &self, - now: SystemTime, - sr: &rtcp::sender_report::SenderReport, - ) { - let mut internal = self.internal.lock(); - internal.process_sender_report(now, sr); - } - - pub(crate) fn generate_report(&self, now: SystemTime) -> rtcp::receiver_report::ReceiverReport { - let mut internal = self.internal.lock(); - internal.generate_report(now) - } -} - -/// RTPReader is used by Interceptor.bind_remote_stream. -#[async_trait] -impl RTPReader for ReceiverStream { - /// read a rtp packet - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let (pkt, attr) = self.parent_rtp_reader.read(buf, a).await?; - - let now = if let Some(f) = &self.now { - f() - } else { - SystemTime::now() - }; - self.process_rtp(now, &pkt); - - Ok((pkt, attr)) - } -} diff --git a/interceptor/src/report/receiver/receiver_test.rs b/interceptor/src/report/receiver/receiver_test.rs deleted file mode 100644 index 77fa4a929..000000000 --- a/interceptor/src/report/receiver/receiver_test.rs +++ /dev/null @@ -1,772 +0,0 @@ -//use bytes::Bytes; -use chrono::prelude::*; -use rtp::extension::abs_send_time_extension::unix2ntp; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::mock::mock_time::MockTime; - -#[tokio::test] -async fn test_receiver_interceptor_before_any_packet() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: 0, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_after_rtp_packets() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - for i in 0..10u16 { - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: i, - ..Default::default() - }, - ..Default::default() - }) - .await; - } - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 9, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: 0, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_after_rtp_and_rtcp_packets() -> Result<()> { - let rtp_time: SystemTime = Utc.with_ymd_and_hms(2009, 10, 23, 0, 0, 0).unwrap().into(); - - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - for i in 0..10u16 { - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: i, - ..Default::default() - }, - ..Default::default() - }) - .await; - } - - let now: SystemTime = Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 1).unwrap().into(); - let rt = 987654321u32.wrapping_add( - (now.duration_since(rtp_time) - .unwrap_or(Duration::from_secs(0)) - .as_secs_f64() - * 90000.0) as u32, - ); - stream - .receive_rtcp(vec![Box::new(rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(now), - rtp_time: rt, - packet_count: 10, - octet_count: 0, - ..Default::default() - })]) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 9, - last_sender_report: 1861287936, - fraction_lost: 0, - total_lost: 0, - delay: rr.reports[0].delay, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_overflow() -> Result<()> { - #![allow(clippy::identity_op)] - - let mt = Arc::new(MockTime::default()); - let _mt2 = Arc::clone(&mt); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xffff, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0, - ..Default::default() - }, - ..Default::default() - }) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: { - // most significant bits: 1 << 16 - // least significant bits: 0x0000 - (1 << 16) | 0x0000 - }, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: rr.reports[0].delay, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_overflow_five_pkts() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xfffd, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xfffe, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xffff, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 1, - ..Default::default() - }, - ..Default::default() - }) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: (1 << 16) | 0x0001, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: rr.reports[0].delay, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_packet_loss() -> Result<()> { - let rtp_time: SystemTime = Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 0).unwrap().into(); - - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0x01, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0x03, - ..Default::default() - }, - ..Default::default() - }) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0x03, - last_sender_report: 0, - fraction_lost: ((1u16 << 8) / 3) as u8, - total_lost: 1, - delay: 0, - jitter: 0, - } - ) - } else { - panic!(); - } - - let now: SystemTime = Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 1).unwrap().into(); - let rt = 987654321u32.wrapping_add( - (now.duration_since(rtp_time) - .unwrap_or(Duration::from_secs(0)) - .as_secs_f64() - * 90000.0) as u32, - ); - stream - .receive_rtcp(vec![Box::new(rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(now), - rtp_time: rt, - packet_count: 10, - octet_count: 0, - ..Default::default() - })]) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0x03, - last_sender_report: 1861287936, - fraction_lost: 0, - total_lost: 1, - delay: rr.reports[0].delay, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_overflow_and_packet_loss() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xffff, - ..Default::default() - }, - ..Default::default() - }) - .await; - - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0x01, - ..Default::default() - }, - ..Default::default() - }) - .await; - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 1 << 16 | 0x01, - last_sender_report: 0, - fraction_lost: ((1u16 << 8) / 3) as u8, - total_lost: 1, - delay: 0, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_reordered_packets() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - for sequence_number in [0x01, 0x03, 0x02, 0x04] { - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number, - ..Default::default() - }, - ..Default::default() - }) - .await; - } - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0x04, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: 0, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test(start_paused = true)] -async fn test_receiver_interceptor_jitter() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - mt.set_now(Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 0).unwrap().into()); - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0x01, - timestamp: 42378934, - ..Default::default() - }, - ..Default::default() - }) - .await; - stream.read_rtp().await; - - mt.set_now(Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 1).unwrap().into()); - stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0x02, - timestamp: 42378934 + 60000, - ..Default::default() - }, - ..Default::default() - }) - .await; - - // Advance the time to generate a report - tokio::time::advance(Duration::from_millis(60)).await; - // Yield to let the reporting task run - tokio::task::yield_now().await; - - let pkts = stream.last_written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0x02, - last_sender_report: 0, - fraction_lost: 0, - total_lost: 0, - delay: 0, - jitter: 30000 / 16, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_receiver_interceptor_delay() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = ReceiverReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - mt.set_now(Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 0).unwrap().into()); - stream - .receive_rtcp(vec![Box::new(rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 0).unwrap().into()), - rtp_time: 987654321, - packet_count: 0, - octet_count: 0, - ..Default::default() - })]) - .await; - stream.read_rtcp().await; - - mt.set_now(Utc.with_ymd_and_hms(2009, 11, 10, 23, 0, 1).unwrap().into()); - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(rr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!(rr.reports.len(), 1); - assert_eq!( - rr.reports[0], - rtcp::reception_report::ReceptionReport { - ssrc: 123456, - last_sequence_number: 0, - last_sender_report: 1861222400, - fraction_lost: 0, - total_lost: 0, - delay: 65536, - jitter: 0, - } - ) - } else { - panic!(); - } - - stream.close().await?; - Ok(()) -} diff --git a/interceptor/src/report/sender/mod.rs b/interceptor/src/report/sender/mod.rs deleted file mode 100644 index 83d1aa38a..000000000 --- a/interceptor/src/report/sender/mod.rs +++ /dev/null @@ -1,182 +0,0 @@ -mod sender_stream; -#[cfg(test)] -mod sender_test; - -use std::collections::HashMap; -use std::time::{Duration, SystemTime}; - -use sender_stream::SenderStream; -use tokio::sync::{mpsc, Mutex}; -use waitgroup::WaitGroup; - -use super::*; -use crate::error::Error; -use crate::*; - -pub(crate) struct SenderReportInternal { - pub(crate) interval: Duration, - pub(crate) now: Option, - pub(crate) streams: Mutex>>, - pub(crate) close_rx: Mutex>>, -} - -/// SenderReport interceptor generates sender reports. -pub struct SenderReport { - pub(crate) internal: Arc, - - pub(crate) wg: Mutex>, - pub(crate) close_tx: Mutex>>, -} - -impl SenderReport { - /// builder returns a new ReportBuilder. - pub fn builder() -> ReportBuilder { - ReportBuilder { - is_rr: false, - ..Default::default() - } - } - - async fn is_closed(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_none() - } - - async fn run( - rtcp_writer: Arc, - internal: Arc, - ) -> Result<()> { - let mut ticker = tokio::time::interval(internal.interval); - let mut close_rx = { - let mut close_rx = internal.close_rx.lock().await; - if let Some(close) = close_rx.take() { - close - } else { - return Err(Error::ErrInvalidCloseRx); - } - }; - - loop { - tokio::select! { - _ = ticker.tick() =>{ - // TODO(cancel safety): This branch isn't cancel safe - let now = if let Some(f) = &internal.now { - f() - } else { - SystemTime::now() - }; - let streams:Vec> = { - let m = internal.streams.lock().await; - m.values().cloned().collect() - }; - for stream in streams { - let pkt = stream.generate_report(now).await; - - let a = Attributes::new(); - if let Err(err) = rtcp_writer.write(&[Box::new(pkt)], &a).await{ - log::warn!("failed sending: {}", err); - } - } - } - _ = close_rx.recv() =>{ - return Ok(()); - } - } - } - } -} - -#[async_trait] -impl Interceptor for SenderReport { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - if self.is_closed().await { - return writer; - } - - let mut w = { - let wait_group = self.wg.lock().await; - wait_group.as_ref().map(|wg| wg.worker()) - }; - let writer2 = Arc::clone(&writer); - let internal = Arc::clone(&self.internal); - tokio::spawn(async move { - let _d = w.take(); - if let Err(err) = SenderReport::run(writer2, internal).await { - log::warn!("bind_rtcp_writer Generator::run got error: {}", err); - } - }); - - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc { - let stream = Arc::new(SenderStream::new( - info.ssrc, - info.clock_rate, - writer, - self.internal.now.clone(), - )); - { - let mut streams = self.internal.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - let mut streams = self.internal.streams.lock().await; - streams.remove(&info.ssrc); - } - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - _info: &StreamInfo, - reader: Arc, - ) -> Arc { - reader - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, _info: &StreamInfo) {} - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - { - let mut wait_group = self.wg.lock().await; - if let Some(wg) = wait_group.take() { - wg.wait().await; - } - } - - Ok(()) - } -} diff --git a/interceptor/src/report/sender/sender_stream.rs b/interceptor/src/report/sender/sender_stream.rs deleted file mode 100644 index df0602afa..000000000 --- a/interceptor/src/report/sender/sender_stream.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::convert::TryInto; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use async_trait::async_trait; -use rtp::extension::abs_send_time_extension::unix2ntp; -use tokio::sync::Mutex; - -use super::*; -use crate::{Attributes, RTPWriter}; - -struct SenderStreamInternal { - ssrc: u32, - clock_rate: f64, - - /// data from rtp packets - last_rtp_time_rtp: u32, - last_rtp_time_time: SystemTime, - counters: Counters, -} - -impl SenderStreamInternal { - fn process_rtp(&mut self, now: SystemTime, pkt: &rtp::packet::Packet) { - // always update time to minimize errors - self.last_rtp_time_rtp = pkt.header.timestamp; - self.last_rtp_time_time = now; - - self.counters.increment_packets(); - self.counters.count_octets(pkt.payload.len()); - } - - fn generate_report(&mut self, now: SystemTime) -> rtcp::sender_report::SenderReport { - rtcp::sender_report::SenderReport { - ssrc: self.ssrc, - ntp_time: unix2ntp(now), - rtp_time: self.last_rtp_time_rtp.wrapping_add( - (now.duration_since(self.last_rtp_time_time) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs_f64() - * self.clock_rate) as u32, - ), - packet_count: self.counters.packet_count(), - octet_count: self.counters.octet_count(), - ..Default::default() - } - } -} - -pub(crate) struct SenderStream { - next_rtp_writer: Arc, - now: Option, - - internal: Mutex, -} - -impl SenderStream { - pub(crate) fn new( - ssrc: u32, - clock_rate: u32, - writer: Arc, - now: Option, - ) -> Self { - SenderStream { - next_rtp_writer: writer, - now, - - internal: Mutex::new(SenderStreamInternal { - ssrc, - clock_rate: clock_rate as f64, - last_rtp_time_rtp: 0, - last_rtp_time_time: SystemTime::UNIX_EPOCH, - counters: Default::default(), - }), - } - } - - async fn process_rtp(&self, now: SystemTime, pkt: &rtp::packet::Packet) { - let mut internal = self.internal.lock().await; - internal.process_rtp(now, pkt); - } - - pub(crate) async fn generate_report( - &self, - now: SystemTime, - ) -> rtcp::sender_report::SenderReport { - let mut internal = self.internal.lock().await; - internal.generate_report(now) - } -} - -/// RTPWriter is used by Interceptor.bind_local_stream. -#[async_trait] -impl RTPWriter for SenderStream { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, a: &Attributes) -> Result { - let now = if let Some(f) = &self.now { - f() - } else { - SystemTime::now() - }; - self.process_rtp(now, pkt).await; - - self.next_rtp_writer.write(pkt, a).await - } -} - -#[derive(Default)] -pub(crate) struct Counters { - packets: u32, - octets: u32, -} - -/// Wrapping counters used for generating [`rtcp::sender_report::SenderReport`] -impl Counters { - pub fn increment_packets(&mut self) { - self.packets = self.packets.wrapping_add(1); - } - - pub fn count_octets(&mut self, octets: usize) { - // account for a payload size of at most `u32::MAX` - // and log a message if larger - self.octets = self - .octets - .wrapping_add(octets.try_into().unwrap_or_else(|_| { - log::warn!("packet payload larger than 32 bits"); - u32::MAX - })); - } - - pub fn packet_count(&self) -> u32 { - self.packets - } - - pub fn octet_count(&self) -> u32 { - self.octets - } - - #[cfg(test)] - pub fn mock(packets: u32, octets: u32) -> Self { - Self { packets, octets } - } -} diff --git a/interceptor/src/report/sender/sender_test.rs b/interceptor/src/report/sender/sender_test.rs deleted file mode 100644 index fdd2bbd5c..000000000 --- a/interceptor/src/report/sender/sender_test.rs +++ /dev/null @@ -1,260 +0,0 @@ -use bytes::Bytes; -use chrono::prelude::*; -use rtp::extension::abs_send_time_extension::unix2ntp; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::mock::mock_time::MockTime; - -#[tokio::test] -async fn test_sender_interceptor_before_any_packet() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = SenderReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - let dt = Utc.with_ymd_and_hms(2009, 10, 23, 0, 0, 0).unwrap(); - mt.set_now(dt.into()); - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(sr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!( - sr, - &rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(mt.now()), - rtp_time: 4294967295, // pion: 2269117121, - packet_count: 0, - octet_count: 0, - ..Default::default() - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_sender_interceptor_after_rtp_packets() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = SenderReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - for i in 0..10u16 { - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: i, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - } - - let dt = Utc.with_ymd_and_hms(2009, 10, 23, 0, 0, 0).unwrap(); - mt.set_now(dt.into()); - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(sr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!( - sr, - &rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(mt.now()), - rtp_time: 4294967295, // pion: 2269117121, - packet_count: 10, - octet_count: 20, - ..Default::default() - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_sender_interceptor_after_rtp_packets_overflow() -> Result<()> { - let mt = Arc::new(MockTime::default()); - let time_gen = { - let mt = Arc::clone(&mt); - Arc::new(move || mt.now()) - }; - - let icpr: Arc = SenderReport::builder() - .with_interval(Duration::from_millis(50)) - .with_now_fn(time_gen) - .build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - clock_rate: 90000, - ..Default::default() - }, - icpr, - ) - .await; - - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xfffd, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xfffe, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0xffff, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 0, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: 1, - ..Default::default() - }, - payload: Bytes::from_static(b"\x00\x00"), - }) - .await?; - - let dt = Utc.with_ymd_and_hms(2009, 10, 23, 0, 0, 0).unwrap(); - mt.set_now(dt.into()); - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(sr) = pkts[0] - .as_any() - .downcast_ref::() - { - assert_eq!( - sr, - &rtcp::sender_report::SenderReport { - ssrc: 123456, - ntp_time: unix2ntp(mt.now()), - rtp_time: 4294967295, // pion: 2269117121, - packet_count: 5, - octet_count: 10, - ..Default::default() - } - ) - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_stream_counters_initially_zero() -> Result<()> { - let counters = sender_stream::Counters::default(); - assert_eq!(counters.octet_count(), 0); - assert_eq!(counters.packet_count(), 0); - Ok(()) -} - -#[tokio::test] -async fn test_stream_packet_counter_wraps_on_overflow() -> Result<()> { - let mut counters = sender_stream::Counters::mock(u32::MAX, 0); - for _ in 0..3 { - counters.increment_packets(); - } - assert_eq!(counters.packet_count(), 2); - Ok(()) -} - -#[tokio::test] -async fn test_stream_octet_counter_wraps_on_overflow() -> Result<()> { - let mut counters = sender_stream::Counters::default(); - counters.count_octets(u32::MAX as usize); - counters.count_octets(3); - assert_eq!(counters.octet_count(), 2); - Ok(()) -} - -#[tokio::test] -async fn test_stream_octet_counter_saturates_u32_from_usize() -> Result<()> { - let mut counters = sender_stream::Counters::default(); - counters.count_octets(0xabcdef01234567_usize); - assert_eq!(counters.octet_count(), 0xffffffff_u32); - Ok(()) -} diff --git a/interceptor/src/stats/interceptor.rs b/interceptor/src/stats/interceptor.rs deleted file mode 100644 index 272336e97..000000000 --- a/interceptor/src/stats/interceptor.rs +++ /dev/null @@ -1,1194 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; -use std::time::SystemTime; - -use async_trait::async_trait; -use rtcp::extended_report::{DLRRReportBlock, ExtendedReport}; -use rtcp::payload_feedbacks::full_intra_request::FullIntraRequest; -use rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; -use rtcp::receiver_report::ReceiverReport; -use rtcp::sender_report::SenderReport; -use rtcp::transport_feedbacks::transport_layer_nack::TransportLayerNack; -use rtp::extension::abs_send_time_extension::unix2ntp; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::Duration; -use util::sync::Mutex; -use util::MarshalSize; - -use super::{inbound, outbound, StatsContainer}; -use crate::error::Result; -use crate::stream_info::StreamInfo; -use crate::{Attributes, Interceptor, RTCPReader, RTCPWriter, RTPReader, RTPWriter}; - -#[derive(Debug)] -enum Message { - StatUpdate { - ssrc: u32, - update: StatsUpdate, - }, - RequestInboundSnapshot { - ssrcs: Vec, - chan: oneshot::Sender>>, - }, - RequestOutboundSnapshot { - ssrcs: Vec, - chan: oneshot::Sender>>, - }, -} - -#[derive(Debug)] -enum StatsUpdate { - /// Stats collected on the receiving end(inbound) of an RTP stream. - InboundRTP { - packets: u64, - header_bytes: u64, - payload_bytes: u64, - last_packet_timestamp: SystemTime, - }, - /// Stats collected on the sending end(outbound) of an RTP stream. - OutboundRTP { - packets: u64, - header_bytes: u64, - payload_bytes: u64, - last_packet_timestamp: SystemTime, - }, - /// Stats collected from received RTCP packets. - InboundRTCP { - fir_count: Option, - pli_count: Option, - nack_count: Option, - }, - /// Stats collected from sent RTCP packets. - OutboundRTCP { - fir_count: Option, - pli_count: Option, - nack_count: Option, - }, - /// An extended sequence number sent in an SR. - OutboundSRExtSeqNum { seq_num: u32 }, - /// Stats collected from received Receiver Reports i.e. where we have an outbound RTP stream. - InboundReceiverReport { - ext_seq_num: u32, - total_lost: u32, - jitter: u32, - rtt_ms: Option, - fraction_lost: u8, - }, - /// Stats collected from received Sender Reports i.e. where we have an inbound RTP stream. - InboundSenderRerport { - packets_and_bytes_sent: Option<(u32, u32)>, - rtt_ms: Option, - }, -} - -pub struct StatsInterceptor { - // Wrapped RTP streams - recv_streams: Mutex>>, - send_streams: Mutex>>, - - tx: mpsc::Sender, - - id: String, - now_gen: Arc SystemTime + Send + Sync>, -} - -impl StatsInterceptor { - pub fn new(id: String) -> Self { - let (tx, rx) = mpsc::channel(100); - - tokio::spawn(run_stats_reducer(rx)); - - Self { - id, - recv_streams: Default::default(), - send_streams: Default::default(), - tx, - now_gen: Arc::new(SystemTime::now), - } - } - - fn with_time_gen(id: String, now_gen: F) -> Self - where - F: Fn() -> SystemTime + Send + Sync + 'static, - { - let (tx, rx) = mpsc::channel(100); - tokio::spawn(run_stats_reducer(rx)); - - Self { - id, - recv_streams: Default::default(), - send_streams: Default::default(), - tx, - now_gen: Arc::new(now_gen), - } - } - - pub async fn fetch_inbound_stats( - &self, - ssrcs: Vec, - ) -> Vec> { - let (tx, rx) = oneshot::channel(); - - if let Err(e) = self - .tx - .send(Message::RequestInboundSnapshot { ssrcs, chan: tx }) - .await - { - log::debug!( - "Failed to fetch inbound RTP stream stats from stats task with error: {}", - e - ); - - return vec![]; - } - - rx.await.unwrap_or_default() - } - - pub async fn fetch_outbound_stats( - &self, - ssrcs: Vec, - ) -> Vec> { - let (tx, rx) = oneshot::channel(); - - if let Err(e) = self - .tx - .send(Message::RequestOutboundSnapshot { ssrcs, chan: tx }) - .await - { - log::debug!( - "Failed to fetch outbound RTP stream stats from stats task with error: {}", - e - ); - - return vec![]; - } - - rx.await.unwrap_or_default() - } -} - -async fn run_stats_reducer(mut rx: mpsc::Receiver) { - let mut ssrc_stats: StatsContainer = Default::default(); - let mut cleanup_ticker = tokio::time::interval(Duration::from_secs(10)); - - loop { - tokio::select! { - maybe_msg = rx.recv() => { - let msg = match maybe_msg { - Some(m) => m, - None => break, - }; - - match msg { - Message::StatUpdate { ssrc, update } => { - handle_stats_update(&mut ssrc_stats, ssrc, update); - } - Message::RequestInboundSnapshot { ssrcs, chan} => { - let result = ssrcs - .into_iter() - .map(|ssrc| ssrc_stats.get_inbound_stats(ssrc).map(inbound::StreamStats::snapshot)) - .collect(); - - let _ = chan.send(result); - } - Message::RequestOutboundSnapshot { ssrcs, chan} => { - let result = ssrcs - .into_iter() - .map(|ssrc| ssrc_stats.get_outbound_stats(ssrc).map(outbound::StreamStats::snapshot)) - .collect(); - - let _ = chan.send(result); - - } - } - - } - _ = cleanup_ticker.tick() => { - ssrc_stats.remove_stale_entries(); - } - } - } -} - -fn handle_stats_update(ssrc_stats: &mut StatsContainer, ssrc: u32, update: StatsUpdate) { - match update { - StatsUpdate::InboundRTP { - packets, - header_bytes, - payload_bytes, - last_packet_timestamp, - } => { - let stats = ssrc_stats.get_or_create_inbound_stream_stats(ssrc); - - stats - .rtp_stats - .update(header_bytes, payload_bytes, packets, last_packet_timestamp); - stats.mark_updated(); - } - StatsUpdate::OutboundRTP { - packets, - header_bytes, - payload_bytes, - last_packet_timestamp, - } => { - let stats = ssrc_stats.get_or_create_outbound_stream_stats(ssrc); - stats - .rtp_stats - .update(header_bytes, payload_bytes, packets, last_packet_timestamp); - stats.mark_updated(); - } - StatsUpdate::InboundRTCP { - fir_count, - pli_count, - nack_count, - } => { - let stats = ssrc_stats.get_or_create_outbound_stream_stats(ssrc); - stats.rtcp_stats.update(fir_count, pli_count, nack_count); - stats.mark_updated(); - } - StatsUpdate::OutboundRTCP { - fir_count, - pli_count, - nack_count, - } => { - let stats = ssrc_stats.get_or_create_inbound_stream_stats(ssrc); - stats.rtcp_stats.update(fir_count, pli_count, nack_count); - stats.mark_updated(); - } - StatsUpdate::OutboundSRExtSeqNum { seq_num } => { - let stats = ssrc_stats.get_or_create_outbound_stream_stats(ssrc); - stats.record_sr_ext_seq_num(seq_num); - stats.mark_updated(); - } - StatsUpdate::InboundReceiverReport { - ext_seq_num, - total_lost, - jitter, - rtt_ms, - fraction_lost, - } => { - let stats = ssrc_stats.get_or_create_outbound_stream_stats(ssrc); - stats.record_remote_round_trip_time(rtt_ms); - stats.update_remote_fraction_lost(fraction_lost); - stats.update_remote_total_lost(total_lost); - stats.update_remote_inbound_packets_received(ext_seq_num, total_lost); - stats.update_remote_jitter(jitter); - - stats.mark_updated(); - } - StatsUpdate::InboundSenderRerport { - rtt_ms, - packets_and_bytes_sent, - } => { - // This is a sender report we received, as such it concerns an RTP stream that's - // outbound at the remote. - let stats = ssrc_stats.get_or_create_inbound_stream_stats(ssrc); - - if let Some((packets_sent, bytes_sent)) = packets_and_bytes_sent { - stats.record_sender_report(packets_sent, bytes_sent); - } - stats.record_remote_round_trip_time(rtt_ms); - - stats.mark_updated(); - } - } -} - -#[async_trait] -impl Interceptor for StatsInterceptor { - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc { - let mut lock = self.recv_streams.lock(); - - let e = lock - .entry(info.ssrc) - .or_insert_with(|| Arc::new(RTPReadRecorder::new(reader, self.tx.clone()))); - - e.clone() - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - let mut lock = self.recv_streams.lock(); - - lock.remove(&info.ssrc); - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc { - let mut lock = self.send_streams.lock(); - - let e = lock - .entry(info.ssrc) - .or_insert_with(|| Arc::new(RTPWriteRecorder::new(writer, self.tx.clone()))); - - e.clone() - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - let mut lock = self.send_streams.lock(); - - lock.remove(&info.ssrc); - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - let now = self.now_gen.clone(); - - Arc::new(RTCPWriteInterceptor { - rtcp_writer: writer, - tx: self.tx.clone(), - now_gen: move || now(), - }) - } - - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - let now = self.now_gen.clone(); - - Arc::new(RTCPReadInterceptor { - rtcp_reader: reader, - tx: self.tx.clone(), - now_gen: move || now(), - }) - } -} - -pub struct RTCPReadInterceptor { - rtcp_reader: Arc, - tx: mpsc::Sender, - now_gen: F, -} - -#[async_trait] -impl RTCPReader for RTCPReadInterceptor -where - F: Fn() -> SystemTime + Send + Sync, -{ - /// read a batch of rtcp packets - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(Vec>, Attributes)> { - let (pkts, attributes) = self.rtcp_reader.read(buf, attributes).await?; - - // Middle 32 bits - let now = (unix2ntp((self.now_gen)()) >> 16) as u32; - - #[derive(Default, Debug)] - struct GenericRTCP { - fir_count: Option, - pli_count: Option, - nack_count: Option, - } - - #[derive(Default, Debug)] - struct ReceiverReportEntry { - /// Extended sequence number value from Receiver Report, used to calculate remote - /// stats. - ext_seq_num: u32, - /// Total loss value from Receiver Report, used to calculate remote - /// stats. - total_lost: u32, - /// Jitter from Receiver Report. - jitter: u32, - /// Round Trip Time calculated from Receiver Report. - rtt_ms: Option, - /// Fraction of packets lost. - fraction_lost: u8, - } - - #[derive(Default, Debug)] - struct SenderReportEntry { - /// NTP timestamp(from Sender Report). - sr_ntp_time: Option, - /// Packets Sent(from Sender Report). - sr_packets_sent: Option, - /// Bytes Sent(from Sender Report). - sr_bytes_sent: Option, - /// Last RR timestamp(middle bits) from DLRR extended report block. - dlrr_last_rr: Option, - /// Delay since last RR from DLRR extended report block. - dlrr_delay_rr: Option, - } - - #[derive(Default, Debug)] - struct Entry { - generic_rtcp: GenericRTCP, - receiver_reports: Vec, - sender_reports: Vec, - } - let updates = pkts - .iter() - .fold(HashMap::::new(), |mut acc, p| { - if let Some(rr) = p.as_any().downcast_ref::() { - for recp in &rr.reports { - let e = acc.entry(recp.ssrc).or_default(); - - let rtt_ms = if recp.delay != 0 { - calculate_rtt_ms(now, recp.delay, recp.last_sender_report) - } else { - None - }; - - e.receiver_reports.push(ReceiverReportEntry { - ext_seq_num: recp.last_sequence_number, - total_lost: recp.total_lost, - jitter: recp.jitter, - rtt_ms, - fraction_lost: recp.fraction_lost, - }); - } - } else if let Some(fir) = p.as_any().downcast_ref::() { - for fir_entry in &fir.fir { - let e = acc.entry(fir_entry.ssrc).or_default(); - e.generic_rtcp.fir_count = - e.generic_rtcp.fir_count.map(|v| v + 1).or(Some(1)); - } - } else if let Some(pli) = p.as_any().downcast_ref::() { - let e = acc.entry(pli.media_ssrc).or_default(); - e.generic_rtcp.pli_count = e.generic_rtcp.pli_count.map(|v| v + 1).or(Some(1)); - } else if let Some(nack) = p.as_any().downcast_ref::() { - let count = nack.nacks.iter().flat_map(|p| p.into_iter()).count() as u64; - - let e = acc.entry(nack.media_ssrc).or_default(); - e.generic_rtcp.nack_count = - e.generic_rtcp.nack_count.map(|v| v + count).or(Some(count)); - } else if let Some(sr) = p.as_any().downcast_ref::() { - let e = acc.entry(sr.ssrc).or_default(); - let sr_e = { - let need_new_entry = e - .sender_reports - .last() - .map(|e| e.sr_packets_sent.is_some()) - .unwrap_or(true); - - if need_new_entry { - e.sender_reports.push(Default::default()); - } - - // SAFETY: Unrwap ok because we just added an entry above - e.sender_reports.last_mut().unwrap() - }; - - sr_e.sr_ntp_time = Some(sr.ntp_time); - sr_e.sr_packets_sent = Some(sr.packet_count); - sr_e.sr_bytes_sent = Some(sr.octet_count); - } else if let Some(xr) = p.as_any().downcast_ref::() { - // Extended Report(XR) - - // We only care about DLRR reports - let dlrrs = xr.reports.iter().flat_map(|report| { - let dlrr = report.as_any().downcast_ref::(); - - dlrr.map(|b| b.reports.iter()).into_iter().flatten() - }); - - for dlrr in dlrrs { - let e = acc.entry(dlrr.ssrc).or_default(); - let sr_e = { - let need_new_entry = e - .sender_reports - .last() - .map(|e| e.dlrr_last_rr.is_some()) - .unwrap_or(true); - - if need_new_entry { - e.sender_reports.push(Default::default()); - } - - // SAFETY: Unrwap ok because we just added an entry above - e.sender_reports.last_mut().unwrap() - }; - - sr_e.dlrr_last_rr = Some(dlrr.last_rr); - sr_e.dlrr_delay_rr = Some(dlrr.dlrr); - } - } - - acc - }); - - for ( - ssrc, - Entry { - generic_rtcp, - mut receiver_reports, - mut sender_reports, - }, - ) in updates.into_iter() - { - // Sort RR by seq number low to high - receiver_reports.sort_by(|a, b| a.ext_seq_num.cmp(&b.ext_seq_num)); - // Sort SR by ntp time, low to high - sender_reports - .sort_by(|a, b| a.sr_ntp_time.unwrap_or(0).cmp(&b.sr_ntp_time.unwrap_or(0))); - - let _ = self - .tx - .send(Message::StatUpdate { - ssrc, - update: StatsUpdate::InboundRTCP { - fir_count: generic_rtcp.fir_count, - pli_count: generic_rtcp.pli_count, - nack_count: generic_rtcp.nack_count, - }, - }) - .await; - - let futures = receiver_reports.into_iter().map(|rr| { - self.tx.send(Message::StatUpdate { - ssrc, - update: StatsUpdate::InboundReceiverReport { - ext_seq_num: rr.ext_seq_num, - total_lost: rr.total_lost, - jitter: rr.jitter, - rtt_ms: rr.rtt_ms, - fraction_lost: rr.fraction_lost, - }, - }) - }); - for fut in futures { - // TODO: Use futures::join_all - let _ = fut.await; - } - - let futures = sender_reports.into_iter().map(|sr| { - let rtt_ms = match (sr.dlrr_last_rr, sr.dlrr_delay_rr, sr.sr_packets_sent) { - (Some(last_rr), Some(delay_rr), Some(_)) if last_rr != 0 && delay_rr != 0 => { - calculate_rtt_ms(now, delay_rr, last_rr) - } - _ => None, - }; - - self.tx.send(Message::StatUpdate { - ssrc, - update: StatsUpdate::InboundSenderRerport { - packets_and_bytes_sent: sr - .sr_packets_sent - .and_then(|ps| sr.sr_bytes_sent.map(|bs| (ps, bs))), - rtt_ms, - }, - }) - }); - for fut in futures { - // TODO: Use futures::join_all - let _ = fut.await; - } - } - - Ok((pkts, attributes)) - } -} - -pub struct RTCPWriteInterceptor { - rtcp_writer: Arc, - tx: mpsc::Sender, - now_gen: F, -} - -#[async_trait] -impl RTCPWriter for RTCPWriteInterceptor -where - F: Fn() -> SystemTime + Send + Sync, -{ - async fn write( - &self, - pkts: &[Box], - attributes: &Attributes, - ) -> Result { - #[derive(Default, Debug)] - struct Entry { - fir_count: Option, - pli_count: Option, - nack_count: Option, - sr_ext_seq_num: Option, - } - let updates = pkts - .iter() - .fold(HashMap::::new(), |mut acc, p| { - if let Some(fir) = p.as_any().downcast_ref::() { - for fir_entry in &fir.fir { - let e = acc.entry(fir_entry.ssrc).or_default(); - e.fir_count = e.fir_count.map(|v| v + 1).or(Some(1)); - } - } else if let Some(pli) = p.as_any().downcast_ref::() { - let e = acc.entry(pli.media_ssrc).or_default(); - e.pli_count = e.pli_count.map(|v| v + 1).or(Some(1)); - } else if let Some(nack) = p.as_any().downcast_ref::() { - let count = nack.nacks.iter().flat_map(|p| p.into_iter()).count() as u64; - - let e = acc.entry(nack.media_ssrc).or_default(); - e.nack_count = e.nack_count.map(|v| v + count).or(Some(count)); - } else if let Some(sr) = p.as_any().downcast_ref::() { - for rep in &sr.reports { - let e = acc.entry(rep.ssrc).or_default(); - - match e.sr_ext_seq_num { - // We want the initial value for `last_sequence_number` from the first - // SR. It's possible that an RTCP batch contains more than one SR, in - // which case we should use the lowest value. - Some(seq_num) if seq_num > rep.last_sequence_number => { - e.sr_ext_seq_num = Some(rep.last_sequence_number) - } - None => e.sr_ext_seq_num = Some(rep.last_sequence_number), - _ => {} - } - } - } - - acc - }); - - for ( - ssrc, - Entry { - fir_count, - pli_count, - nack_count, - sr_ext_seq_num, - }, - ) in updates.into_iter() - { - let _ = self - .tx - .send(Message::StatUpdate { - ssrc, - update: StatsUpdate::OutboundRTCP { - fir_count, - pli_count, - nack_count, - }, - }) - .await; - - if let Some(seq_num) = sr_ext_seq_num { - let _ = self - .tx - .send(Message::StatUpdate { - ssrc, - update: StatsUpdate::OutboundSRExtSeqNum { seq_num }, - }) - .await; - } - } - - self.rtcp_writer.write(pkts, attributes).await - } -} - -pub struct RTPReadRecorder { - rtp_reader: Arc, - tx: mpsc::Sender, -} - -impl RTPReadRecorder { - fn new(rtp_reader: Arc, tx: mpsc::Sender) -> Self { - Self { rtp_reader, tx } - } -} - -impl fmt::Debug for RTPReadRecorder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RTPReadRecorder").finish() - } -} - -#[async_trait] -impl RTPReader for RTPReadRecorder { - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let (pkt, attributes) = self.rtp_reader.read(buf, attributes).await?; - - let _ = self - .tx - .send(Message::StatUpdate { - ssrc: pkt.header.ssrc, - update: StatsUpdate::InboundRTP { - packets: 1, - header_bytes: pkt.header.marshal_size() as u64, - payload_bytes: pkt.payload.len() as u64, - last_packet_timestamp: SystemTime::now(), - }, - }) - .await; - - Ok((pkt, attributes)) - } -} - -pub struct RTPWriteRecorder { - rtp_writer: Arc, - tx: mpsc::Sender, -} - -impl RTPWriteRecorder { - fn new(rtp_writer: Arc, tx: mpsc::Sender) -> Self { - Self { rtp_writer, tx } - } -} - -impl fmt::Debug for RTPWriteRecorder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RTPWriteRecorder").finish() - } -} - -#[async_trait] -impl RTPWriter for RTPWriteRecorder { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, attributes: &Attributes) -> Result { - let n = self.rtp_writer.write(pkt, attributes).await?; - - let _ = self - .tx - .send(Message::StatUpdate { - ssrc: pkt.header.ssrc, - update: StatsUpdate::OutboundRTP { - packets: 1, - header_bytes: pkt.header.marshal_size() as u64, - payload_bytes: pkt.payload.len() as u64, - last_packet_timestamp: SystemTime::now(), - }, - }) - .await; - - Ok(n) - } -} - -/// Calculate the round trip time for a given peer as described in -/// [RFC3550 6.4.1](https://datatracker.ietf.org/doc/html/rfc3550#section-6.4.1). -/// -/// ## Params -/// -/// - `now` the current middle 32 bits of an NTP timestamp for the current time. -/// - `delay` the delay(`DLSR`) since last sender report expressed as fractions of a second in 32 bits. -/// - `last_report` the middle 32 bits of an NTP timestamp for the most recent sender report(LSR) or Receiver Report(LRR). -fn calculate_rtt_ms(now: u32, delay: u32, last_report: u32) -> Option { - // [10 Nov 1995 11:33:25.125 UTC] [10 Nov 1995 11:33:36.5 UTC] - // n SR(n) A=b710:8000 (46864.500 s) - // ----------------------------------------------------------------> - // v ^ - // ntp_sec =0xb44db705 v ^ dlsr=0x0005:4000 ( 5.250s) - // ntp_frac=0x20000000 v ^ lsr =0xb705:2000 (46853.125s) - // (3024992005.125 s) v ^ - // r v ^ RR(n) - // ----------------------------------------------------------------> - // |<-DLSR->| - // (5.250 s) - // - // A 0xb710:8000 (46864.500 s) - // DLSR -0x0005:4000 ( 5.250 s) - // LSR -0xb705:2000 (46853.125 s) - // ------------------------------- - // delay 0x0006:2000 ( 6.125 s) - - let rtt = now.checked_sub(delay)?.checked_sub(last_report)?; - let rtt_seconds = rtt >> 16; - let rtt_fraction = (rtt & (u16::MAX as u32)) as f64 / (u16::MAX as u32) as f64; - - Some(rtt_seconds as f64 * 1000.0 + rtt_fraction * 1000.0) -} - -#[cfg(test)] -mod test { - // Silence warning on `..Default::default()` with no effect: - #![allow(clippy::needless_update)] - - macro_rules! assert_feq { - ($left: expr, $right: expr) => { - assert_feq!($left, $right, 0.01); - }; - ($left: expr, $right: expr, $eps: expr) => { - if ($left - $right).abs() >= $eps { - panic!("{:?} was not within {:?} of {:?}", $left, $eps, $right); - } - }; - } - - use std::sync::Arc; - use std::time::{Duration, SystemTime}; - - use bytes::Bytes; - use rtcp::extended_report::{DLRRReport, DLRRReportBlock, ExtendedReport}; - use rtcp::payload_feedbacks::full_intra_request::{FirEntry, FullIntraRequest}; - use rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; - use rtcp::receiver_report::ReceiverReport; - use rtcp::reception_report::ReceptionReport; - use rtcp::sender_report::SenderReport; - use rtcp::transport_feedbacks::transport_layer_nack::{NackPair, TransportLayerNack}; - - use super::StatsInterceptor; - use crate::error::Result; - use crate::mock::mock_stream::MockStream; - use crate::stream_info::StreamInfo; - - #[tokio::test] - async fn test_stats_interceptor_rtp() -> Result<()> { - let icpr: Arc<_> = Arc::new(StatsInterceptor::new("Hello".to_owned())); - - let recv_stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - ..Default::default() - }, - icpr.clone(), - ) - .await; - - let send_stream = MockStream::new( - &StreamInfo { - ssrc: 234567, - ..Default::default() - }, - icpr.clone(), - ) - .await; - - recv_stream - .receive_rtp(rtp::packet::Packet { - header: rtp::header::Header { - ssrc: 123456, - ..Default::default() - }, - payload: Bytes::from_static(b"\xde\xad\xbe\xef"), - }) - .await; - - let _ = recv_stream - .read_rtp() - .await - .expect("After calling receive_rtp read_rtp should return Some")?; - - let _ = send_stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - ssrc: 234567, - ..Default::default() - }, - payload: Bytes::from_static(b"\xde\xad\xbe\xef\xde\xad\xbe\xef"), - }) - .await; - - let _ = send_stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - ssrc: 234567, - ..Default::default() - }, - payload: Bytes::from_static(&[0x13, 0x37]), - }) - .await; - - let snapshots = icpr.fetch_inbound_stats(vec![123456]).await; - let recv_snapshot = snapshots[0] - .as_ref() - .expect("Stats should exist for ssrc: 123456"); - assert_eq!(recv_snapshot.packets_received(), 1); - assert_eq!(recv_snapshot.header_bytes_received(), 12); - assert_eq!(recv_snapshot.payload_bytes_received(), 4); - - let snapshots = icpr.fetch_outbound_stats(vec![234567]).await; - let send_snapshot = snapshots[0] - .as_ref() - .expect("Stats should exist for ssrc: 234567"); - assert_eq!(send_snapshot.packets_sent(), 2); - assert_eq!(send_snapshot.header_bytes_sent(), 24); - assert_eq!(send_snapshot.payload_bytes_sent(), 10); - - Ok(()) - } - - #[tokio::test] - async fn test_stats_interceptor_rtcp() -> Result<()> { - let icpr: Arc<_> = Arc::new(StatsInterceptor::with_time_gen("Hello".to_owned(), || { - // 10 Nov 1995 11:33:36.5 UTC - SystemTime::UNIX_EPOCH + Duration::from_secs_f64(816003216.5) - })); - - let recv_stream = MockStream::new( - &StreamInfo { - ssrc: 123456, - ..Default::default() - }, - icpr.clone(), - ) - .await; - - let send_stream = MockStream::new( - &StreamInfo { - ssrc: 234567, - ..Default::default() - }, - icpr.clone(), - ) - .await; - - send_stream - .write_rtcp(&[Box::new(SenderReport { - ssrc: 234567, - reports: vec![ - ReceptionReport { - ssrc: 234567, - last_sequence_number: (5 << 16) | 10, - ..Default::default() - }, - ReceptionReport { - ssrc: 234567, - last_sequence_number: (5 << 16) | 85, - ..Default::default() - }, - ], - ..Default::default() - })]) - .await - .expect("Failed to write RTCP packets"); - - send_stream - .receive_rtcp(vec![ - Box::new(ReceiverReport { - reports: vec![ - ReceptionReport { - ssrc: 234567, - last_sequence_number: (5 << 16) | 64, - total_lost: 5, - ..Default::default() - }, - ReceptionReport { - ssrc: 234567, - last_sender_report: 0xb705_2000, - delay: 0x0005_4000, - last_sequence_number: (5 << 16) | 70, - total_lost: 8, - fraction_lost: 32, - jitter: 2250, - ..Default::default() - }, - ], - ..Default::default() - }), - Box::new(TransportLayerNack { - sender_ssrc: 0, - media_ssrc: 234567, - nacks: vec![NackPair { - packet_id: 5, - lost_packets: 0b0011_0110, - }], - }), - Box::new(TransportLayerNack { - sender_ssrc: 0, - // NB: Different SSRC - media_ssrc: 999999, - nacks: vec![NackPair { - packet_id: 5, - lost_packets: 0b0011_0110, - }], - }), - Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: 234567, - }), - Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: 234567, - }), - Box::new(FullIntraRequest { - sender_ssrc: 0, - media_ssrc: 234567, - fir: vec![ - FirEntry { - ssrc: 234567, - sequence_number: 132, - }, - FirEntry { - ssrc: 234567, - sequence_number: 135, - }, - ], - }), - ]) - .await; - let snapshots = icpr.fetch_outbound_stats(vec![234567]).await; - let send_snapshot = snapshots[0] - .as_ref() - .expect("Outbound Stats should exist for ssrc: 234567"); - - assert!( - send_snapshot.remote_round_trip_time().is_none() - && send_snapshot.remote_round_trip_time_measurements() == 0, - "Before receiving the first RR we should not have a remote round trip time" - ); - let _ = send_stream - .read_rtcp() - .await - .expect("After calling `receive_rtcp`, `read_rtcp` should return some packets"); - - recv_stream - .write_rtcp(&[ - Box::new(TransportLayerNack { - sender_ssrc: 0, - media_ssrc: 123456, - nacks: vec![NackPair { - packet_id: 5, - lost_packets: 0b0011_0111, - }], - }), - Box::new(TransportLayerNack { - sender_ssrc: 0, - // NB: Different SSRC - media_ssrc: 999999, - nacks: vec![NackPair { - packet_id: 5, - lost_packets: 0b1111_0110, - }], - }), - Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: 123456, - }), - Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: 123456, - }), - Box::new(PictureLossIndication { - sender_ssrc: 0, - media_ssrc: 123456, - }), - Box::new(FullIntraRequest { - sender_ssrc: 0, - media_ssrc: 123456, - fir: vec![FirEntry { - ssrc: 123456, - sequence_number: 132, - }], - }), - ]) - .await - .expect("Failed to write RTCP packets for recv_stream"); - - recv_stream - .receive_rtcp(vec![ - Box::new(SenderReport { - ssrc: 123456, - ntp_time: 12345, // Used for ordering - packet_count: 52, - octet_count: 8172, - reports: vec![], - ..Default::default() - }), - Box::new(SenderReport { - ssrc: 123456, - ntp_time: 23456, // Used for ordering - packet_count: 82, - octet_count: 10351, - reports: vec![], - ..Default::default() - }), - Box::new(ExtendedReport { - sender_ssrc: 928191, - reports: vec![Box::new(DLRRReportBlock { - reports: vec![DLRRReport { - ssrc: 123456, - last_rr: 0xb705_2000, - dlrr: 0x0005_4000, - }], - })], - }), - Box::new(SenderReport { - // NB: Different SSRC - ssrc: 9999999, - ntp_time: 99999, // Used for ordering - packet_count: 1231, - octet_count: 193812, - reports: vec![], - ..Default::default() - }), - ]) - .await; - - let snapshots = icpr.fetch_inbound_stats(vec![123456]).await; - let recv_snapshot = snapshots[0] - .as_ref() - .expect("Stats should exist for ssrc: 123456"); - assert!( - recv_snapshot.remote_round_trip_time().is_none() - && recv_snapshot.remote_round_trip_time_measurements() == 0, - "Before receiving the first SR/DLRR we should not have a remote round trip time" - ); - - let _ = recv_stream.read_rtcp().await.expect("read_rtcp failed"); - - let snapshots = icpr.fetch_outbound_stats(vec![234567]).await; - let send_snapshot = snapshots[0] - .as_ref() - .expect("Outbound Stats should exist for ssrc: 234567"); - let rtt_ms = send_snapshot.remote_round_trip_time().expect( - "After receiving an RR with a DSLR block we should have a remote round trip time", - ); - assert_feq!(rtt_ms, 6125.0); - - assert_eq!(send_snapshot.nacks_received(), 5); - assert_eq!(send_snapshot.plis_received(), 2); - assert_eq!(send_snapshot.firs_received(), 2); - // Last Seq Num(RR) - total lost(RR) - Initial Seq Num(SR) + 1 - // 70 - 8 - 10 + 1 = 53 - assert_eq!(send_snapshot.remote_packets_received(), 53); - assert_feq!( - send_snapshot - .remote_fraction_lost() - .expect("Should have a fraction lost values after receiving RR"), - 32.0 / 256.0 - ); - assert_eq!(send_snapshot.remote_total_lost(), 8); - assert_eq!(send_snapshot.remote_jitter(), 2250); - - let snapshots = icpr.fetch_inbound_stats(vec![123456]).await; - let recv_snapshot = snapshots[0] - .as_ref() - .expect("Stats should exist for ssrc: 123456"); - assert_eq!(recv_snapshot.nacks_sent(), 6); - assert_eq!(recv_snapshot.plis_sent(), 3); - assert_eq!(recv_snapshot.firs_sent(), 1); - assert_eq!(recv_snapshot.remote_packets_sent(), 82); - assert_eq!(recv_snapshot.remote_bytes_sent(), 10351); - let rtt_ms = recv_snapshot - .remote_round_trip_time() - .expect("After receiving SR and DLRR we should have a round trip time "); - assert_feq!(rtt_ms, 6125.0); - assert_eq!(recv_snapshot.remote_reports_sent(), 2); - assert_eq!(recv_snapshot.remote_round_trip_time_measurements(), 1); - assert_feq!(recv_snapshot.remote_total_round_trip_time(), 6125.0); - - Ok(()) - } -} diff --git a/interceptor/src/stats/mod.rs b/interceptor/src/stats/mod.rs deleted file mode 100644 index 2f1bb0074..000000000 --- a/interceptor/src/stats/mod.rs +++ /dev/null @@ -1,617 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::time::SystemTime; - -use tokio::time::Duration; - -mod interceptor; - -pub use self::interceptor::StatsInterceptor; - -pub fn make_stats_interceptor(id: &str) -> Arc { - Arc::new(StatsInterceptor::new(id.to_owned())) -} - -/// Types related to inbound RTP streams. -mod inbound { - use std::time::SystemTime; - - use tokio::time::{Duration, Instant}; - - use super::{RTCPStats, RTPStats}; - - #[derive(Debug, Clone)] - /// Stats collected for an inbound RTP stream. - /// Contains both stats relating to the inbound stream and remote stats for the corresponding - /// outbound stream at the remote end. - pub(super) struct StreamStats { - /// Received RTP stats. - pub(super) rtp_stats: RTPStats, - /// Common RTCP stats derived from inbound and outbound RTCP packets. - pub(super) rtcp_stats: RTCPStats, - - /// The last time any stats where update, used for garbage collection to remove obsolete stats. - last_update: Instant, - - /// The number of packets sent as reported in the latest SR from the remote. - remote_packets_sent: u32, - - /// The number of bytes sent as reported in the latest SR from the remote. - remote_bytes_sent: u32, - - /// The total number of sender reports sent by the remote and received. - remote_reports_sent: u64, - - /// The last remote round trip time measurement in ms. [`None`] if no round trip time has - /// been derived yet, or if it wasn't possible to derive it. - remote_round_trip_time: Option, - - /// The cumulative total round trip times reported in ms. - remote_total_round_trip_time: f64, - - /// The total number of measurements of the remote round trip time. - remote_round_trip_time_measurements: u64, - } - - impl Default for StreamStats { - fn default() -> Self { - Self { - rtp_stats: RTPStats::default(), - rtcp_stats: RTCPStats::default(), - last_update: Instant::now(), - remote_packets_sent: 0, - remote_bytes_sent: 0, - remote_reports_sent: 0, - remote_round_trip_time: None, - remote_total_round_trip_time: 0.0, - remote_round_trip_time_measurements: 0, - } - } - } - - impl StreamStats { - pub(super) fn snapshot(&self) -> StatsSnapshot { - self.into() - } - - pub(super) fn mark_updated(&mut self) { - self.last_update = Instant::now(); - } - - pub(super) fn duration_since_last_update(&self) -> Duration { - self.last_update.elapsed() - } - - pub(super) fn record_sender_report(&mut self, packets_sent: u32, bytes_sent: u32) { - self.remote_reports_sent += 1; - self.remote_packets_sent = packets_sent; - self.remote_bytes_sent = bytes_sent; - } - - pub(super) fn record_remote_round_trip_time(&mut self, round_trip_time: Option) { - // Store the latest measurement, even if it's None. - self.remote_round_trip_time = round_trip_time; - - if let Some(rtt) = round_trip_time { - // Only if we have a valid measurement do we update the totals - self.remote_total_round_trip_time += rtt; - self.remote_round_trip_time_measurements += 1; - } - } - } - - /// A point in time snapshot of the stream stats for an inbound RTP stream. - /// - /// Created by [`StreamStats::snapshot`]. - #[derive(Debug)] - pub struct StatsSnapshot { - /// Received RTP stats. - rtp_stats: RTPStats, - /// Common RTCP stats derived from inbound and outbound RTCP packets. - rtcp_stats: RTCPStats, - - /// The number of packets sent as reported in the latest SR from the remote. - remote_packets_sent: u32, - - /// The number of bytes sent as reported in the latest SR from the remote. - remote_bytes_sent: u32, - - /// The total number of sender reports sent by the remote and received. - remote_reports_sent: u64, - - /// The last remote round trip time measurement in ms. [`None`] if no round trip time has - /// been derived yet, or if it wasn't possible to derive it. - remote_round_trip_time: Option, - - /// The cumulative total round trip times reported in ms. - remote_total_round_trip_time: f64, - - /// The total number of measurements of the remote round trip time. - remote_round_trip_time_measurements: u64, - } - - impl StatsSnapshot { - pub fn packets_received(&self) -> u64 { - self.rtp_stats.packets - } - - pub fn payload_bytes_received(&self) -> u64 { - self.rtp_stats.payload_bytes - } - - pub fn header_bytes_received(&self) -> u64 { - self.rtp_stats.header_bytes - } - - pub fn last_packet_received_timestamp(&self) -> Option { - self.rtp_stats.last_packet_timestamp - } - - pub fn nacks_sent(&self) -> u64 { - self.rtcp_stats.nack_count - } - - pub fn firs_sent(&self) -> u64 { - self.rtcp_stats.fir_count - } - - pub fn plis_sent(&self) -> u64 { - self.rtcp_stats.pli_count - } - pub fn remote_packets_sent(&self) -> u32 { - self.remote_packets_sent - } - - pub fn remote_bytes_sent(&self) -> u32 { - self.remote_bytes_sent - } - - pub fn remote_reports_sent(&self) -> u64 { - self.remote_reports_sent - } - - pub fn remote_round_trip_time(&self) -> Option { - self.remote_round_trip_time - } - - pub fn remote_total_round_trip_time(&self) -> f64 { - self.remote_total_round_trip_time - } - - pub fn remote_round_trip_time_measurements(&self) -> u64 { - self.remote_round_trip_time_measurements - } - } - - impl From<&StreamStats> for StatsSnapshot { - fn from(stream_stats: &StreamStats) -> Self { - Self { - rtp_stats: stream_stats.rtp_stats.clone(), - rtcp_stats: stream_stats.rtcp_stats.clone(), - remote_packets_sent: stream_stats.remote_packets_sent, - remote_bytes_sent: stream_stats.remote_bytes_sent, - remote_reports_sent: stream_stats.remote_reports_sent, - remote_round_trip_time: stream_stats.remote_round_trip_time, - remote_total_round_trip_time: stream_stats.remote_total_round_trip_time, - remote_round_trip_time_measurements: stream_stats - .remote_round_trip_time_measurements, - } - } - } -} - -/// Types related to outbound RTP streams. -mod outbound { - use std::time::SystemTime; - - use tokio::time::{Duration, Instant}; - - use super::{RTCPStats, RTPStats}; - - #[derive(Debug, Clone)] - /// Stats collected for an outbound RTP stream. - /// Contains both stats relating to the outbound stream and remote stats for the corresponding - /// inbound stream. - pub(super) struct StreamStats { - /// Sent RTP stats. - pub(super) rtp_stats: RTPStats, - /// Common RTCP stats derived from inbound and outbound RTCP packets. - pub(super) rtcp_stats: RTCPStats, - - /// The last time any stats where update, used for garbage collection to remove obsolete stats. - last_update: Instant, - - /// The first value of extended seq num that was sent in an SR for this SSRC. [`None`] before - /// the first SR is sent. - /// - /// Used to calculate packet statistic for remote stats. - initial_outbound_ext_seq_num: Option, - - /// The number of inbound packets received by the remote side for this stream. - remote_packets_received: u64, - - /// The number of lost packets reported by the remote for this tream. - remote_total_lost: u32, - - /// The estimated remote jitter for this stream in timestamp units. - remote_jitter: u32, - - /// The last remote round trip time measurement in ms. [`None`] if no round trip time has - /// been derived yet, or if it wasn't possible to derive it. - remote_round_trip_time: Option, - - /// The cumulative total round trip times reported in ms. - remote_total_round_trip_time: f64, - - /// The total number of measurements of the remote round trip time. - remote_round_trip_time_measurements: u64, - - /// The latest fraction lost value from RR. - remote_fraction_lost: Option, - } - - impl Default for StreamStats { - fn default() -> Self { - Self { - rtp_stats: RTPStats::default(), - rtcp_stats: RTCPStats::default(), - last_update: Instant::now(), - initial_outbound_ext_seq_num: None, - remote_packets_received: 0, - remote_total_lost: 0, - remote_jitter: 0, - remote_round_trip_time: None, - remote_total_round_trip_time: 0.0, - remote_round_trip_time_measurements: 0, - remote_fraction_lost: None, - } - } - } - - impl StreamStats { - pub(super) fn snapshot(&self) -> StatsSnapshot { - self.into() - } - - pub(super) fn mark_updated(&mut self) { - self.last_update = Instant::now(); - } - - pub(super) fn duration_since_last_update(&self) -> Duration { - self.last_update.elapsed() - } - - pub(super) fn update_remote_inbound_packets_received( - &mut self, - rr_ext_seq_num: u32, - rr_total_lost: u32, - ) { - if let Some(initial_ext_seq_num) = self.initial_outbound_ext_seq_num { - // Total number of RTP packets received for this SSRC. - // At the receiving endpoint, this is calculated as defined in [RFC3550] section 6.4.1. - // At the sending endpoint the packetsReceived is estimated by subtracting the - // Cumulative Number of Packets Lost from the Extended Highest Sequence Number Received, - // both reported in the RTCP Receiver Report, and then subtracting the - // initial Extended Sequence Number that was sent to this SSRC in a RTCP Sender Report and then adding one, - // to mirror what is discussed in Appendix A.3 in [RFC3550], but for the sender side. - // If no RTCP Receiver Report has been received yet, then return 0. - self.remote_packets_received = - (rr_ext_seq_num as u64) - (rr_total_lost as u64) - (initial_ext_seq_num as u64) - + 1; - } - } - - #[inline(always)] - pub(super) fn record_sr_ext_seq_num(&mut self, seq_num: u32) { - // Only record the initial value - if self.initial_outbound_ext_seq_num.is_none() { - self.initial_outbound_ext_seq_num = Some(seq_num); - } - } - - pub(super) fn record_remote_round_trip_time(&mut self, round_trip_time: Option) { - // Store the latest measurement, even if it's None. - self.remote_round_trip_time = round_trip_time; - - if let Some(rtt) = round_trip_time { - // Only if we have a valid measurement do we update the totals - self.remote_total_round_trip_time += rtt; - self.remote_round_trip_time_measurements += 1; - } - } - - pub(super) fn update_remote_fraction_lost(&mut self, fraction_lost: u8) { - self.remote_fraction_lost = Some(fraction_lost); - } - - pub(super) fn update_remote_jitter(&mut self, jitter: u32) { - self.remote_jitter = jitter; - } - - pub(super) fn update_remote_total_lost(&mut self, lost: u32) { - self.remote_total_lost = lost; - } - } - - /// A point in time snapshot of the stream stats for an outbound RTP stream. - /// - /// Created by [`StreamStats::snapshot`]. - #[derive(Debug)] - pub struct StatsSnapshot { - /// Sent RTP stats. - rtp_stats: RTPStats, - /// Common RTCP stats derived from inbound and outbound RTCP packets. - rtcp_stats: RTCPStats, - - /// The number of inbound packets received by the remote side for this stream. - remote_packets_received: u64, - - /// The number of lost packets reported by the remote for this tream. - remote_total_lost: u32, - - /// The estimated remote jitter for this stream in timestamp units. - remote_jitter: u32, - - /// The most recent remote round trip time in milliseconds. - remote_round_trip_time: Option, - - /// The cumulative total round trip times reported in ms. - remote_total_round_trip_time: f64, - - /// The total number of measurements of the remote round trip time. - remote_round_trip_time_measurements: u64, - - /// The fraction of packets lost reported for this stream. - /// Calculated as defined in [RFC3550](https://www.rfc-editor.org/rfc/rfc3550) section 6.4.1 and Appendix A.3. - remote_fraction_lost: Option, - } - - impl StatsSnapshot { - pub fn packets_sent(&self) -> u64 { - self.rtp_stats.packets - } - - pub fn payload_bytes_sent(&self) -> u64 { - self.rtp_stats.payload_bytes - } - - pub fn header_bytes_sent(&self) -> u64 { - self.rtp_stats.header_bytes - } - - pub fn last_packet_sent_timestamp(&self) -> Option { - self.rtp_stats.last_packet_timestamp - } - - pub fn nacks_received(&self) -> u64 { - self.rtcp_stats.nack_count - } - - pub fn firs_received(&self) -> u64 { - self.rtcp_stats.fir_count - } - - pub fn plis_received(&self) -> u64 { - self.rtcp_stats.pli_count - } - - /// Packets received on the remote side. - pub fn remote_packets_received(&self) -> u64 { - self.remote_packets_received - } - - /// The number of lost packets reported by the remote for this tream. - pub fn remote_total_lost(&self) -> u32 { - self.remote_total_lost - } - - /// The estimated remote jitter for this stream in timestamp units. - pub fn remote_jitter(&self) -> u32 { - self.remote_jitter - } - - /// The latest RTT in ms if enough data is available to measure it. - pub fn remote_round_trip_time(&self) -> Option { - self.remote_round_trip_time - } - - /// Total RTT in ms. - pub fn remote_total_round_trip_time(&self) -> f64 { - self.remote_total_round_trip_time - } - - /// The number of RTT measurements so far. - pub fn remote_round_trip_time_measurements(&self) -> u64 { - self.remote_round_trip_time_measurements - } - - /// The latest fraction lost value from the remote or None if it hasn't been reported yet. - pub fn remote_fraction_lost(&self) -> Option { - self.remote_fraction_lost - } - } - - impl From<&StreamStats> for StatsSnapshot { - fn from(stream_stats: &StreamStats) -> Self { - Self { - rtp_stats: stream_stats.rtp_stats.clone(), - rtcp_stats: stream_stats.rtcp_stats.clone(), - remote_packets_received: stream_stats.remote_packets_received, - remote_total_lost: stream_stats.remote_total_lost, - remote_jitter: stream_stats.remote_jitter, - remote_round_trip_time: stream_stats.remote_round_trip_time, - remote_total_round_trip_time: stream_stats.remote_total_round_trip_time, - remote_round_trip_time_measurements: stream_stats - .remote_round_trip_time_measurements, - remote_fraction_lost: stream_stats - .remote_fraction_lost - .map(|fraction| (fraction as f64) / (u8::MAX as f64)), - } - } - } -} - -#[derive(Default, Debug)] -struct StatsContainer { - inbound_stats: HashMap, - outbound_stats: HashMap, -} - -impl StatsContainer { - fn get_or_create_inbound_stream_stats(&mut self, ssrc: u32) -> &mut inbound::StreamStats { - self.inbound_stats.entry(ssrc).or_default() - } - - fn get_or_create_outbound_stream_stats(&mut self, ssrc: u32) -> &mut outbound::StreamStats { - self.outbound_stats.entry(ssrc).or_default() - } - - fn get_inbound_stats(&self, ssrc: u32) -> Option<&inbound::StreamStats> { - self.inbound_stats.get(&ssrc) - } - - fn get_outbound_stats(&self, ssrc: u32) -> Option<&outbound::StreamStats> { - self.outbound_stats.get(&ssrc) - } - - fn remove_stale_entries(&mut self) { - const MAX_AGE: Duration = Duration::from_secs(60); - - self.inbound_stats - .retain(|_, s| s.duration_since_last_update() < MAX_AGE); - self.outbound_stats - .retain(|_, s| s.duration_since_last_update() < MAX_AGE); - } -} - -#[derive(Debug, Default, Clone, PartialEq, Eq)] -/// Records stats about a given RTP stream. -pub struct RTPStats { - /// Packets sent or received - packets: u64, - - /// Payload bytes sent or received - payload_bytes: u64, - - /// Header bytes sent or received - header_bytes: u64, - - /// A wall clock timestamp for when the last packet was sent or received encoded as milliseconds since - /// [`SystemTime::UNIX_EPOCH`]. - last_packet_timestamp: Option, -} - -impl RTPStats { - fn update(&mut self, header_bytes: u64, payload_bytes: u64, packets: u64, now: SystemTime) { - self.header_bytes += header_bytes; - self.payload_bytes += payload_bytes; - self.packets += packets; - self.last_packet_timestamp = Some(now); - } - - pub fn header_bytes(&self) -> u64 { - self.header_bytes - } - - pub fn payload_bytes(&self) -> u64 { - self.payload_bytes - } - - pub fn packets(&self) -> u64 { - self.packets - } - - pub fn last_packet_timestamp(&self) -> Option { - self.last_packet_timestamp - } -} - -#[derive(Debug, Default, Clone)] -pub struct RTCPStats { - /// The number of FIRs sent or received - fir_count: u64, - - /// The number of PLIs sent or received - pli_count: u64, - - /// The number of NACKs sent or received - nack_count: u64, -} - -impl RTCPStats { - #[allow(clippy::too_many_arguments)] - fn update(&mut self, fir_count: Option, pli_count: Option, nack_count: Option) { - if let Some(fir_count) = fir_count { - self.fir_count += fir_count; - } - - if let Some(pli_count) = pli_count { - self.pli_count += pli_count; - } - - if let Some(nack_count) = nack_count { - self.nack_count += nack_count; - } - } - - pub fn fir_count(&self) -> u64 { - self.fir_count - } - - pub fn pli_count(&self) -> u64 { - self.pli_count - } - - pub fn nack_count(&self) -> u64 { - self.nack_count - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_rtp_stats() { - let mut stats: RTPStats = Default::default(); - assert_eq!( - (stats.header_bytes(), stats.payload_bytes(), stats.packets()), - (0, 0, 0), - ); - - stats.update(24, 960, 1, SystemTime::now()); - - assert_eq!( - (stats.header_bytes(), stats.payload_bytes(), stats.packets()), - (24, 960, 1), - ); - } - - #[test] - fn test_rtcp_stats() { - let mut stats: RTCPStats = Default::default(); - assert_eq!( - (stats.fir_count(), stats.pli_count(), stats.nack_count()), - (0, 0, 0), - ); - - stats.update(Some(1), Some(2), Some(3)); - - assert_eq!( - (stats.fir_count(), stats.pli_count(), stats.nack_count()), - (1, 2, 3), - ); - } - - #[test] - fn test_rtp_stats_send_sync() { - fn test_send_sync() {} - test_send_sync::(); - } - - #[test] - fn test_rtcp_stats_send_sync() { - fn test_send_sync() {} - test_send_sync::(); - } -} diff --git a/interceptor/src/stream_info.rs b/interceptor/src/stream_info.rs deleted file mode 100644 index 5e9f93d5e..000000000 --- a/interceptor/src/stream_info.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::Attributes; - -/// RTPHeaderExtension represents a negotiated RFC5285 RTP header extension. -#[derive(Default, Debug, Clone)] -pub struct RTPHeaderExtension { - pub uri: String, - pub id: isize, -} - -/// StreamInfo is the Context passed when a StreamLocal or StreamRemote has been Binded or Unbinded -#[derive(Default, Debug, Clone)] -pub struct StreamInfo { - pub id: String, - pub attributes: Attributes, - pub ssrc: u32, - pub payload_type: u8, - pub rtp_header_extensions: Vec, - pub mime_type: String, - pub clock_rate: u32, - pub channels: u16, - pub sdp_fmtp_line: String, - pub rtcp_feedback: Vec, -} - -/// RTCPFeedback signals the connection to use additional RTCP packet types. -/// -#[derive(Default, Debug, Clone)] -pub struct RTCPFeedback { - /// Type is the type of feedback. - /// see: - /// valid: ack, ccm, nack, goog-remb, transport-cc - pub typ: String, - - /// The parameter value depends on the type. - /// For example, type="nack" parameter="pli" will send Picture Loss Indicator packets. - pub parameter: String, -} diff --git a/interceptor/src/stream_reader.rs b/interceptor/src/stream_reader.rs deleted file mode 100644 index 4f4578631..000000000 --- a/interceptor/src/stream_reader.rs +++ /dev/null @@ -1,27 +0,0 @@ -use async_trait::async_trait; -use srtp::stream::Stream; - -use crate::error::Result; -use crate::{Attributes, RTCPReader, RTPReader}; - -#[async_trait] -impl RTPReader for Stream { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - Ok((self.read_rtp(buf).await?, a.clone())) - } -} - -#[async_trait] -impl RTCPReader for Stream { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> Result<(Vec>, Attributes)> { - Ok((self.read_rtcp(buf).await?, a.clone())) - } -} diff --git a/interceptor/src/twcc/mod.rs b/interceptor/src/twcc/mod.rs deleted file mode 100644 index ed3fb10dc..000000000 --- a/interceptor/src/twcc/mod.rs +++ /dev/null @@ -1,279 +0,0 @@ -#[cfg(test)] -mod twcc_test; - -pub mod receiver; -pub mod sender; - -use std::cmp::Ordering; - -use rtcp::transport_feedbacks::transport_layer_cc::{ - PacketStatusChunk, RecvDelta, RunLengthChunk, StatusChunkTypeTcc, StatusVectorChunk, - SymbolSizeTypeTcc, SymbolTypeTcc, TransportLayerCc, -}; - -#[derive(Default, Debug, PartialEq, Clone)] -struct PktInfo { - sequence_number: u32, - arrival_time: i64, -} - -/// Recorder records incoming RTP packets and their delays and creates -/// transport wide congestion control feedback reports as specified in -/// -#[derive(Default, Debug, PartialEq, Clone)] -pub struct Recorder { - received_packets: Vec, - - cycles: u32, - last_sequence_number: u16, - - sender_ssrc: u32, - media_ssrc: u32, - fb_pkt_cnt: u8, -} - -impl Recorder { - /// new creates a new Recorder which uses the given sender_ssrc in the created - /// feedback packets. - pub fn new(sender_ssrc: u32) -> Self { - Recorder { - sender_ssrc, - ..Default::default() - } - } - - /// record marks a packet with media_ssrc and a transport wide sequence number sequence_number as received at arrival_time. - pub fn record(&mut self, media_ssrc: u32, sequence_number: u16, arrival_time: i64) { - self.media_ssrc = media_ssrc; - if sequence_number < 0x0fff && self.last_sequence_number > 0xf000 { - self.cycles += 1 << 16; - } - self.received_packets.push(PktInfo { - sequence_number: self.cycles | sequence_number as u32, - arrival_time, - }); - self.last_sequence_number = sequence_number; - } - - /// build_feedback_packet creates a new RTCP packet containing a TWCC feedback report. - pub fn build_feedback_packet(&mut self) -> Vec> { - if self.received_packets.len() < 2 { - return vec![]; - } - let mut feedback = Feedback::new(self.sender_ssrc, self.media_ssrc, self.fb_pkt_cnt); - self.fb_pkt_cnt = self.fb_pkt_cnt.wrapping_add(1); - - self.received_packets - .sort_by(|a: &PktInfo, b: &PktInfo| -> Ordering { - a.sequence_number.cmp(&b.sequence_number) - }); - feedback.set_base( - (self.received_packets[0].sequence_number & 0xffff) as u16, - self.received_packets[0].arrival_time, - ); - - let mut pkts = vec![]; - for pkt in &self.received_packets { - let built = - feedback.add_received((pkt.sequence_number & 0xffff) as u16, pkt.arrival_time); - if !built { - let p: Box = Box::new(feedback.get_rtcp()); - pkts.push(p); - feedback = Feedback::new(self.sender_ssrc, self.media_ssrc, self.fb_pkt_cnt); - self.fb_pkt_cnt = self.fb_pkt_cnt.wrapping_add(1); - feedback.add_received((pkt.sequence_number & 0xffff) as u16, pkt.arrival_time); - } - } - self.received_packets.clear(); - let p: Box = Box::new(feedback.get_rtcp()); - pkts.push(p); - pkts - } -} - -#[derive(Default, Debug, PartialEq, Clone)] -struct Feedback { - rtcp: TransportLayerCc, - base_sequence_number: u16, - ref_timestamp64ms: i64, - last_timestamp_us: i64, - next_sequence_number: u16, - sequence_number_count: u16, - len: usize, - last_chunk: Chunk, - chunks: Vec, - deltas: Vec, -} - -impl Feedback { - fn new(sender_ssrc: u32, media_ssrc: u32, fb_pkt_count: u8) -> Self { - Feedback { - rtcp: TransportLayerCc { - sender_ssrc, - media_ssrc, - fb_pkt_count, - ..Default::default() - }, - ..Default::default() - } - } - - fn set_base(&mut self, sequence_number: u16, time_us: i64) { - self.base_sequence_number = sequence_number; - self.next_sequence_number = self.base_sequence_number; - self.ref_timestamp64ms = time_us / 64000; - self.last_timestamp_us = self.ref_timestamp64ms * 64000; - } - - fn get_rtcp(&mut self) -> TransportLayerCc { - self.rtcp.packet_status_count = self.sequence_number_count; - self.rtcp.reference_time = self.ref_timestamp64ms as u32; - self.rtcp.base_sequence_number = self.base_sequence_number; - while !self.last_chunk.deltas.is_empty() { - self.chunks.push(self.last_chunk.encode()); - } - self.rtcp.packet_chunks.extend_from_slice(&self.chunks); - self.rtcp.recv_deltas.clone_from(&self.deltas); - - self.rtcp.clone() - } - - fn add_received(&mut self, sequence_number: u16, timestamp_us: i64) -> bool { - let delta_us = timestamp_us - self.last_timestamp_us; - let delta250us = delta_us / 250; - if delta250us < i16::MIN as i64 || delta250us > i16::MAX as i64 { - // delta doesn't fit into 16 bit, need to create new packet - return false; - } - - while self.next_sequence_number != sequence_number { - if !self - .last_chunk - .can_add(SymbolTypeTcc::PacketNotReceived as u16) - { - self.chunks.push(self.last_chunk.encode()); - } - self.last_chunk.add(SymbolTypeTcc::PacketNotReceived as u16); - self.sequence_number_count = self.sequence_number_count.wrapping_add(1); - self.next_sequence_number = self.next_sequence_number.wrapping_add(1); - } - - let recv_delta = if (0..=0xff).contains(&delta250us) { - self.len += 1; - SymbolTypeTcc::PacketReceivedSmallDelta - } else { - self.len += 2; - SymbolTypeTcc::PacketReceivedLargeDelta - }; - - if !self.last_chunk.can_add(recv_delta as u16) { - self.chunks.push(self.last_chunk.encode()); - } - self.last_chunk.add(recv_delta as u16); - self.deltas.push(RecvDelta { - type_tcc_packet: recv_delta, - delta: delta_us, - }); - self.last_timestamp_us = timestamp_us; - self.sequence_number_count = self.sequence_number_count.wrapping_add(1); - self.next_sequence_number = self.next_sequence_number.wrapping_add(1); - true - } -} - -const MAX_RUN_LENGTH_CAP: usize = 0x1fff; // 13 bits -const MAX_ONE_BIT_CAP: usize = 14; // bits -const MAX_TWO_BIT_CAP: usize = 7; // bits - -#[derive(Default, Debug, PartialEq, Clone)] -struct Chunk { - has_large_delta: bool, - has_different_types: bool, - deltas: Vec, -} - -impl Chunk { - fn can_add(&self, delta: u16) -> bool { - if self.deltas.len() < MAX_TWO_BIT_CAP { - return true; - } - if self.deltas.len() < MAX_ONE_BIT_CAP - && !self.has_large_delta - && delta != SymbolTypeTcc::PacketReceivedLargeDelta as u16 - { - return true; - } - if self.deltas.len() < MAX_RUN_LENGTH_CAP - && !self.has_different_types - && delta == self.deltas[0] - { - return true; - } - false - } - - fn add(&mut self, delta: u16) { - self.deltas.push(delta); - self.has_large_delta = - self.has_large_delta || delta == SymbolTypeTcc::PacketReceivedLargeDelta as u16; - self.has_different_types = self.has_different_types || delta != self.deltas[0]; - } - - fn encode(&mut self) -> PacketStatusChunk { - if !self.has_different_types { - let p = PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: self.deltas[0].into(), - run_length: self.deltas.len() as u16, - }); - self.reset(); - return p; - } - if self.deltas.len() == MAX_ONE_BIT_CAP { - let p = PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: self - .deltas - .iter() - .map(|x| SymbolTypeTcc::from(*x)) - .collect::>(), - }); - self.reset(); - return p; - } - - let min_cap = std::cmp::min(MAX_TWO_BIT_CAP, self.deltas.len()); - let svc = PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: self.deltas[..min_cap] - .iter() - .map(|x| SymbolTypeTcc::from(*x)) - .collect::>(), - }); - self.deltas.drain(..min_cap); - self.has_different_types = false; - self.has_large_delta = false; - - if !self.deltas.is_empty() { - let tmp = self.deltas[0]; - for d in &self.deltas { - if tmp != *d { - self.has_different_types = true; - } - if *d == SymbolTypeTcc::PacketReceivedLargeDelta as u16 { - self.has_large_delta = true; - } - } - } - - svc - } - - fn reset(&mut self) { - self.deltas = vec![]; - self.has_large_delta = false; - self.has_different_types = false; - } -} diff --git a/interceptor/src/twcc/receiver/mod.rs b/interceptor/src/twcc/receiver/mod.rs deleted file mode 100644 index aaaf81369..000000000 --- a/interceptor/src/twcc/receiver/mod.rs +++ /dev/null @@ -1,262 +0,0 @@ -mod receiver_stream; -#[cfg(test)] -mod receiver_test; - -use std::time::Duration; - -use receiver_stream::ReceiverStream; -use rtp::extension::transport_cc_extension::TransportCcExtension; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::MissedTickBehavior; -use util::Unmarshal; -use waitgroup::WaitGroup; - -use crate::twcc::sender::TRANSPORT_CC_URI; -use crate::twcc::Recorder; -use crate::*; - -/// ReceiverBuilder is a InterceptorBuilder for a SenderInterceptor -#[derive(Default)] -pub struct ReceiverBuilder { - interval: Option, -} - -impl ReceiverBuilder { - /// with_interval sets send interval for the interceptor. - pub fn with_interval(mut self, interval: Duration) -> ReceiverBuilder { - self.interval = Some(interval); - self - } -} - -impl InterceptorBuilder for ReceiverBuilder { - fn build(&self, _id: &str) -> Result> { - let (close_tx, close_rx) = mpsc::channel(1); - let (packet_chan_tx, packet_chan_rx) = mpsc::channel(1); - Ok(Arc::new(Receiver { - internal: Arc::new(ReceiverInternal { - interval: if let Some(interval) = &self.interval { - *interval - } else { - Duration::from_millis(100) - }, - recorder: Mutex::new(Recorder::default()), - packet_chan_rx: Mutex::new(Some(packet_chan_rx)), - streams: Mutex::new(HashMap::new()), - close_rx: Mutex::new(Some(close_rx)), - }), - start_time: tokio::time::Instant::now(), - packet_chan_tx, - wg: Mutex::new(Some(WaitGroup::new())), - close_tx: Mutex::new(Some(close_tx)), - })) - } -} - -struct Packet { - hdr: rtp::header::Header, - sequence_number: u16, - arrival_time: i64, - ssrc: u32, -} - -struct ReceiverInternal { - interval: Duration, - recorder: Mutex, - packet_chan_rx: Mutex>>, - streams: Mutex>>, - close_rx: Mutex>>, -} - -/// Receiver sends transport-wide congestion control reports as specified in: -/// -pub struct Receiver { - internal: Arc, - - // we use tokio's Instant because it makes testing easier via `tokio::time::advance`. - start_time: tokio::time::Instant, - packet_chan_tx: mpsc::Sender, - - wg: Mutex>, - close_tx: Mutex>>, -} - -impl Receiver { - /// builder returns a new ReceiverBuilder. - pub fn builder() -> ReceiverBuilder { - ReceiverBuilder::default() - } - - async fn is_closed(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_none() - } - - async fn run( - rtcp_writer: Arc, - internal: Arc, - ) -> Result<()> { - let mut close_rx = { - let mut close_rx = internal.close_rx.lock().await; - if let Some(close_rx) = close_rx.take() { - close_rx - } else { - return Err(Error::ErrInvalidCloseRx); - } - }; - let mut packet_chan_rx = { - let mut packet_chan_rx = internal.packet_chan_rx.lock().await; - if let Some(packet_chan_rx) = packet_chan_rx.take() { - packet_chan_rx - } else { - return Err(Error::ErrInvalidPacketRx); - } - }; - - let a = Attributes::new(); - let mut ticker = tokio::time::interval(internal.interval); - ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = close_rx.recv() =>{ - return Ok(()); - } - p = packet_chan_rx.recv() => { - if let Some(p) = p { - let mut recorder = internal.recorder.lock().await; - recorder.record(p.ssrc, p.sequence_number, p.arrival_time); - } - } - _ = ticker.tick() =>{ - // build and send twcc - let pkts = { - let mut recorder = internal.recorder.lock().await; - recorder.build_feedback_packet() - }; - - if pkts.is_empty() { - continue; - } - - if let Err(err) = rtcp_writer.write(&pkts, &a).await{ - log::error!("rtcp_writer.write got err: {}", err); - } - } - } - } - } -} - -#[async_trait] -impl Interceptor for Receiver { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - if self.is_closed().await { - return writer; - } - - { - let mut recorder = self.internal.recorder.lock().await; - *recorder = Recorder::new(rand::random::()); - } - - let mut w = { - let wait_group = self.wg.lock().await; - wait_group.as_ref().map(|wg| wg.worker()) - }; - let writer2 = Arc::clone(&writer); - let internal = Arc::clone(&self.internal); - tokio::spawn(async move { - let _d = w.take(); - if let Err(err) = Receiver::run(writer2, internal).await { - log::warn!("bind_rtcp_writer TWCC Sender::run got error: {}", err); - } - }); - - writer - } - - /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method - /// will be called once per rtp packet. - async fn bind_local_stream( - &self, - _info: &StreamInfo, - writer: Arc, - ) -> Arc { - writer - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, _info: &StreamInfo) {} - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - info: &StreamInfo, - reader: Arc, - ) -> Arc { - let mut hdr_ext_id = 0u8; - for e in &info.rtp_header_extensions { - if e.uri == TRANSPORT_CC_URI { - hdr_ext_id = e.id as u8; - break; - } - } - if hdr_ext_id == 0 { - // Don't try to read header extension if ID is 0, because 0 is an invalid extension ID - return reader; - } - - let stream = Arc::new(ReceiverStream::new( - reader, - hdr_ext_id, - info.ssrc, - self.packet_chan_tx.clone(), - self.start_time, - )); - - { - let mut streams = self.internal.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, info: &StreamInfo) { - let mut streams = self.internal.streams.lock().await; - streams.remove(&info.ssrc); - } - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - { - let mut wait_group = self.wg.lock().await; - if let Some(wg) = wait_group.take() { - wg.wait().await; - } - } - - Ok(()) - } -} diff --git a/interceptor/src/twcc/receiver/receiver_stream.rs b/interceptor/src/twcc/receiver/receiver_stream.rs deleted file mode 100644 index 764b26c9d..000000000 --- a/interceptor/src/twcc/receiver/receiver_stream.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::*; - -pub(super) struct ReceiverStream { - parent_rtp_reader: Arc, - hdr_ext_id: u8, - ssrc: u32, - packet_chan_tx: mpsc::Sender, - // we use tokio's Instant because it makes testing easier via `tokio::time::advance`. - start_time: tokio::time::Instant, -} - -impl ReceiverStream { - pub(super) fn new( - parent_rtp_reader: Arc, - hdr_ext_id: u8, - ssrc: u32, - packet_chan_tx: mpsc::Sender, - start_time: tokio::time::Instant, - ) -> Self { - ReceiverStream { - parent_rtp_reader, - hdr_ext_id, - ssrc, - packet_chan_tx, - start_time, - } - } -} - -#[async_trait] -impl RTPReader for ReceiverStream { - /// read a rtp packet - async fn read( - &self, - buf: &mut [u8], - attributes: &Attributes, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let (pkt, attr) = self.parent_rtp_reader.read(buf, attributes).await?; - - if let Some(mut ext) = pkt.header.get_extension(self.hdr_ext_id) { - let tcc_ext = TransportCcExtension::unmarshal(&mut ext)?; - - let _ = self - .packet_chan_tx - .send(Packet { - hdr: pkt.header.clone(), - sequence_number: tcc_ext.transport_sequence, - arrival_time: (tokio::time::Instant::now() - self.start_time).as_micros() - as i64, - ssrc: self.ssrc, - }) - .await; - } - - Ok((pkt, attr)) - } -} diff --git a/interceptor/src/twcc/receiver/receiver_test.rs b/interceptor/src/twcc/receiver/receiver_test.rs deleted file mode 100644 index 24f60f184..000000000 --- a/interceptor/src/twcc/receiver/receiver_test.rs +++ /dev/null @@ -1,361 +0,0 @@ -// Silence warning on `..Default::default()` with no effect: -#![allow(clippy::needless_update)] - -use rtcp::transport_feedbacks::transport_layer_cc::{ - PacketStatusChunk, RunLengthChunk, StatusChunkTypeTcc, StatusVectorChunk, SymbolSizeTypeTcc, - SymbolTypeTcc, TransportLayerCc, -}; -use util::Marshal; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::stream_info::RTPHeaderExtension; - -#[tokio::test] -async fn test_twcc_receiver_interceptor_before_any_packets() -> Result<()> { - let builder = Receiver::builder(); - let icpr = builder.build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - tokio::select! { - pkts = stream.written_rtcp() => { - assert!(pkts.map(|p| p.is_empty()).unwrap_or(true), "Should not have sent an RTCP packet before receiving the first RTP packets") - } - _ = tokio::time::sleep(Duration::from_millis(300)) => { - // All good - } - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_twcc_receiver_interceptor_after_rtp_packets() -> Result<()> { - let builder = Receiver::builder(); - let icpr = builder.build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - for i in 0..10 { - let mut hdr = rtp::header::Header::default(); - let tcc = TransportCcExtension { - transport_sequence: i, - } - .marshal()?; - hdr.set_extension(1, tcc)?; - stream - .receive_rtp(rtp::packet::Packet { - header: hdr, - ..Default::default() - }) - .await; - } - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(cc) = pkts[0].as_any().downcast_ref::() { - assert_eq!(cc.media_ssrc, 1); - assert_eq!(cc.base_sequence_number, 0); - assert_eq!( - cc.packet_chunks, - vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 10, - })] - ); - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test(start_paused = true)] -async fn test_twcc_receiver_interceptor_different_delays_between_rtp_packets() -> Result<()> { - let builder = Receiver::builder().with_interval(Duration::from_millis(500)); - let icpr = builder.build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - let delays = [0, 10, 100, 200]; - for (i, d) in delays.iter().enumerate() { - tokio::time::advance(Duration::from_millis(*d)).await; - - let mut hdr = rtp::header::Header::default(); - let tcc = TransportCcExtension { - transport_sequence: i as u16, - } - .marshal()?; - - hdr.set_extension(1, tcc)?; - stream - .receive_rtp(rtp::packet::Packet { - header: hdr, - ..Default::default() - }) - .await; - - // Yield so this packet can be processed - tokio::task::yield_now().await; - } - - // Force a packet to be generated - tokio::time::advance(Duration::from_millis(2001)).await; - tokio::task::yield_now().await; - - let pkts = stream.written_rtcp().await.unwrap(); - - assert_eq!(pkts.len(), 1); - if let Some(cc) = pkts[0].as_any().downcast_ref::() { - assert_eq!(cc.base_sequence_number, 0); - assert_eq!( - cc.packet_chunks, - vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - ], - })] - ); - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test(start_paused = true)] -async fn test_twcc_receiver_interceptor_packet_loss() -> Result<()> { - let builder = Receiver::builder().with_interval(Duration::from_secs(2)); - let icpr = builder.build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - let sequence_number_to_delay = &[ - (0, 0), - (1, 10), - (4, 100), - (8, 200), - (9, 20), - (10, 20), - (30, 300), - ]; - - for (i, d) in sequence_number_to_delay { - tokio::time::advance(Duration::from_millis(*d)).await; - let mut hdr = rtp::header::Header::default(); - let tcc = TransportCcExtension { - transport_sequence: *i, - } - .marshal()?; - hdr.set_extension(1, tcc)?; - stream - .receive_rtp(rtp::packet::Packet { - header: hdr, - ..Default::default() - }) - .await; - - // Yield so this packet can be processed - tokio::task::yield_now().await; - } - - // Force a packet to be generated - tokio::time::advance(Duration::from_millis(2001)).await; - tokio::task::yield_now().await; - - let pkts = stream.written_rtcp().await.unwrap(); - - assert_eq!(pkts.len(), 1); - if let Some(cc) = pkts[0].as_any().downcast_ref::() { - assert_eq!(cc.base_sequence_number, 0); - assert_eq!( - cc.packet_chunks, - vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketNotReceived, - run_length: 16, - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedLargeDelta, - run_length: 1, - }), - ] - ); - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_twcc_receiver_interceptor_overflow() -> Result<()> { - let builder = Receiver::builder(); - let icpr = builder.build("")?; - - let stream = MockStream::new( - &StreamInfo { - ssrc: 1, - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - ..Default::default() - }], - ..Default::default() - }, - icpr, - ) - .await; - - for i in [65530, 65534, 65535, 1, 2, 10] { - let mut hdr = rtp::header::Header::default(); - let tcc = TransportCcExtension { - transport_sequence: i, - } - .marshal()?; - hdr.set_extension(1, tcc)?; - stream - .receive_rtp(rtp::packet::Packet { - header: hdr, - ..Default::default() - }) - .await; - } - - let pkts = stream.written_rtcp().await.unwrap(); - assert_eq!(pkts.len(), 1); - if let Some(cc) = pkts[0].as_any().downcast_ref::() { - assert_eq!(cc.base_sequence_number, 65530); - assert_eq!( - cc.packet_chunks, - vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - ], - }), - ] - ); - } else { - panic!(); - } - - stream.close().await?; - - Ok(()) -} diff --git a/interceptor/src/twcc/sender/mod.rs b/interceptor/src/twcc/sender/mod.rs deleted file mode 100644 index d3ed5673d..000000000 --- a/interceptor/src/twcc/sender/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -mod sender_stream; -#[cfg(test)] -mod sender_test; - -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicU32; -use rtp::extension::transport_cc_extension::TransportCcExtension; -use sender_stream::SenderStream; -use tokio::sync::Mutex; -use util::Marshal; - -use crate::{Attributes, RTPWriter, *}; - -pub(crate) const TRANSPORT_CC_URI: &str = - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; - -/// HeaderExtensionBuilder is a InterceptorBuilder for a HeaderExtension Interceptor -#[derive(Default)] -pub struct SenderBuilder { - init_sequence_nr: u32, -} - -impl SenderBuilder { - /// with_init_sequence_nr sets the init sequence number of the interceptor. - pub fn with_init_sequence_nr(mut self, init_sequence_nr: u32) -> SenderBuilder { - self.init_sequence_nr = init_sequence_nr; - self - } -} - -impl InterceptorBuilder for SenderBuilder { - /// build constructs a new SenderInterceptor - fn build(&self, _id: &str) -> Result> { - Ok(Arc::new(Sender { - next_sequence_nr: Arc::new(AtomicU32::new(self.init_sequence_nr)), - streams: Mutex::new(HashMap::new()), - })) - } -} - -/// Sender adds transport wide sequence numbers as header extension to each RTP packet -pub struct Sender { - next_sequence_nr: Arc, - streams: Mutex>>, -} - -impl Sender { - /// builder returns a new SenderBuilder. - pub fn builder() -> SenderBuilder { - SenderBuilder::default() - } -} - -#[async_trait] -impl Interceptor for Sender { - /// bind_rtcp_reader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might - /// change in the future. The returned method will be called once per packet batch. - async fn bind_rtcp_reader( - &self, - reader: Arc, - ) -> Arc { - reader - } - - /// bind_rtcp_writer lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method - /// will be called once per packet batch. - async fn bind_rtcp_writer( - &self, - writer: Arc, - ) -> Arc { - writer - } - - /// bind_local_stream returns a writer that adds a rtp TransportCCExtension - /// header with increasing sequence numbers to each outgoing packet. - async fn bind_local_stream( - &self, - info: &StreamInfo, - writer: Arc, - ) -> Arc { - let mut hdr_ext_id = 0u8; - for e in &info.rtp_header_extensions { - if e.uri == TRANSPORT_CC_URI { - hdr_ext_id = e.id as u8; - break; - } - } - if hdr_ext_id == 0 { - // Don't add header extension if ID is 0, because 0 is an invalid extension ID - return writer; - } - - let stream = Arc::new(SenderStream::new( - writer, - Arc::clone(&self.next_sequence_nr), - hdr_ext_id, - )); - - { - let mut streams = self.streams.lock().await; - streams.insert(info.ssrc, Arc::clone(&stream)); - } - - stream - } - - /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_local_stream(&self, info: &StreamInfo) { - let mut streams = self.streams.lock().await; - streams.remove(&info.ssrc); - } - - /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method - /// will be called once per rtp packet. - async fn bind_remote_stream( - &self, - _info: &StreamInfo, - reader: Arc, - ) -> Arc { - reader - } - - /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track. - async fn unbind_remote_stream(&self, _info: &StreamInfo) {} - - /// close closes the Interceptor, cleaning up any data if necessary. - async fn close(&self) -> Result<()> { - Ok(()) - } -} diff --git a/interceptor/src/twcc/sender/sender_stream.rs b/interceptor/src/twcc/sender/sender_stream.rs deleted file mode 100644 index 29754070d..000000000 --- a/interceptor/src/twcc/sender/sender_stream.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::*; - -pub(super) struct SenderStream { - next_rtp_writer: Arc, - next_sequence_nr: Arc, - hdr_ext_id: u8, -} - -impl SenderStream { - pub(super) fn new( - next_rtp_writer: Arc, - next_sequence_nr: Arc, - hdr_ext_id: u8, - ) -> Self { - SenderStream { - next_rtp_writer, - next_sequence_nr, - hdr_ext_id, - } - } -} - -/// RTPWriter is used by Interceptor.bind_local_stream. -#[async_trait] -impl RTPWriter for SenderStream { - /// write a rtp packet - async fn write(&self, pkt: &rtp::packet::Packet, a: &Attributes) -> Result { - let sequence_number = self.next_sequence_nr.fetch_add(1, Ordering::SeqCst); - - let tcc_ext = TransportCcExtension { - transport_sequence: sequence_number as u16, - }; - let tcc_payload = tcc_ext.marshal()?; - - let mut pkt = pkt.clone(); - pkt.header.set_extension(self.hdr_ext_id, tcc_payload)?; - - self.next_rtp_writer.write(&pkt, a).await - } -} diff --git a/interceptor/src/twcc/sender/sender_test.rs b/interceptor/src/twcc/sender/sender_test.rs deleted file mode 100644 index 4ae519792..000000000 --- a/interceptor/src/twcc/sender/sender_test.rs +++ /dev/null @@ -1,85 +0,0 @@ -use rtp::packet::Packet; -use tokio::sync::mpsc; -use tokio::time::Duration; -use util::Unmarshal; -use waitgroup::WaitGroup; - -use super::*; -use crate::mock::mock_stream::MockStream; -use crate::stream_info::RTPHeaderExtension; - -#[tokio::test] -async fn test_twcc_sender_interceptor() -> Result<()> { - // "add transport wide cc to each packet" - let builder = Sender::builder().with_init_sequence_nr(0); - let icpr = builder.build("")?; - - let (p_chan_tx, mut p_chan_rx) = mpsc::channel::(10 * 5); - tokio::spawn(async move { - // start some parallel streams using the same interceptor to test for race conditions - let wg = WaitGroup::new(); - for i in 0..10 { - let w = wg.worker(); - let p_chan_tx2 = p_chan_tx.clone(); - let icpr2 = Arc::clone(&icpr); - tokio::spawn(async move { - let _d = w; - let stream = MockStream::new( - &StreamInfo { - rtp_header_extensions: vec![RTPHeaderExtension { - uri: TRANSPORT_CC_URI.to_owned(), - id: 1, - }], - ..Default::default() - }, - icpr2, - ) - .await; - - let id = i + 1; - #[allow(clippy::identity_op)] - for seq_num in [id * 1, id * 2, id * 3, id * 4, id * 5] { - stream - .write_rtp(&rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq_num, - ..Default::default() - }, - ..Default::default() - }) - .await - .unwrap(); - - let timeout = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timeout); - - tokio::select! { - p = stream.written_rtp() =>{ - if let Some(p) = p { - assert_eq!(p.header.sequence_number, seq_num); - let _ = p_chan_tx2.send(p).await; - }else{ - panic!("stream.written_rtp none"); - } - } - _ = timeout.as_mut()=>{ - panic!("written rtp packet not found"); - } - }; - } - - let _ = stream.close().await; - }); - } - wg.wait().await; - }); - - while let Some(p) = p_chan_rx.recv().await { - // Can't check for increasing transport cc sequence number, since we can't ensure ordering between the streams - // on pChan is same as in the interceptor, but at least make sure each packet has a seq nr. - let mut extension_header = p.header.get_extension(1).unwrap(); - let _twcc = TransportCcExtension::unmarshal(&mut extension_header)?; - } - - Ok(()) -} diff --git a/interceptor/src/twcc/twcc_test.rs b/interceptor/src/twcc/twcc_test.rs deleted file mode 100644 index 2bef1ffda..000000000 --- a/interceptor/src/twcc/twcc_test.rs +++ /dev/null @@ -1,565 +0,0 @@ -use rtcp::packet::Packet; -use util::Marshal; - -use super::*; -use crate::error::Result; - -#[test] -fn test_chunk_add() -> Result<()> { - //"fill with not received" - { - let mut c = Chunk::default(); - - for i in 0..MAX_RUN_LENGTH_CAP { - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16), "{}", i); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - } - assert_eq!(c.deltas, vec![0u16; MAX_RUN_LENGTH_CAP]); - assert!(!c.has_different_types); - - assert!(!c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - - let status_chunk = c.encode(); - match status_chunk { - PacketStatusChunk::RunLengthChunk(_) => {} - _ => panic!(), - }; - - let buf = status_chunk.marshal()?; - assert_eq!(&buf[..], &[0x1f, 0xff]); - } - - //"fill with small delta" - { - let mut c = Chunk::default(); - - for i in 0..MAX_ONE_BIT_CAP { - assert!( - c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16), - "{}", - i - ); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - } - - assert_eq!(c.deltas, vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); - assert!(!c.has_different_types); - - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - assert!(!c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - - let status_chunk = c.encode(); - match status_chunk { - PacketStatusChunk::RunLengthChunk(_) => {} - _ => panic!(), - }; - - let buf = status_chunk.marshal()?; - assert_eq!(&buf[..], &[0x20, 0xe]); - } - - //"fill with large delta" - { - let mut c = Chunk::default(); - - for i in 0..MAX_TWO_BIT_CAP { - assert!( - c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16), - "{}", - i - ); - c.add(SymbolTypeTcc::PacketReceivedLargeDelta as u16); - } - - assert_eq!(c.deltas, vec![2, 2, 2, 2, 2, 2, 2]); - assert!(c.has_large_delta); - assert!(!c.has_different_types); - - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - assert!(!c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - - let status_chunk = c.encode(); - match status_chunk { - PacketStatusChunk::RunLengthChunk(_) => {} - _ => panic!(), - }; - - let buf = status_chunk.marshal()?; - assert_eq!(&buf[..], &[0x40, 0x7]); - } - - // "fill with different types" - { - let mut c = Chunk::default(); - - assert!(c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - - assert!(c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedLargeDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedLargeDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedLargeDelta as u16); - - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - - let status_chunk = c.encode(); - match status_chunk { - PacketStatusChunk::StatusVectorChunk(_) => {} - _ => panic!(), - }; - - let buf = status_chunk.marshal()?; - assert_eq!(&buf[..], &[0xd5, 0x6a]); - } - - //"overfill and encode" - { - let mut c = Chunk::default(); - - assert!(c.can_add(SymbolTypeTcc::PacketReceivedSmallDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedSmallDelta as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - assert!(c.can_add(SymbolTypeTcc::PacketNotReceived as u16)); - c.add(SymbolTypeTcc::PacketNotReceived as u16); - - assert!(!c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - - let status_chunk1 = c.encode(); - match status_chunk1 { - PacketStatusChunk::StatusVectorChunk(_) => {} - _ => panic!(), - }; - assert_eq!(c.deltas.len(), 1); - - assert!(c.can_add(SymbolTypeTcc::PacketReceivedLargeDelta as u16)); - c.add(SymbolTypeTcc::PacketReceivedLargeDelta as u16); - - let status_chunk2 = c.encode(); - match status_chunk2 { - PacketStatusChunk::StatusVectorChunk(_) => {} - _ => panic!(), - }; - assert_eq!(c.deltas.len(), 0); - - assert_eq!( - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta - ], - }), - status_chunk2 - ); - } - - Ok(()) -} - -#[test] -fn test_feedback() -> Result<()> { - //"add simple" - { - let mut f = Feedback::default(); - let got = f.add_received(0, 10); - assert!(got); - } - - //"add too large" - { - let mut f = Feedback::default(); - - assert!(!f.add_received(12, 8200 * 1000 * 250)); - } - - // "add received 1" - { - let mut f = Feedback::default(); - f.set_base(1, 1000 * 1000); - - let got = f.add_received(1, 1023 * 1000); - - assert!(got); - assert_eq!(f.next_sequence_number, 2); - assert_eq!(f.ref_timestamp64ms, 15); - - let got = f.add_received(4, 1086 * 1000); - assert!(got); - assert_eq!(f.next_sequence_number, 5); - assert_eq!(f.ref_timestamp64ms, 15); - - assert!(f.last_chunk.has_different_types); - assert_eq!(f.last_chunk.deltas.len(), 4); - assert!(!f - .last_chunk - .deltas - .contains(&(SymbolTypeTcc::PacketReceivedLargeDelta as u16))); - } - - //"add received 2" - { - let mut f = Feedback::new(0, 0, 0); - f.set_base(5, 320 * 1000); - - let mut got = f.add_received(5, 320 * 1000); - assert!(got); - got = f.add_received(7, 448 * 1000); - assert!(got); - got = f.add_received(8, 512 * 1000); - assert!(got); - got = f.add_received(11, 768 * 1000); - assert!(got); - - let pkt = f.get_rtcp(); - - assert!(pkt.header().padding); - assert_eq!(pkt.header().length, 7); - assert_eq!(pkt.base_sequence_number, 5); - assert_eq!(pkt.packet_status_count, 7); - assert_eq!(pkt.reference_time, 5); - assert_eq!(pkt.fb_pkt_count, 0); - assert_eq!(pkt.packet_chunks.len(), 1); - - assert_eq!( - vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - ], - })], - pkt.packet_chunks - ); - - let expected_deltas = [ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0200 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0100 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0400 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - ]; - assert_eq!(pkt.recv_deltas.len(), expected_deltas.len()); - for (i, expected) in expected_deltas.iter().enumerate() { - assert_eq!(&pkt.recv_deltas[i], expected); - } - } - - //"add received wrapped sequence number" - { - let mut f = Feedback::new(0, 0, 0); - f.set_base(65535, 320 * 1000); - - let mut got = f.add_received(65535, 320 * 1000); - assert!(got); - got = f.add_received(7, 448 * 1000); - assert!(got); - got = f.add_received(8, 512 * 1000); - assert!(got); - got = f.add_received(11, 768 * 1000); - assert!(got); - - let pkt = f.get_rtcp(); - - assert!(pkt.header().padding); - assert_eq!(pkt.header().length, 7); - assert_eq!(pkt.base_sequence_number, 65535); - assert_eq!(pkt.packet_status_count, 13); - assert_eq!(pkt.reference_time, 5); - assert_eq!(pkt.fb_pkt_count, 0); - assert_eq!(pkt.packet_chunks.len(), 2); - - assert_eq!( - vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - ], - }), - ], - pkt.packet_chunks - ); - - let expected_deltas = [ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0200 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0100 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0x0400 * TYPE_TCC_DELTA_SCALE_FACTOR, - }, - ]; - assert_eq!(pkt.recv_deltas.len(), expected_deltas.len()); - for (i, expected) in expected_deltas.iter().enumerate() { - assert_eq!(&pkt.recv_deltas[i], expected); - } - } - - //"get RTCP" - { - let tests = vec![(320, 1, 5, 1), (1000, 2, 15, 2)]; - for (arrival_ts, sequence_number, want_ref_time, want_base_sequence_number) in tests { - let mut f = Feedback::new(0, 0, 0); - f.set_base(sequence_number, arrival_ts * 1000); - - let got = f.get_rtcp(); - assert_eq!(got.reference_time, want_ref_time); - assert_eq!(got.base_sequence_number, want_base_sequence_number); - } - } - - Ok(()) -} - -fn add_run(r: &mut Recorder, sequence_numbers: &[u16], arrival_times: &[i64]) { - assert_eq!(sequence_numbers.len(), arrival_times.len()); - - for i in 0..sequence_numbers.len() { - r.record(5000, sequence_numbers[i], arrival_times[i]); - } -} - -const TYPE_TCC_DELTA_SCALE_FACTOR: i64 = 250; -const SCALE_FACTOR_REFERENCE_TIME: i64 = 64000; - -fn increase_time(arrival_time: &mut i64, increase_amount: i64) -> i64 { - *arrival_time += increase_amount; - *arrival_time -} - -fn marshal_all(pkts: &[Box]) -> Result<()> { - for pkt in pkts { - let _ = pkt.marshal()?; - } - Ok(()) -} - -#[test] -fn test_build_feedback_packet() -> Result<()> { - let mut r = Recorder::new(5000); - - let mut arrival_time = SCALE_FACTOR_REFERENCE_TIME; - add_run( - &mut r, - &[0, 1, 2, 3, 4, 5, 6, 7], - &[ - SCALE_FACTOR_REFERENCE_TIME, - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR * 256), - ], - ); - - let rtcp_packets = r.build_feedback_packet(); - assert_eq!(1, rtcp_packets.len()); - - let expected = TransportLayerCc { - sender_ssrc: 5000, - media_ssrc: 5000, - base_sequence_number: 0, - reference_time: 1, - fb_pkt_count: 0, - packet_status_count: 8, - packet_chunks: vec![ - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 7, - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedLargeDelta, - run_length: 1, - }), - ], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR * 256, - }, - ], - }; - - if let Some(tcc) = rtcp_packets[0].as_any().downcast_ref::() { - assert_eq!(tcc, &expected); - } else { - panic!(); - } - - marshal_all(&rtcp_packets[..])?; - - Ok(()) -} - -#[test] -fn test_build_feedback_packet_rolling() -> Result<()> { - let mut r = Recorder::new(5000); - - let mut arrival_time = SCALE_FACTOR_REFERENCE_TIME; - add_run(&mut r, &[3], &[arrival_time]); - - let rtcp_packets = r.build_feedback_packet(); - assert_eq!(0, rtcp_packets.len()); - - add_run( - &mut r, - &[4, 8, 9], - &[ - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - increase_time(&mut arrival_time, TYPE_TCC_DELTA_SCALE_FACTOR), - ], - ); - - let rtcp_packets = r.build_feedback_packet(); - assert_eq!(rtcp_packets.len(), 1); - - let expected = TransportLayerCc { - sender_ssrc: 5000, - media_ssrc: 5000, - base_sequence_number: 3, - reference_time: 1, - fb_pkt_count: 0, - packet_status_count: 7, - packet_chunks: vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - ], - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: TYPE_TCC_DELTA_SCALE_FACTOR, - }, - ], - }; - - if let Some(tcc) = rtcp_packets[0].as_any().downcast_ref::() { - assert_eq!(tcc, &expected); - } else { - panic!(); - } - - marshal_all(&rtcp_packets[..])?; - - Ok(()) -} diff --git a/mdns/.gitignore b/mdns/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/mdns/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/mdns/CHANGELOG.md b/mdns/CHANGELOG.md deleted file mode 100644 index 597cbc79c..000000000 --- a/mdns/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# webrtc-mdns changelog - -## Unreleased - -## v0.5.2 - -* Change log level for packet reception [#366](https://github.com/webrtc-rs/webrtc/pull/366). - -## v0.5.1 - -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - -## v0.5.0 - -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). -* [#275 mdns: get_interface_addr_for_ip once per query](https://github.com/webrtc-rs/webrtc/pull/275) by [@melekes](https://github.com/melekes). - - -## Prior to 0.5.0 - -Before 0.5.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/mdns/releases). - diff --git a/mdns/Cargo.toml b/mdns/Cargo.toml deleted file mode 100644 index 33bfafbc0..000000000 --- a/mdns/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "webrtc-mdns" -version = "0.7.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of mDNS" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-mdns" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/mdns" - -[features] -default = [ "reuse_port" ] -reuse_port = [] - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["ifaces"] } - -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -socket2 = { version = "0.5", features = ["all"] } -log = "0.4" -thiserror = "1" - -[dev-dependencies] -env_logger = "0.10" -chrono = "0.4.28" -clap = "3" - -[[example]] -name = "mdns_query" -path = "examples/mdns_query.rs" -bench = false - -[[example]] -name = "mdns_server" -path = "examples/mdns_server.rs" -bench = false - -[[example]] -name = "mdns_server_query" -path = "examples/mdns_server_query.rs" -bench = false diff --git a/mdns/LICENSE-APACHE b/mdns/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/mdns/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/mdns/LICENSE-MIT b/mdns/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/mdns/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/mdns/README.md b/mdns/README.md deleted file mode 100644 index e19a09ee8..000000000 --- a/mdns/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of mDNS. Rewrite Pion mDNS in Rust -

diff --git a/mdns/codecov.yml b/mdns/codecov.yml deleted file mode 100644 index b69bc9bed..000000000 --- a/mdns/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: eb4a349b-5dd5-442b-a57c-a3de73aedf9b - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/mdns/doc/webrtc.rs.png b/mdns/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/mdns/examples/mdns_query.rs b/mdns/examples/mdns_query.rs deleted file mode 100644 index 1cac50471..000000000 --- a/mdns/examples/mdns_query.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::io::Write; -use std::net::SocketAddr; -use std::str::FromStr; - -use clap::{App, AppSettings, Arg}; -use mdns::config::*; -use mdns::conn::*; -use mdns::Error; -use tokio::sync::mpsc; -use webrtc_mdns as mdns; - -// For interop with webrtc-rs/mdns_server -// cargo run --color=always --package webrtc-mdns --example mdns_query - -// For interop with pion/mdns_server: -// cargo run --color=always --package webrtc-mdns --example mdns_query -- --local-name pion-test.local - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("mDNS Query") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of mDNS Query") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("0.0.0.0:5353") - .long("server") - .help("mDNS Server name."), - ) - .arg( - Arg::with_name("local-name") - .long("local-name") - .takes_value(true) - .default_value("webrtc-rs-test.local") - .help("Local name"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - let local_name = matches.value_of("local-name").unwrap(); - - let server = DnsConn::server( - SocketAddr::from_str(server)?, - Config { - ..Default::default() - }, - ) - .unwrap(); - - log::info!("querying dns"); - - let (_a, b) = mpsc::channel(1); - - let (answer, src) = server.query(local_name, b).await.unwrap(); - log::info!("dns queried"); - println!("answer = {answer}, src = {src}"); - - server.close().await.unwrap(); - Ok(()) -} diff --git a/mdns/examples/mdns_server.rs b/mdns/examples/mdns_server.rs deleted file mode 100644 index a66d5afcf..000000000 --- a/mdns/examples/mdns_server.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::io::Write; -use std::net::SocketAddr; -use std::str::FromStr; - -use clap::{App, AppSettings, Arg}; -use mdns::config::*; -use mdns::conn::*; -use mdns::Error; -use webrtc_mdns as mdns; - -// For interop with webrtc-rs/mdns_server -// cargo run --color=always --package webrtc-mdns --example mdns_server - -// For interop with pion/mdns_client: -// cargo run --color=always --package webrtc-mdns --example mdns_server -- --local-name pion-test.local - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - let mut app = App::new("mDNS Sever") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of mDNS Sever") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("0.0.0.0:5353") - .long("server") - .help("mDNS Server name."), - ) - .arg( - Arg::with_name("local-name") - .long("local-name") - .takes_value(true) - .default_value("webrtc-rs-test.local") - .help("Local name"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - let local_name = matches.value_of("local-name").unwrap(); - - let server = DnsConn::server( - SocketAddr::from_str(server)?, - Config { - local_names: vec![local_name.to_owned()], - ..Default::default() - }, - ) - .unwrap(); - - println!("Press ctlr-c to stop server"); - tokio::signal::ctrl_c().await.unwrap(); - server.close().await.unwrap(); - Ok(()) -} diff --git a/mdns/examples/mdns_server_query.rs b/mdns/examples/mdns_server_query.rs deleted file mode 100644 index b1a195ad9..000000000 --- a/mdns/examples/mdns_server_query.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::net::{Ipv4Addr, SocketAddr}; - -use tokio::sync::mpsc; -use webrtc_mdns::config::*; -use webrtc_mdns::conn::*; - -#[tokio::main] -async fn main() { - env_logger::init(); - - log::trace!("server a created"); - - let server_a = DnsConn::server( - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 5353), - Config { - local_names: vec![ - "webrtc-rs-mdns-1.local".to_owned(), - "webrtc-rs-mdns-2.local".to_owned(), - ], - ..Default::default() - }, - ) - .unwrap(); - - let server_b = DnsConn::server( - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 5353), - Config { - ..Default::default() - }, - ) - .unwrap(); - - let (a, b) = mpsc::channel(1); - - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(20)).await; - a.send(()).await - }); - - let (answer, src) = server_b.query("webrtc-rs-mdns-1.local", b).await.unwrap(); - println!("webrtc-rs-mdns-1.local answer = {answer}, src = {src}"); - - let (a, b) = mpsc::channel(1); - - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(20)).await; - a.send(()).await - }); - - let (answer, src) = server_b.query("webrtc-rs-mdns-2.local", b).await.unwrap(); - println!("webrtc-rs-mdns-2.local answer = {answer}, src = {src}"); - - server_a.close().await.unwrap(); - server_b.close().await.unwrap(); -} diff --git a/mdns/src/config.rs b/mdns/src/config.rs deleted file mode 100644 index b9f06de46..000000000 --- a/mdns/src/config.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::time::Duration; - -// Config is used to configure a mDNS client or server. -#[derive(Default, Debug)] -pub struct Config { - // query_interval controls how often we sends Queries until we - // get a response for the requested name - pub query_interval: Duration, - - // local_names are the names that we will generate answers for - // when we get questions - pub local_names: Vec, - //LoggerFactory logging.LoggerFactory -} diff --git a/mdns/src/conn/conn_test.rs b/mdns/src/conn/conn_test.rs deleted file mode 100644 index f284b86d9..000000000 --- a/mdns/src/conn/conn_test.rs +++ /dev/null @@ -1,47 +0,0 @@ -#[cfg(test)] -mod test { - use tokio::time::timeout; - - use crate::config::Config; - use crate::conn::*; - - #[tokio::test] - async fn test_multiple_close() -> Result<()> { - let server_a = DnsConn::server( - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 5353), - Config::default(), - )?; - - server_a.close().await?; - - if let Err(err) = server_a.close().await { - assert_eq!(err, Error::ErrConnectionClosed); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) - } - - #[tokio::test] - async fn test_query_respect_timeout() -> Result<()> { - let server_a = DnsConn::server( - SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 5353), - Config::default(), - )?; - - let (a, b) = mpsc::channel(1); - - timeout(Duration::from_millis(100), a.send(())) - .await - .unwrap() - .unwrap(); - - let res = server_a.query("invalid-host", b).await; - assert!(res.is_err(), "server_a.query expects timeout!"); - - server_a.close().await?; - - Ok(()) - } -} diff --git a/mdns/src/conn/mod.rs b/mdns/src/conn/mod.rs deleted file mode 100644 index e7377977b..000000000 --- a/mdns/src/conn/mod.rs +++ /dev/null @@ -1,436 +0,0 @@ -use core::sync::atomic; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::Arc; -use std::time::Duration; - -use socket2::SockAddr; -use tokio::net::{ToSocketAddrs, UdpSocket}; -use tokio::sync::{mpsc, Mutex}; -use util::ifaces; - -use crate::config::*; -use crate::error::*; -use crate::message::header::*; -use crate::message::name::*; -use crate::message::parser::*; -use crate::message::question::*; -use crate::message::resource::a::*; -use crate::message::resource::*; -use crate::message::*; - -mod conn_test; - -pub const DEFAULT_DEST_ADDR: &str = "224.0.0.251:5353"; - -const INBOUND_BUFFER_SIZE: usize = 65535; -const DEFAULT_QUERY_INTERVAL: Duration = Duration::from_secs(1); -const MAX_MESSAGE_RECORDS: usize = 3; -const RESPONSE_TTL: u32 = 120; - -// Conn represents a mDNS Server -pub struct DnsConn { - socket: Arc, - dst_addr: SocketAddr, - - query_interval: Duration, - queries: Arc>>, - - is_server_closed: Arc, - close_server: mpsc::Sender<()>, -} - -struct Query { - name_with_suffix: String, - query_result_chan: mpsc::Sender, -} - -struct QueryResult { - answer: ResourceHeader, - addr: SocketAddr, -} - -impl DnsConn { - /// server establishes a mDNS connection over an existing connection - pub fn server(addr: SocketAddr, config: Config) -> Result { - let socket = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(socket2::Protocol::UDP), - )?; - - #[cfg(feature = "reuse_port")] - #[cfg(target_family = "unix")] - socket.set_reuse_port(true)?; - - socket.set_reuse_address(true)?; - socket.set_broadcast(true)?; - socket.set_nonblocking(true)?; - - socket.bind(&SockAddr::from(addr))?; - { - let mut join_error_count = 0; - let interfaces = match ifaces::ifaces() { - Ok(e) => e, - Err(e) => { - log::error!("Error getting interfaces: {:?}", e); - return Err(Error::Other(e.to_string())); - } - }; - - for interface in &interfaces { - if let Some(SocketAddr::V4(e)) = interface.addr { - if let Err(e) = socket.join_multicast_v4(&Ipv4Addr::new(224, 0, 0, 251), e.ip()) - { - log::trace!("Error connecting multicast, error: {:?}", e); - join_error_count += 1; - continue; - } - - log::trace!("Connected to interface address {:?}", e); - } - } - - if join_error_count >= interfaces.len() { - return Err(Error::ErrJoiningMulticastGroup); - } - } - - let socket = UdpSocket::from_std(socket.into())?; - - let local_names = config - .local_names - .iter() - .map(|l| l.to_string() + ".") - .collect(); - - let dst_addr: SocketAddr = DEFAULT_DEST_ADDR.parse()?; - - let is_server_closed = Arc::new(atomic::AtomicBool::new(false)); - - let (close_server_send, close_server_rcv) = mpsc::channel(1); - - let c = DnsConn { - query_interval: if config.query_interval != Duration::from_secs(0) { - config.query_interval - } else { - DEFAULT_QUERY_INTERVAL - }, - - queries: Arc::new(Mutex::new(vec![])), - socket: Arc::new(socket), - dst_addr, - is_server_closed: Arc::clone(&is_server_closed), - close_server: close_server_send, - }; - - let queries = c.queries.clone(); - let socket = Arc::clone(&c.socket); - - tokio::spawn(async move { - DnsConn::start( - close_server_rcv, - is_server_closed, - socket, - local_names, - dst_addr, - queries, - ) - .await - }); - - Ok(c) - } - - /// Close closes the mDNS Conn - pub async fn close(&self) -> Result<()> { - log::info!("Closing connection"); - if self.is_server_closed.load(atomic::Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - log::trace!("Sending close command to server"); - match self.close_server.send(()).await { - Ok(_) => { - log::trace!("Close command sent"); - Ok(()) - } - Err(e) => { - log::warn!("Error sending close command to server: {:?}", e); - Err(Error::ErrConnectionClosed) - } - } - } - - /// Query sends mDNS Queries for the following name until - /// either there's a close signal or we get a result - pub async fn query( - &self, - name: &str, - mut close_query_signal: mpsc::Receiver<()>, - ) -> Result<(ResourceHeader, SocketAddr)> { - if self.is_server_closed.load(atomic::Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let name_with_suffix = name.to_owned() + "."; - - let (query_tx, mut query_rx) = mpsc::channel(1); - { - let mut queries = self.queries.lock().await; - queries.push(Query { - name_with_suffix: name_with_suffix.clone(), - query_result_chan: query_tx, - }); - } - - log::trace!("Sending query"); - self.send_question(&name_with_suffix).await; - - loop { - tokio::select! { - _ = tokio::time::sleep(self.query_interval) => { - log::trace!("Sending query"); - self.send_question(&name_with_suffix).await - }, - - _ = close_query_signal.recv() => { - log::info!("Query close signal received."); - return Err(Error::ErrConnectionClosed) - }, - - res_opt = query_rx.recv() =>{ - log::info!("Received query result"); - if let Some(res) = res_opt{ - return Ok((res.answer, res.addr)); - } - } - } - } - } - - async fn send_question(&self, name: &str) { - let packed_name = match Name::new(name) { - Ok(pn) => pn, - Err(err) => { - log::warn!("Failed to construct mDNS packet: {}", err); - return; - } - }; - - let raw_query = { - let mut msg = Message { - header: Header::default(), - questions: vec![Question { - typ: DnsType::A, - class: DNSCLASS_INET, - name: packed_name, - }], - ..Default::default() - }; - - match msg.pack() { - Ok(v) => v, - Err(err) => { - log::error!("Failed to construct mDNS packet {}", err); - return; - } - } - }; - - log::trace!("{:?} sending {:?}...", self.socket.local_addr(), raw_query); - if let Err(err) = self.socket.send_to(&raw_query, self.dst_addr).await { - log::error!("Failed to send mDNS packet {}", err); - } - } - - async fn start( - mut closed_rx: mpsc::Receiver<()>, - close_server: Arc, - socket: Arc, - local_names: Vec, - dst_addr: SocketAddr, - queries: Arc>>, - ) -> Result<()> { - log::info!("Looping and listening {:?}", socket.local_addr()); - - let mut b = vec![0u8; INBOUND_BUFFER_SIZE]; - let (mut n, mut src); - - loop { - tokio::select! { - _ = closed_rx.recv() => { - log::info!("Closing server connection"); - close_server.store(true, atomic::Ordering::SeqCst); - - return Ok(()); - } - - result = socket.recv_from(&mut b) => { - match result{ - Ok((len, addr)) => { - n = len; - src = addr; - log::trace!("Received new connection from {:?}", addr); - }, - - Err(err) => { - log::error!("Error receiving from socket connection: {:?}", err); - continue; - }, - } - } - } - - let mut p = Parser::default(); - if let Err(err) = p.start(&b[..n]) { - log::error!("Failed to parse mDNS packet {}", err); - continue; - } - - run(&mut p, &socket, &local_names, src, dst_addr, &queries).await - } - } -} - -async fn run( - p: &mut Parser<'_>, - socket: &Arc, - local_names: &[String], - src: SocketAddr, - dst_addr: SocketAddr, - queries: &Arc>>, -) { - let mut interface_addr = None; - for _ in 0..=MAX_MESSAGE_RECORDS { - let q = match p.question() { - Ok(q) => q, - Err(err) => { - if Error::ErrSectionDone == err { - log::trace!("Parsing has completed"); - break; - } else { - log::error!("Failed to parse mDNS packet {}", err); - return; - } - } - }; - - for local_name in local_names { - if *local_name == q.name.data { - let interface_addr = match interface_addr { - Some(addr) => addr, - None => match get_interface_addr_for_ip(src).await { - Ok(addr) => { - interface_addr.replace(addr); - addr - } - Err(e) => { - log::warn!( - "Failed to get local interface to communicate with {}: {:?}", - &src, - e - ); - continue; - } - }, - }; - - log::trace!( - "Found local name: {} to send answer, IP {}, interface addr {}", - local_name, - src.ip(), - interface_addr - ); - if let Err(e) = - send_answer(socket, &interface_addr, &q.name.data, src.ip(), dst_addr).await - { - log::error!("Error sending answer to client: {:?}", e); - continue; - }; - } - } - } - - // There might be more than MAX_MESSAGE_RECORDS questions, so skip the rest - let _ = p.skip_all_questions(); - - for _ in 0..=MAX_MESSAGE_RECORDS { - let a = match p.answer_header() { - Ok(a) => a, - Err(err) => { - if Error::ErrSectionDone != err { - log::warn!("Failed to parse mDNS packet {}", err); - } - return; - } - }; - - if a.typ != DnsType::A && a.typ != DnsType::Aaaa { - continue; - } - - let mut qs = queries.lock().await; - for j in (0..qs.len()).rev() { - if qs[j].name_with_suffix == a.name.data { - let _ = qs[j] - .query_result_chan - .send(QueryResult { - answer: a.clone(), - addr: src, - }) - .await; - qs.remove(j); - } - } - } -} - -async fn send_answer( - socket: &Arc, - interface_addr: &SocketAddr, - name: &str, - dst: IpAddr, - dst_addr: SocketAddr, -) -> Result<()> { - let raw_answer = { - let mut msg = Message { - header: Header { - response: true, - authoritative: true, - ..Default::default() - }, - - answers: vec![Resource { - header: ResourceHeader { - typ: DnsType::A, - class: DNSCLASS_INET, - name: Name::new(name)?, - ttl: RESPONSE_TTL, - ..Default::default() - }, - body: Some(Box::new(AResource { - a: match interface_addr.ip() { - IpAddr::V4(ip) => ip.octets(), - IpAddr::V6(_) => { - return Err(Error::Other("Unexpected IpV6 addr".to_owned())) - } - }, - })), - }], - ..Default::default() - }; - - msg.pack()? - }; - - socket.send_to(&raw_answer, dst_addr).await?; - log::trace!("Sent answer to IP {}", dst); - - Ok(()) -} - -async fn get_interface_addr_for_ip(addr: impl ToSocketAddrs) -> std::io::Result { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - socket.connect(addr).await?; - socket.local_addr() -} diff --git a/mdns/src/error.rs b/mdns/src/error.rs deleted file mode 100644 index 1149f3fed..000000000 --- a/mdns/src/error.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::string::FromUtf8Error; -use std::{io, net}; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("mDNS: failed to join multicast group")] - ErrJoiningMulticastGroup, - #[error("mDNS: connection is closed")] - ErrConnectionClosed, - #[error("mDNS: context has elapsed")] - ErrContextElapsed, - #[error("mDNS: config must not be nil")] - ErrNilConfig, - #[error("parsing/packing of this type isn't available yet")] - ErrNotStarted, - #[error("parsing/packing of this section has completed")] - ErrSectionDone, - #[error("parsing/packing of this section is header")] - ErrSectionHeader, - #[error("insufficient data for base length type")] - ErrBaseLen, - #[error("insufficient data for calculated length type")] - ErrCalcLen, - #[error("segment prefix is reserved")] - ErrReserved, - #[error("too many pointers (>10)")] - ErrTooManyPtr, - #[error("invalid pointer")] - ErrInvalidPtr, - #[error("nil resource body")] - ErrNilResourceBody, - #[error("insufficient data for resource body length")] - ErrResourceLen, - #[error("segment length too long")] - ErrSegTooLong, - #[error("zero length segment")] - ErrZeroSegLen, - #[error("resource length too long")] - ErrResTooLong, - #[error("too many Questions to pack (>65535)")] - ErrTooManyQuestions, - #[error("too many Answers to pack (>65535)")] - ErrTooManyAnswers, - #[error("too many Authorities to pack (>65535)")] - ErrTooManyAuthorities, - #[error("too many Additionals to pack (>65535)")] - ErrTooManyAdditionals, - #[error("name is not in canonical format (it must end with a .)")] - ErrNonCanonicalName, - #[error("character string exceeds maximum length (255)")] - ErrStringTooLong, - #[error("compressed name in SRV resource data")] - ErrCompressedSrv, - #[error("empty builder msg")] - ErrEmptyBuilderMsg, - #[error("{0}")] - Io(#[source] IoError), - #[error("utf-8 error: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("parse addr: {0}")] - ParseIp(#[from] net::AddrParseError), - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} diff --git a/mdns/src/lib.rs b/mdns/src/lib.rs deleted file mode 100644 index eda3a3e83..000000000 --- a/mdns/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod config; -pub mod conn; -mod error; -pub mod message; - -pub use error::Error; diff --git a/mdns/src/message/builder.rs b/mdns/src/message/builder.rs deleted file mode 100644 index 183a593c4..000000000 --- a/mdns/src/message/builder.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::collections::HashMap; - -use super::header::*; -use super::question::*; -use super::resource::*; -use super::*; -use crate::error::*; - -// A Builder allows incrementally packing a DNS message. -// -// Example usage: -// b := NewBuilder(Header{...}) -// b.enable_compression() -// // Optionally start a section and add things to that section. -// // Repeat adding sections as necessary. -// buf, err := b.Finish() -// // If err is nil, buf[2:] will contain the built bytes. -#[derive(Default)] -pub struct Builder { - // msg is the storage for the message being built. - pub msg: Option>, - - // section keeps track of the current section being built. - pub section: Section, - - // header keeps track of what should go in the header when Finish is - // called. - pub header: HeaderInternal, - - // start is the starting index of the bytes allocated in msg for header. - pub start: usize, - - // compression is a mapping from name suffixes to their starting index - // in msg. - pub compression: Option>, -} - -impl Builder { - // NewBuilder creates a new builder with compression disabled. - // - // Note: Most users will want to immediately enable compression with the - // enable_compression method. See that method's comment for why you may or may - // not want to enable compression. - // - // The DNS message is appended to the provided initial buffer buf (which may be - // nil) as it is built. The final message is returned by the (*Builder).Finish - // method, which may return the same underlying array if there was sufficient - // capacity in the slice. - pub fn new(h: &Header) -> Self { - let (id, bits) = h.pack(); - - Builder { - msg: Some(vec![0; HEADER_LEN]), - start: 0, - section: Section::Header, - header: HeaderInternal { - id, - bits, - ..Default::default() - }, - compression: None, - } - - //var hb [HEADER_LEN]byte - //b.msg = append(b.msg, hb[:]...) - //return b - } - - // enable_compression enables compression in the Builder. - // - // Leaving compression disabled avoids compression related allocations, but can - // result in larger message sizes. Be careful with this mode as it can cause - // messages to exceed the UDP size limit. - // - // According to RFC 1035, section 4.1.4, the use of compression is optional, but - // all implementations must accept both compressed and uncompressed DNS - // messages. - // - // Compression should be enabled before any sections are added for best results. - pub fn enable_compression(&mut self) { - self.compression = Some(HashMap::new()); - } - - fn start_check(&self, section: Section) -> Result<()> { - if self.section <= Section::NotStarted { - return Err(Error::ErrNotStarted); - } - if self.section > section { - return Err(Error::ErrSectionDone); - } - - Ok(()) - } - - // start_questions prepares the builder for packing Questions. - pub fn start_questions(&mut self) -> Result<()> { - self.start_check(Section::Questions)?; - self.section = Section::Questions; - Ok(()) - } - - // start_answers prepares the builder for packing Answers. - pub fn start_answers(&mut self) -> Result<()> { - self.start_check(Section::Answers)?; - self.section = Section::Answers; - Ok(()) - } - - // start_authorities prepares the builder for packing Authorities. - pub fn start_authorities(&mut self) -> Result<()> { - self.start_check(Section::Authorities)?; - self.section = Section::Authorities; - Ok(()) - } - - // start_additionals prepares the builder for packing Additionals. - pub fn start_additionals(&mut self) -> Result<()> { - self.start_check(Section::Additionals)?; - self.section = Section::Additionals; - Ok(()) - } - - fn increment_section_count(&mut self) -> Result<()> { - let section = self.section; - let (count, err) = match section { - Section::Questions => (&mut self.header.questions, Error::ErrTooManyQuestions), - Section::Answers => (&mut self.header.answers, Error::ErrTooManyAnswers), - Section::Authorities => (&mut self.header.authorities, Error::ErrTooManyAuthorities), - Section::Additionals => (&mut self.header.additionals, Error::ErrTooManyAdditionals), - Section::NotStarted => return Err(Error::ErrNotStarted), - Section::Done => return Err(Error::ErrSectionDone), - Section::Header => return Err(Error::ErrSectionHeader), - }; - - if *count == u16::MAX { - Err(err) - } else { - *count += 1; - Ok(()) - } - } - - // question adds a single question. - pub fn add_question(&mut self, q: &Question) -> Result<()> { - if self.section < Section::Questions { - return Err(Error::ErrNotStarted); - } - if self.section > Section::Questions { - return Err(Error::ErrSectionDone); - } - let msg = self.msg.take(); - if let Some(mut msg) = msg { - msg = q.pack(msg, &mut self.compression, self.start)?; - self.increment_section_count()?; - self.msg = Some(msg); - } - - Ok(()) - } - - fn check_resource_section(&self) -> Result<()> { - if self.section < Section::Answers { - return Err(Error::ErrNotStarted); - } - if self.section > Section::Additionals { - return Err(Error::ErrSectionDone); - } - Ok(()) - } - - // Resource adds a single resource. - pub fn add_resource(&mut self, r: &mut Resource) -> Result<()> { - self.check_resource_section()?; - - if let Some(body) = &r.body { - r.header.typ = body.real_type(); - } else { - return Err(Error::ErrNilResourceBody); - } - - if let Some(msg) = self.msg.take() { - let (mut msg, len_off) = r.header.pack(msg, &mut self.compression, self.start)?; - let pre_len = msg.len(); - if let Some(body) = &r.body { - msg = body.pack(msg, &mut self.compression, self.start)?; - r.header.fix_len(&mut msg, len_off, pre_len)?; - self.increment_section_count()?; - } - self.msg = Some(msg); - } - - Ok(()) - } - - // Finish ends message building and generates a binary message. - pub fn finish(&mut self) -> Result> { - if self.section < Section::Header { - return Err(Error::ErrNotStarted); - } - self.section = Section::Done; - - // Space for the header was allocated in NewBuilder. - let buf = self.header.pack(vec![]); - assert_eq!(buf.len(), HEADER_LEN); - if let Some(mut msg) = self.msg.take() { - msg[..HEADER_LEN].copy_from_slice(&buf[..HEADER_LEN]); - Ok(msg) - } else { - Err(Error::ErrEmptyBuilderMsg) - } - } -} diff --git a/mdns/src/message/header.rs b/mdns/src/message/header.rs deleted file mode 100644 index 7a382e2cf..000000000 --- a/mdns/src/message/header.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::*; - -// Header is a representation of a DNS message header. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct Header { - pub id: u16, - pub response: bool, - pub op_code: OpCode, - pub authoritative: bool, - pub truncated: bool, - pub recursion_desired: bool, - pub recursion_available: bool, - pub rcode: RCode, -} - -impl fmt::Display for Header { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.Header{{id: {}, response: {}, op_code: {}, authoritative: {}, truncated: {}, recursion_desired: {}, recursion_available: {}, rcode: {} }}", - self.id, - self.response, - self.op_code, - self.authoritative, - self.truncated, - self.recursion_desired, - self.recursion_available, - self.rcode - ) - } -} - -impl Header { - pub fn pack(&self) -> (u16, u16) { - let id = self.id; - let mut bits = self.op_code << 11 | self.rcode as u16; - if self.recursion_available { - bits |= HEADER_BIT_RA - } - if self.recursion_desired { - bits |= HEADER_BIT_RD - } - if self.truncated { - bits |= HEADER_BIT_TC - } - if self.authoritative { - bits |= HEADER_BIT_AA - } - if self.response { - bits |= HEADER_BIT_QR - } - - (id, bits) - } -} - -#[derive(Default, Copy, Clone, PartialOrd, PartialEq, Eq)] -pub enum Section { - #[default] - NotStarted = 0, - Header = 1, - Questions = 2, - Answers = 3, - Authorities = 4, - Additionals = 5, - Done = 6, -} - -impl From for Section { - fn from(v: u8) -> Self { - match v { - 0 => Section::NotStarted, - 1 => Section::Header, - 2 => Section::Questions, - 3 => Section::Answers, - 4 => Section::Authorities, - 5 => Section::Additionals, - _ => Section::Done, - } - } -} - -impl fmt::Display for Section { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Section::NotStarted => "NotStarted", - Section::Header => "Header", - Section::Questions => "question", - Section::Answers => "answer", - Section::Authorities => "authority", - Section::Additionals => "additional", - Section::Done => "Done", - }; - write!(f, "{s}") - } -} - -// header is the wire format for a DNS message header. -#[derive(Default)] -pub struct HeaderInternal { - pub id: u16, - pub bits: u16, - pub questions: u16, - pub answers: u16, - pub authorities: u16, - pub additionals: u16, -} - -impl HeaderInternal { - pub(crate) fn count(&self, sec: Section) -> u16 { - match sec { - Section::Questions => self.questions, - Section::Answers => self.answers, - Section::Authorities => self.authorities, - Section::Additionals => self.additionals, - _ => 0, - } - } - - // pack appends the wire format of the header to msg. - pub(crate) fn pack(&self, mut msg: Vec) -> Vec { - msg = pack_uint16(msg, self.id); - msg = pack_uint16(msg, self.bits); - msg = pack_uint16(msg, self.questions); - msg = pack_uint16(msg, self.answers); - msg = pack_uint16(msg, self.authorities); - msg = pack_uint16(msg, self.additionals); - msg - } - - pub(crate) fn unpack(&mut self, msg: &[u8], off: usize) -> Result { - let (id, off) = unpack_uint16(msg, off)?; - self.id = id; - - let (bits, off) = unpack_uint16(msg, off)?; - self.bits = bits; - - let (questions, off) = unpack_uint16(msg, off)?; - self.questions = questions; - - let (answers, off) = unpack_uint16(msg, off)?; - self.answers = answers; - - let (authorities, off) = unpack_uint16(msg, off)?; - self.authorities = authorities; - - let (additionals, off) = unpack_uint16(msg, off)?; - self.additionals = additionals; - - Ok(off) - } - - pub(crate) fn header(&self) -> Header { - Header { - id: self.id, - response: (self.bits & HEADER_BIT_QR) != 0, - op_code: ((self.bits >> 11) & 0xF) as OpCode, - authoritative: (self.bits & HEADER_BIT_AA) != 0, - truncated: (self.bits & HEADER_BIT_TC) != 0, - recursion_desired: (self.bits & HEADER_BIT_RD) != 0, - recursion_available: (self.bits & HEADER_BIT_RA) != 0, - rcode: RCode::from((self.bits & 0xF) as u8), - } - } -} diff --git a/mdns/src/message/message_test.rs b/mdns/src/message/message_test.rs deleted file mode 100644 index d18acbab6..000000000 --- a/mdns/src/message/message_test.rs +++ /dev/null @@ -1,1311 +0,0 @@ -// Silence warning on complex types: -#![allow(clippy::type_complexity)] - -use std::collections::HashMap; - -use super::builder::*; -use super::header::*; -use super::name::*; -use super::parser::*; -use super::question::*; -use super::resource::a::*; -use super::resource::aaaa::*; -use super::resource::cname::*; -use super::resource::mx::*; -use super::resource::ns::*; -use super::resource::opt::*; -use super::resource::ptr::*; -use super::resource::soa::*; -use super::resource::srv::*; -use super::resource::txt::*; -use super::resource::*; -use super::*; -use crate::error::*; - -fn small_test_msg() -> Result { - let name = Name::new("example.com.")?; - Ok(Message { - header: Header { - response: true, - authoritative: true, - ..Default::default() - }, - questions: vec![Question { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - }], - answers: vec![Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 1] })), - }], - authorities: vec![Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 1] })), - }], - additionals: vec![Resource { - header: ResourceHeader { - name, - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 1] })), - }], - }) -} - -fn large_test_msg() -> Result { - let name = Name::new("foo.bar.example.com.")?; - Ok(Message { - header: Header { - response: true, - authoritative: true, - ..Default::default() - }, - questions: vec![Question { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - }], - answers: vec![ - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 1] })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 2] })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AaaaResource { - aaaa: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Cname, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(CnameResource { - cname: Name::new("alias.example.com.")?, - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Soa, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(SoaResource { - ns: Name::new("ns1.example.com.")?, - mbox: Name::new("mb.example.com.")?, - serial: 1, - refresh: 2, - retry: 3, - expire: 4, - min_ttl: 5, - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Ptr, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(PtrResource { - ptr: Name::new("ptr.example.com.")?, - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Mx, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(MxResource { - pref: 7, - mx: Name::new("mx.example.com.")?, - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Srv, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(SrvResource { - priority: 8, - weight: 9, - port: 11, - target: Name::new("srv.example.com.")?, - })), - }, - ], - authorities: vec![ - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Ns, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(NsResource { - ns: Name::new("ns1.example.com.")?, - })), - }, - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Ns, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(NsResource { - ns: Name::new("ns2.example.com.")?, - })), - }, - ], - additionals: vec![ - Resource { - header: ResourceHeader { - name: name.clone(), - typ: DnsType::Txt, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(TxtResource { - txt: vec!["So Long, and Thanks for All the Fish".into()], - })), - }, - Resource { - header: ResourceHeader { - name, - typ: DnsType::Txt, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(TxtResource { - txt: vec!["Hamster Huey and the Gooey Kablooie".into()], - })), - }, - Resource { - header: must_edns0_resource_header(4096, 0xfe0 | (RCode::Success as u32), false)?, - body: Some(Box::new(OptResource { - options: vec![DnsOption { - code: 10, // see RFC 7873 - data: vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef], - }], - })), - }, - ], - }) -} - -fn must_edns0_resource_header(l: u16, extrc: u32, d: bool) -> Result { - let mut h = ResourceHeader { - class: DNSCLASS_INET, - ..Default::default() - }; - h.set_edns0(l, extrc, d)?; - Ok(h) -} - -#[test] -fn test_name_string() -> Result<()> { - let want = "foo"; - let name = Name::new(want)?; - assert_eq!(name.to_string(), want); - - Ok(()) -} - -#[test] -fn test_question_pack_unpack() -> Result<()> { - let want = Question { - name: Name::new(".")?, - typ: DnsType::A, - class: DNSCLASS_INET, - }; - let buf = want.pack(vec![0; 1], &mut Some(HashMap::new()), 1)?; - let mut p = Parser { - msg: &buf, - header: HeaderInternal { - questions: 1, - ..Default::default() - }, - section: Section::Questions, - off: 1, - ..Default::default() - }; - - let got = p.question()?; - assert_eq!( - p.off, - buf.len(), - "unpacked different amount than packed: got = {}, want = {}", - p.off, - buf.len(), - ); - assert_eq!( - got, want, - "got from Parser.Question() = {got}, want = {want}" - ); - - Ok(()) -} - -#[test] -fn test_name() -> Result<()> { - let tests = vec![ - "", - ".", - "google..com", - "google.com", - "google..com.", - "google.com.", - ".google.com.", - "www..google.com.", - "www.google.com.", - ]; - - for test in tests { - let name = Name::new(test)?; - let ns = name.to_string(); - assert_eq!(ns, test, "got {name} = {ns}, want = {test}"); - } - - Ok(()) -} - -#[test] -fn test_name_pack_unpack() -> Result<()> { - let tests: Vec<(&str, &str, Option)> = vec![ - ("", "", Some(Error::ErrNonCanonicalName)), - (".", ".", None), - ("google..com", "", Some(Error::ErrNonCanonicalName)), - ("google.com", "", Some(Error::ErrNonCanonicalName)), - ("google..com.", "", Some(Error::ErrZeroSegLen)), - ("google.com.", "google.com.", None), - (".google.com.", "", Some(Error::ErrZeroSegLen)), - ("www..google.com.", "", Some(Error::ErrZeroSegLen)), - ("www.google.com.", "www.google.com.", None), - ]; - - for (input, want, want_err) in tests { - let input = Name::new(input)?; - let result = input.pack(vec![], &mut Some(HashMap::new()), 0); - if let Some(want_err) = want_err { - if let Err(actual_err) = result { - assert_eq!(actual_err, want_err); - } else { - panic!(); - } - continue; - } else { - assert!(result.is_ok()); - } - - let buf = result.unwrap(); - - let want = Name::new(want)?; - - let mut got = Name::default(); - let n = got.unpack(&buf, 0)?; - assert_eq!( - n, - buf.len(), - "unpacked different amount than packed for {}: got = {}, want = {}", - input, - n, - buf.len(), - ); - - assert_eq!( - got, want, - "unpacking packing of {input}: got = {got}, want = {want}" - ); - } - - Ok(()) -} - -#[test] -fn test_incompressible_name() -> Result<()> { - let name = Name::new("example.com.")?; - let mut compression = Some(HashMap::new()); - let buf = name.pack(vec![], &mut compression, 0)?; - let buf = name.pack(buf, &mut compression, 0)?; - let mut n1 = Name::default(); - let off = n1.unpack_compressed(&buf, 0, false /* allowCompression */)?; - let mut n2 = Name::default(); - let result = n2.unpack_compressed(&buf, off, false /* allowCompression */); - if let Err(err) = result { - assert_eq!( - Error::ErrCompressedSrv, - err, - "unpacking compressed incompressible name with pointers: got {}, want = {}", - err, - Error::ErrCompressedSrv - ); - } else { - panic!(); - } - - Ok(()) -} - -#[test] -fn test_header_unpack_error() -> Result<()> { - let wants = vec![ - "id", - "bits", - "questions", - "answers", - "authorities", - "additionals", - ]; - - let mut buf = vec![]; - for want in wants { - let mut h = HeaderInternal::default(); - let result = h.unpack(&buf, 0); - assert!(result.is_err(), "{}", want); - buf.extend_from_slice(&[0, 0]); - } - - Ok(()) -} - -#[test] -fn test_parser_start() -> Result<()> { - let mut p = Parser::default(); - let result = p.start(&[]); - assert!(result.is_err()); - - Ok(()) -} - -#[test] -fn test_resource_not_started() -> Result<()> { - let tests: Vec<(&str, Box) -> Result<()>>)> = vec![ - ( - "CNAMEResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "MXResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "NSResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "PTRResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "SOAResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "TXTResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "SRVResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "AResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ( - "AAAAResource", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.resource_body().map(|_| ()) }), - ), - ]; - - for (name, test_fn) in tests { - let mut p = Parser::default(); - if let Err(err) = test_fn(&mut p) { - assert_eq!(err, Error::ErrNotStarted, "{name}"); - } - } - - Ok(()) -} - -#[test] -fn test_srv_pack_unpack() -> Result<()> { - let want = Box::new(SrvResource { - priority: 8, - weight: 9, - port: 11, - target: Name::new("srv.example.com.")?, - }); - - let b = want.pack(vec![], &mut None, 0)?; - let mut got = SrvResource::default(); - got.unpack(&b, 0, 0)?; - assert_eq!(got.to_string(), want.to_string(),); - - Ok(()) -} - -#[test] -fn test_dns_pack_unpack() -> Result<()> { - let wants = vec![ - Message { - header: Header::default(), - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - answers: vec![], - authorities: vec![], - additionals: vec![], - }, - large_test_msg()?, - ]; - - for mut want in wants { - let b = want.pack()?; - let mut got = Message::default(); - got.unpack(&b)?; - assert_eq!(got.to_string(), want.to_string()); - } - - Ok(()) -} - -#[test] -fn test_dns_append_pack_unpack() -> Result<()> { - let wants = vec![ - Message { - header: Header::default(), - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - answers: vec![], - authorities: vec![], - additionals: vec![], - }, - large_test_msg()?, - ]; - - for mut want in wants { - let mut b = vec![0; 2]; - b = want.append_pack(b)?; - let mut got = Message::default(); - got.unpack(&b[2..])?; - assert_eq!(got.to_string(), want.to_string()); - } - - Ok(()) -} - -#[test] -fn test_skip_all() -> Result<()> { - let mut msg = large_test_msg()?; - let buf = msg.pack()?; - let mut p = Parser::default(); - p.start(&buf)?; - - for _ in 1..=3 { - p.skip_all_questions()?; - } - for _ in 1..=3 { - p.skip_all_answers()?; - } - for _ in 1..=3 { - p.skip_all_authorities()?; - } - for _ in 1..=3 { - p.skip_all_additionals()?; - } - - Ok(()) -} - -#[test] -fn test_skip_each() -> Result<()> { - let mut msg = small_test_msg()?; - let buf = msg.pack()?; - let mut p = Parser::default(); - p.start(&buf)?; - - // {"SkipQuestion", p.SkipQuestion}, - // {"SkipAnswer", p.SkipAnswer}, - // {"SkipAuthority", p.SkipAuthority}, - // {"SkipAdditional", p.SkipAdditional}, - - p.skip_question()?; - if let Err(err) = p.skip_question() { - assert_eq!(err, Error::ErrSectionDone); - } else { - panic!("expected error, but got ok"); - } - - p.skip_answer()?; - if let Err(err) = p.skip_answer() { - assert_eq!(err, Error::ErrSectionDone); - } else { - panic!("expected error, but got ok"); - } - - p.skip_authority()?; - if let Err(err) = p.skip_authority() { - assert_eq!(err, Error::ErrSectionDone); - } else { - panic!("expected error, but got ok"); - } - - p.skip_additional()?; - if let Err(err) = p.skip_additional() { - assert_eq!(err, Error::ErrSectionDone); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_skip_after_read() -> Result<()> { - let mut msg = small_test_msg()?; - let buf = msg.pack()?; - let mut p = Parser::default(); - p.start(&buf)?; - - let tests: Vec<(&str, Box) -> Result<()>>)> = vec![ - ( - "Question", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.question().map(|_| ()) }), - ), - ( - "Answer", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.answer().map(|_| ()) }), - ), - ( - "Authority", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.authority().map(|_| ()) }), - ), - ( - "Additional", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.additional().map(|_| ()) }), - ), - ]; - - for (name, read_fn) in tests { - read_fn(&mut p)?; - - let result = match name { - "Question" => p.skip_question(), - "Answer" => p.skip_answer(), - "Authority" => p.skip_authority(), - _ => p.skip_additional(), - }; - - if let Err(err) = result { - assert_eq!(err, Error::ErrSectionDone); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_skip_not_started() -> Result<()> { - let tests: Vec<(&str, Box) -> Result<()>>)> = vec![ - ( - "SkipAllQuestions", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.skip_all_questions() }), - ), - ( - "SkipAllAnswers", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.skip_all_answers() }), - ), - ( - "SkipAllAuthorities", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.skip_all_authorities() }), - ), - ( - "SkipAllAdditionals", - Box::new(|p: &mut Parser<'_>| -> Result<()> { p.skip_all_additionals() }), - ), - ]; - - let mut p = Parser::default(); - for (name, test_fn) in tests { - if let Err(err) = test_fn(&mut p) { - assert_eq!(err, Error::ErrNotStarted); - } else { - panic!("{name} expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_too_many_records() -> Result<()> { - let recs: usize = u16::MAX as usize + 1; - let tests = vec![ - ( - "Questions", - Message { - questions: vec![Question::default(); recs], - ..Default::default() - }, - Error::ErrTooManyQuestions, - ), - ( - "Answers", - Message { - answers: { - let mut a = vec![]; - for _ in 0..recs { - a.push(Resource::default()); - } - a - }, - ..Default::default() - }, - Error::ErrTooManyAnswers, - ), - ( - "Authorities", - Message { - authorities: { - let mut a = vec![]; - for _ in 0..recs { - a.push(Resource::default()); - } - a - }, - ..Default::default() - }, - Error::ErrTooManyAuthorities, - ), - ( - "Additionals", - Message { - additionals: { - let mut a = vec![]; - for _ in 0..recs { - a.push(Resource::default()); - } - a - }, - ..Default::default() - }, - Error::ErrTooManyAdditionals, - ), - ]; - - for (name, mut msg, want) in tests { - if let Err(got) = msg.pack() { - assert_eq!( - got, want, - "got Message.Pack() for {name} = {got}, want = {want}" - ) - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_very_long_txt() -> Result<()> { - let mut str255 = String::new(); - for _ in 0..255 { - str255.push('.'); - } - - let mut want = Resource { - header: ResourceHeader { - name: Name::new("foo.bar.example.com.")?, - typ: DnsType::Txt, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(TxtResource { - txt: vec![ - "".to_owned(), - "".to_owned(), - "foo bar".to_owned(), - "".to_owned(), - "www.example.com".to_owned(), - "www.example.com.".to_owned(), - str255, - ], - })), - }; - - let buf = want.pack(vec![], &mut Some(HashMap::new()), 0)?; - let mut got = Resource::default(); - let off = got.header.unpack(&buf, 0, 0)?; - let (body, n) = unpack_resource_body(got.header.typ, &buf, off, got.header.length as usize)?; - got.body = Some(body); - assert_eq!( - n, - buf.len(), - "unpacked different amount than packed: got = {}, want = {}", - n, - buf.len(), - ); - assert_eq!(got.to_string(), want.to_string()); - - Ok(()) -} - -#[test] -fn test_too_long_txt() -> Result<()> { - let mut str256 = String::new(); - for _ in 0..256 { - str256.push('.'); - } - let rb = TxtResource { txt: vec![str256] }; - if let Err(err) = rb.pack(vec![], &mut Some(HashMap::new()), 0) { - assert_eq!(err, Error::ErrStringTooLong); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_start_error() -> Result<()> { - let tests: Vec<(&str, Box Result<()>>)> = vec![ - ( - "Questions", - Box::new(|b: &mut Builder| -> Result<()> { b.start_questions() }), - ), - ( - "Answers", - Box::new(|b: &mut Builder| -> Result<()> { b.start_answers() }), - ), - ( - "Authorities", - Box::new(|b: &mut Builder| -> Result<()> { b.start_authorities() }), - ), - ( - "Additionals", - Box::new(|b: &mut Builder| -> Result<()> { b.start_additionals() }), - ), - ]; - - let envs: Vec<(&str, Box Builder>, Error)> = vec![ - ( - "sectionNotStarted", - Box::new(|| -> Builder { - Builder { - section: Section::NotStarted, - ..Default::default() - } - }), - Error::ErrNotStarted, - ), - ( - "sectionDone", - Box::new(|| -> Builder { - Builder { - section: Section::Done, - ..Default::default() - } - }), - Error::ErrSectionDone, - ), - ]; - - for (env_name, env_fn, env_err) in &envs { - for (test_name, test_fn) in &tests { - let mut b = env_fn(); - if let Err(got_err) = test_fn(&mut b) { - assert_eq!( - got_err, *env_err, - "got Builder{env_name}.{test_name} = {got_err}, want = {env_err}" - ); - } else { - panic!("{env_name}.{test_name}expected error, but got ok"); - } - } - } - - Ok(()) -} - -#[test] -fn test_builder_resource_error() -> Result<()> { - let tests: Vec<(&str, Box Result<()>>)> = vec![ - ( - "CNAMEResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "MXResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "NSResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "PTRResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "SOAResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "TXTResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "SRVResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "AResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "AAAAResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ( - "OPTResource", - Box::new(|b: &mut Builder| -> Result<()> { - b.add_resource(&mut Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }) - }), - ), - ]; - - let envs: Vec<(&str, Box Builder>, Error)> = vec![ - ( - "sectionNotStarted", - Box::new(|| -> Builder { - Builder { - section: Section::NotStarted, - ..Default::default() - } - }), - Error::ErrNotStarted, - ), - ( - "sectionHeader", - Box::new(|| -> Builder { - Builder { - section: Section::Header, - ..Default::default() - } - }), - Error::ErrNotStarted, - ), - ( - "sectionQuestions", - Box::new(|| -> Builder { - Builder { - section: Section::Questions, - ..Default::default() - } - }), - Error::ErrNotStarted, - ), - ( - "sectionDone", - Box::new(|| -> Builder { - Builder { - section: Section::Done, - ..Default::default() - } - }), - Error::ErrSectionDone, - ), - ]; - - for (env_name, env_fn, env_err) in &envs { - for (test_name, test_fn) in &tests { - let mut b = env_fn(); - if let Err(got_err) = test_fn(&mut b) { - assert_eq!( - got_err, *env_err, - "got Builder{env_name}.{test_name} = {got_err}, want = {env_err}" - ); - } else { - panic!("{env_name}.{test_name}expected error, but got ok"); - } - } - } - - Ok(()) -} - -#[test] -fn test_finish_error() -> Result<()> { - let mut b = Builder::default(); - let want = Error::ErrNotStarted; - if let Err(got) = b.finish() { - assert_eq!(got, want, "got Builder.Finish() = {got}, want = {want}"); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_builder() -> Result<()> { - let mut msg = large_test_msg()?; - let want = msg.pack()?; - - let mut b = Builder::new(&msg.header); - b.enable_compression(); - - b.start_questions()?; - for q in &msg.questions { - b.add_question(q)?; - } - - b.start_answers()?; - for r in &mut msg.answers { - b.add_resource(r)?; - } - - b.start_authorities()?; - for r in &mut msg.authorities { - b.add_resource(r)?; - } - - b.start_additionals()?; - for r in &mut msg.additionals { - b.add_resource(r)?; - } - - let got = b.finish()?; - assert_eq!( - got, - want, - "got.len()={}, want.len()={}", - got.len(), - want.len() - ); - - Ok(()) -} - -#[test] -fn test_resource_pack() -> Result<()> { - let tests = vec![ - ( - Message { - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - answers: vec![Resource { - header: ResourceHeader::default(), - body: None, - }], - ..Default::default() - }, - Error::ErrNilResourceBody, - ), - ( - Message { - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - authorities: vec![Resource { - header: ResourceHeader::default(), - body: Some(Box::::default()), - }], - ..Default::default() - }, - Error::ErrNonCanonicalName, - ), - ( - Message { - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::A, - class: DNSCLASS_INET, - }], - additionals: vec![Resource { - header: ResourceHeader::default(), - body: None, - }], - ..Default::default() - }, - Error::ErrNilResourceBody, - ), - ]; - - for (mut m, want_err) in tests { - if let Err(err) = m.pack() { - assert_eq!(err, want_err); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_resource_pack_length() -> Result<()> { - let mut r = Resource { - header: ResourceHeader { - name: Name::new(".")?, - typ: DnsType::A, - class: DNSCLASS_INET, - ..Default::default() - }, - body: Some(Box::new(AResource { a: [127, 0, 0, 2] })), - }; - - let (hb, _) = r.header.pack(vec![], &mut None, 0)?; - let buf = r.pack(vec![], &mut None, 0)?; - - let mut hdr = ResourceHeader::default(); - hdr.unpack(&buf, 0, 0)?; - - let (got, want) = (hdr.length as usize, buf.len() - hb.len()); - assert_eq!(got, want, "got hdr.Length = {got}, want = {want}"); - - Ok(()) -} - -#[test] -fn test_option_pack_unpack() -> Result<()> { - let tests = vec![ - ( - "without EDNS(0) options", - vec![ - 0x00, 0x00, 0x29, 0x10, 0x00, 0xfe, 0x00, 0x80, 0x00, 0x00, 0x00, - ], - Message { - header: Header { - rcode: RCode::FormatError, - ..Default::default() - }, - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::A, - class: DNSCLASS_INET, - }], - additionals: vec![Resource { - header: must_edns0_resource_header( - 4096, - 0xfe0 | RCode::FormatError as u32, - true, - )?, - body: Some(Box::::default()), - }], - ..Default::default() - }, - //true, - //0xfe0 | RCode::FormatError as u32, - ), - ( - "with EDNS(0) options", - vec![ - 0x00, 0x00, 0x29, 0x10, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x12, 0x34, - ], - Message { - header: Header { - rcode: RCode::ServerFailure, - ..Default::default() - }, - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - additionals: vec![Resource { - header: must_edns0_resource_header( - 4096, - 0xff0 | RCode::ServerFailure as u32, - false, - )?, - body: Some(Box::new(OptResource { - options: vec![ - DnsOption { - code: 12, // see RFC 7828 - data: vec![0x00, 0x00], - }, - DnsOption { - code: 11, // see RFC 7830 - data: vec![0x12, 0x34], - }, - ], - })), - }], - ..Default::default() - }, - //dnssecOK: false, - //extRCode: 0xff0 | RCodeServerFailure, - ), - ( - // Containing multiple OPT resources in a - // message is invalid, but it's necessary for - // protocol conformance testing. - "with multiple OPT resources", - vec![ - 0x00, 0x00, 0x29, 0x10, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0b, 0x00, - 0x02, 0x12, 0x34, 0x00, 0x00, 0x29, 0x10, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, - ], - Message { - header: Header { - rcode: RCode::NameError, - ..Default::default() - }, - questions: vec![Question { - name: Name::new(".")?, - typ: DnsType::Aaaa, - class: DNSCLASS_INET, - }], - additionals: vec![ - Resource { - header: must_edns0_resource_header( - 4096, - 0xff0 | RCode::NameError as u32, - false, - )?, - body: Some(Box::new(OptResource { - options: vec![DnsOption { - code: 11, // see RFC 7830 - data: vec![0x12, 0x34], - }], - })), - }, - Resource { - header: must_edns0_resource_header( - 4096, - 0xff0 | RCode::NameError as u32, - false, - )?, - body: Some(Box::new(OptResource { - options: vec![DnsOption { - code: 12, // see RFC 7828 - data: vec![0x00, 0x00], - }], - })), - }, - ], - ..Default::default() - }, - ), - ]; - - for (_tt_name, tt_w, mut tt_m) in tests { - let w = tt_m.pack()?; - - assert_eq!(&w[w.len() - tt_w.len()..], &tt_w[..]); - - let mut m = Message::default(); - m.unpack(&w)?; - - let ms: Vec = m.additionals.iter().map(|s| s.to_string()).collect(); - let tt_ms: Vec = tt_m.additionals.iter().map(|s| s.to_string()).collect(); - assert_eq!(ms, tt_ms); - } - - Ok(()) -} diff --git a/mdns/src/message/mod.rs b/mdns/src/message/mod.rs deleted file mode 100644 index ae1e35351..000000000 --- a/mdns/src/message/mod.rs +++ /dev/null @@ -1,349 +0,0 @@ -#[cfg(test)] -mod message_test; - -pub mod builder; -pub mod header; -pub mod name; -mod packer; -pub mod parser; -pub mod question; -pub mod resource; - -use std::collections::HashMap; -use std::fmt; - -use header::*; -use packer::*; -use parser::*; -use question::*; -use resource::*; - -use crate::error::*; - -// Message formats - -// A Type is a type of DNS request and response. -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] -pub enum DnsType { - // ResourceHeader.Type and question.Type - A = 1, - Ns = 2, - Cname = 5, - Soa = 6, - Ptr = 12, - Mx = 15, - Txt = 16, - Aaaa = 28, - Srv = 33, - Opt = 41, - - // question.Type - Wks = 11, - Hinfo = 13, - Minfo = 14, - Axfr = 252, - All = 255, - - #[default] - Unsupported = 0, -} - -impl From for DnsType { - fn from(v: u16) -> Self { - match v { - 1 => DnsType::A, - 2 => DnsType::Ns, - 5 => DnsType::Cname, - 6 => DnsType::Soa, - 12 => DnsType::Ptr, - 15 => DnsType::Mx, - 16 => DnsType::Txt, - 28 => DnsType::Aaaa, - 33 => DnsType::Srv, - 41 => DnsType::Opt, - - // question.Type - 11 => DnsType::Wks, - 13 => DnsType::Hinfo, - 14 => DnsType::Minfo, - 252 => DnsType::Axfr, - 255 => DnsType::All, - - _ => DnsType::Unsupported, - } - } -} - -impl fmt::Display for DnsType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - DnsType::A => "A", - DnsType::Ns => "NS", - DnsType::Cname => "CNAME", - DnsType::Soa => "SOA", - DnsType::Ptr => "PTR", - DnsType::Mx => "MX", - DnsType::Txt => "TXT", - DnsType::Aaaa => "AAAA", - DnsType::Srv => "SRV", - DnsType::Opt => "OPT", - DnsType::Wks => "WKS", - DnsType::Hinfo => "HINFO", - DnsType::Minfo => "MINFO", - DnsType::Axfr => "AXFR", - DnsType::All => "ALL", - _ => "Unsupported", - }; - write!(f, "{s}") - } -} - -impl DnsType { - // pack_type appends the wire format of field to msg. - pub(crate) fn pack(&self, msg: Vec) -> Vec { - pack_uint16(msg, *self as u16) - } - - pub(crate) fn unpack(&mut self, msg: &[u8], off: usize) -> Result { - let (t, o) = unpack_uint16(msg, off)?; - *self = DnsType::from(t); - Ok(o) - } - - pub(crate) fn skip(msg: &[u8], off: usize) -> Result { - skip_uint16(msg, off) - } -} - -// A Class is a type of network. -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] -pub struct DnsClass(pub u16); - -// ResourceHeader.Class and question.Class -pub const DNSCLASS_INET: DnsClass = DnsClass(1); -pub const DNSCLASS_CSNET: DnsClass = DnsClass(2); -pub const DNSCLASS_CHAOS: DnsClass = DnsClass(3); -pub const DNSCLASS_HESIOD: DnsClass = DnsClass(4); -// question.Class -pub const DNSCLASS_ANY: DnsClass = DnsClass(255); - -impl fmt::Display for DnsClass { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let other = format!("{}", self.0); - let s = match *self { - DNSCLASS_INET => "ClassINET", - DNSCLASS_CSNET => "ClassCSNET", - DNSCLASS_CHAOS => "ClassCHAOS", - DNSCLASS_HESIOD => "ClassHESIOD", - DNSCLASS_ANY => "ClassANY", - _ => other.as_str(), - }; - write!(f, "{s}") - } -} - -impl DnsClass { - // pack_class appends the wire format of field to msg. - pub(crate) fn pack(&self, msg: Vec) -> Vec { - pack_uint16(msg, self.0) - } - - pub(crate) fn unpack(&mut self, msg: &[u8], off: usize) -> Result { - let (c, o) = unpack_uint16(msg, off)?; - *self = DnsClass(c); - Ok(o) - } - - pub(crate) fn skip(msg: &[u8], off: usize) -> Result { - skip_uint16(msg, off) - } -} - -// An OpCode is a DNS operation code. -pub type OpCode = u16; - -// An RCode is a DNS response status code. -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] -pub enum RCode { - // Message.Rcode - #[default] - Success = 0, - FormatError = 1, - ServerFailure = 2, - NameError = 3, - NotImplemented = 4, - Refused = 5, - Unsupported, -} - -impl From for RCode { - fn from(v: u8) -> Self { - match v { - 0 => RCode::Success, - 1 => RCode::FormatError, - 2 => RCode::ServerFailure, - 3 => RCode::NameError, - 4 => RCode::NotImplemented, - 5 => RCode::Refused, - _ => RCode::Unsupported, - } - } -} - -impl fmt::Display for RCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RCode::Success => "RCodeSuccess", - RCode::FormatError => "RCodeFormatError", - RCode::ServerFailure => "RCodeServerFailure", - RCode::NameError => "RCodeNameError", - RCode::NotImplemented => "RCodeNotImplemented", - RCode::Refused => "RCodeRefused", - RCode::Unsupported => "RCodeUnsupported", - }; - write!(f, "{s}") - } -} - -// Internal constants. - -// PACK_STARTING_CAP is the default initial buffer size allocated during -// packing. -// -// The starting capacity doesn't matter too much, but most DNS responses -// Will be <= 512 bytes as it is the limit for DNS over UDP. -const PACK_STARTING_CAP: usize = 512; - -// UINT16LEN is the length (in bytes) of a uint16. -const UINT16LEN: usize = 2; - -// UINT32LEN is the length (in bytes) of a uint32. -const UINT32LEN: usize = 4; - -// HEADER_LEN is the length (in bytes) of a DNS header. -// -// A header is comprised of 6 uint16s and no padding. -const HEADER_LEN: usize = 6 * UINT16LEN; - -const HEADER_BIT_QR: u16 = 1 << 15; // query/response (response=1) -const HEADER_BIT_AA: u16 = 1 << 10; // authoritative -const HEADER_BIT_TC: u16 = 1 << 9; // truncated -const HEADER_BIT_RD: u16 = 1 << 8; // recursion desired -const HEADER_BIT_RA: u16 = 1 << 7; // recursion available - -// Message is a representation of a DNS message. -#[derive(Default, Debug)] -pub struct Message { - pub header: Header, - pub questions: Vec, - pub answers: Vec, - pub authorities: Vec, - pub additionals: Vec, -} - -impl fmt::Display for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut s = "dnsmessage.Message{Header: ".to_owned(); - s += self.header.to_string().as_str(); - - s += ", Questions: "; - let v: Vec = self.questions.iter().map(|q| q.to_string()).collect(); - s += &v.join(", "); - - s += ", Answers: "; - let v: Vec = self.answers.iter().map(|q| q.to_string()).collect(); - s += &v.join(", "); - - s += ", Authorities: "; - let v: Vec = self.authorities.iter().map(|q| q.to_string()).collect(); - s += &v.join(", "); - - s += ", Additionals: "; - let v: Vec = self.additionals.iter().map(|q| q.to_string()).collect(); - s += &v.join(", "); - - write!(f, "{s}") - } -} - -impl Message { - // Unpack parses a full Message. - pub fn unpack(&mut self, msg: &[u8]) -> Result<()> { - let mut p = Parser::default(); - self.header = p.start(msg)?; - self.questions = p.all_questions()?; - self.answers = p.all_answers()?; - self.authorities = p.all_authorities()?; - self.additionals = p.all_additionals()?; - Ok(()) - } - - // Pack packs a full Message. - pub fn pack(&mut self) -> Result> { - self.append_pack(vec![]) - } - - // append_pack is like Pack but appends the full Message to b and returns the - // extended buffer. - pub fn append_pack(&mut self, b: Vec) -> Result> { - // Validate the lengths. It is very unlikely that anyone will try to - // pack more than 65535 of any particular type, but it is possible and - // we should fail gracefully. - if self.questions.len() > u16::MAX as usize { - return Err(Error::ErrTooManyQuestions); - } - if self.answers.len() > u16::MAX as usize { - return Err(Error::ErrTooManyAnswers); - } - if self.authorities.len() > u16::MAX as usize { - return Err(Error::ErrTooManyAuthorities); - } - if self.additionals.len() > u16::MAX as usize { - return Err(Error::ErrTooManyAdditionals); - } - - let (id, bits) = self.header.pack(); - - let questions = self.questions.len() as u16; - let answers = self.answers.len() as u16; - let authorities = self.authorities.len() as u16; - let additionals = self.additionals.len() as u16; - - let h = HeaderInternal { - id, - bits, - questions, - answers, - authorities, - additionals, - }; - - let compression_off = b.len(); - let mut msg = h.pack(b); - - // RFC 1035 allows (but does not require) compression for packing. RFC - // 1035 requires unpacking implementations to support compression, so - // unconditionally enabling it is fine. - // - // DNS lookups are typically done over UDP, and RFC 1035 states that UDP - // DNS messages can be a maximum of 512 bytes long. Without compression, - // many DNS response messages are over this limit, so enabling - // compression will help ensure compliance. - let mut compression = Some(HashMap::new()); - - for question in &self.questions { - msg = question.pack(msg, &mut compression, compression_off)?; - } - for answer in &mut self.answers { - msg = answer.pack(msg, &mut compression, compression_off)?; - } - for authority in &mut self.authorities { - msg = authority.pack(msg, &mut compression, compression_off)?; - } - for additional in &mut self.additionals { - msg = additional.pack(msg, &mut compression, compression_off)?; - } - - Ok(msg) - } -} diff --git a/mdns/src/message/name.rs b/mdns/src/message/name.rs deleted file mode 100644 index f813fbd0f..000000000 --- a/mdns/src/message/name.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::collections::HashMap; -use std::fmt; - -use crate::error::*; - -const NAME_LEN: usize = 255; - -// A Name is a non-encoded domain name. It is used instead of strings to avoid -// allocations. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct Name { - pub data: String, -} - -// String implements fmt.Stringer.String. -impl fmt::Display for Name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.data) - } -} - -impl Name { - pub fn new(data: &str) -> Result { - if data.len() > NAME_LEN { - Err(Error::ErrCalcLen) - } else { - Ok(Name { - data: data.to_owned(), - }) - } - } - - // pack appends the wire format of the Name to msg. - // - // Domain names are a sequence of counted strings split at the dots. They end - // with a zero-length string. Compression can be used to reuse domain suffixes. - // - // The compression map will be updated with new domain suffixes. If compression - // is nil, compression will not be used. - pub fn pack( - &self, - mut msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - let data = self.data.as_bytes(); - - // Add a trailing dot to canonicalize name. - if data.is_empty() || data[data.len() - 1] != b'.' { - return Err(Error::ErrNonCanonicalName); - } - - // Allow root domain. - if data.len() == 1 && data[0] == b'.' { - msg.push(0); - return Ok(msg); - } - - // Emit sequence of counted strings, chopping at dots. - let mut begin = 0; - for i in 0..data.len() { - // Check for the end of the segment. - if data[i] == b'.' { - // The two most significant bits have special meaning. - // It isn't allowed for segments to be long enough to - // need them. - if i - begin >= (1 << 6) { - return Err(Error::ErrSegTooLong); - } - - // Segments must have a non-zero length. - if i - begin == 0 { - return Err(Error::ErrZeroSegLen); - } - - msg.push((i - begin) as u8); - msg.extend_from_slice(&data[begin..i]); - - begin = i + 1; - continue; - } - - // We can only compress domain suffixes starting with a new - // segment. A pointer is two bytes with the two most significant - // bits set to 1 to indicate that it is a pointer. - if i == 0 || data[i - 1] == b'.' { - if let Some(compression) = compression { - let key: String = self.data.chars().skip(i).collect(); - if let Some(ptr) = compression.get(&key) { - // Hit. Emit a pointer instead of the rest of - // the domain. - msg.push(((ptr >> 8) | 0xC0) as u8); - msg.push((ptr & 0xFF) as u8); - return Ok(msg); - } - - // Miss. Add the suffix to the compression table if the - // offset can be stored in the available 14 bytes. - if msg.len() <= 0x3FFF { - compression.insert(key, msg.len() - compression_off); - } - } - } - } - - msg.push(0); - Ok(msg) - } - - // unpack unpacks a domain name. - pub fn unpack(&mut self, msg: &[u8], off: usize) -> Result { - self.unpack_compressed(msg, off, true /* allowCompression */) - } - - pub fn unpack_compressed( - &mut self, - msg: &[u8], - off: usize, - allow_compression: bool, - ) -> Result { - // curr_off is the current working offset. - let mut curr_off = off; - - // new_off is the offset where the next record will start. Pointers lead - // to data that belongs to other names and thus doesn't count towards to - // the usage of this name. - let mut new_off = off; - - // ptr is the number of pointers followed. - let mut ptr = 0; - - // Name is a slice representation of the name data. - let mut name = String::new(); //n.Data[:0] - - loop { - if curr_off >= msg.len() { - return Err(Error::ErrBaseLen); - } - let c = msg[curr_off]; - curr_off += 1; - match c & 0xC0 { - 0x00 => { - // String segment - if c == 0x00 { - // A zero length signals the end of the name. - break; - } - let end_off = curr_off + c as usize; - if end_off > msg.len() { - return Err(Error::ErrCalcLen); - } - name.push_str(String::from_utf8(msg[curr_off..end_off].to_vec())?.as_str()); - name.push('.'); - curr_off = end_off; - } - 0xC0 => { - // Pointer - if !allow_compression { - return Err(Error::ErrCompressedSrv); - } - if curr_off >= msg.len() { - return Err(Error::ErrInvalidPtr); - } - let c1 = msg[curr_off]; - curr_off += 1; - if ptr == 0 { - new_off = curr_off; - } - // Don't follow too many pointers, maybe there's a loop. - ptr += 1; - if ptr > 10 { - return Err(Error::ErrTooManyPtr); - } - curr_off = ((c ^ 0xC0) as usize) << 8 | (c1 as usize); - } - _ => { - // Prefixes 0x80 and 0x40 are reserved. - return Err(Error::ErrReserved); - } - } - } - if name.is_empty() { - name.push('.'); - } - if name.len() > NAME_LEN { - return Err(Error::ErrCalcLen); - } - self.data = name; - if ptr == 0 { - new_off = curr_off; - } - Ok(new_off) - } - - pub(crate) fn skip(msg: &[u8], off: usize) -> Result { - // new_off is the offset where the next record will start. Pointers lead - // to data that belongs to other names and thus doesn't count towards to - // the usage of this name. - let mut new_off = off; - - loop { - if new_off >= msg.len() { - return Err(Error::ErrBaseLen); - } - let c = msg[new_off]; - new_off += 1; - match c & 0xC0 { - 0x00 => { - if c == 0x00 { - // A zero length signals the end of the name. - break; - } - // literal string - new_off += c as usize; - if new_off > msg.len() { - return Err(Error::ErrCalcLen); - } - } - 0xC0 => { - // Pointer to somewhere else in msg. - - // Pointers are two bytes. - new_off += 1; - - // Don't follow the pointer as the data here has ended. - break; - } - _ => { - // Prefixes 0x80 and 0x40 are reserved. - return Err(Error::ErrReserved); - } - } - } - - Ok(new_off) - } -} diff --git a/mdns/src/message/packer.rs b/mdns/src/message/packer.rs deleted file mode 100644 index 76c617cc5..000000000 --- a/mdns/src/message/packer.rs +++ /dev/null @@ -1,92 +0,0 @@ -use super::*; -use crate::error::*; - -// pack_bytes appends the wire format of field to msg. -pub(crate) fn pack_bytes(mut msg: Vec, field: &[u8]) -> Vec { - msg.extend_from_slice(field); - msg -} - -pub(crate) fn unpack_bytes(msg: &[u8], off: usize, field: &mut [u8]) -> Result { - let new_off = off + field.len(); - if new_off > msg.len() { - return Err(Error::ErrBaseLen); - } - field.copy_from_slice(&msg[off..new_off]); - Ok(new_off) -} - -// pack_uint16 appends the wire format of field to msg. -pub(crate) fn pack_uint16(mut msg: Vec, field: u16) -> Vec { - msg.extend_from_slice(&field.to_be_bytes()); - msg -} - -pub(crate) fn unpack_uint16(msg: &[u8], off: usize) -> Result<(u16, usize)> { - if off + UINT16LEN > msg.len() { - return Err(Error::ErrBaseLen); - } - - Ok(( - (msg[off] as u16) << 8 | (msg[off + 1] as u16), - off + UINT16LEN, - )) -} - -pub(crate) fn skip_uint16(msg: &[u8], off: usize) -> Result { - if off + UINT16LEN > msg.len() { - return Err(Error::ErrBaseLen); - } - Ok(off + UINT16LEN) -} - -// pack_uint32 appends the wire format of field to msg. -pub(crate) fn pack_uint32(mut msg: Vec, field: u32) -> Vec { - msg.extend_from_slice(&field.to_be_bytes()); - msg -} - -pub(crate) fn unpack_uint32(msg: &[u8], off: usize) -> Result<(u32, usize)> { - if off + UINT32LEN > msg.len() { - return Err(Error::ErrBaseLen); - } - let v = (msg[off] as u32) << 24 - | (msg[off + 1] as u32) << 16 - | (msg[off + 2] as u32) << 8 - | (msg[off + 3] as u32); - Ok((v, off + UINT32LEN)) -} - -pub(crate) fn skip_uint32(msg: &[u8], off: usize) -> Result { - if off + UINT32LEN > msg.len() { - return Err(Error::ErrBaseLen); - } - Ok(off + UINT32LEN) -} - -// pack_text appends the wire format of field to msg. -pub(crate) fn pack_str(mut msg: Vec, field: &str) -> Result> { - let l = field.len(); - if l > 255 { - return Err(Error::ErrStringTooLong); - } - msg.push(l as u8); - msg.extend_from_slice(field.as_bytes()); - Ok(msg) -} - -pub(crate) fn unpack_str(msg: &[u8], off: usize) -> Result<(String, usize)> { - if off >= msg.len() { - return Err(Error::ErrBaseLen); - } - let begin_off = off + 1; - let end_off = begin_off + msg[off] as usize; - if end_off > msg.len() { - return Err(Error::ErrCalcLen); - } - - Ok(( - String::from_utf8(msg[begin_off..end_off].to_vec())?, - end_off, - )) -} diff --git a/mdns/src/message/parser.rs b/mdns/src/message/parser.rs deleted file mode 100644 index 8b120f8ee..000000000 --- a/mdns/src/message/parser.rs +++ /dev/null @@ -1,347 +0,0 @@ -use crate::error::*; -use crate::message::header::{Header, HeaderInternal, Section}; -use crate::message::name::Name; -use crate::message::question::Question; -use crate::message::resource::{unpack_resource_body, Resource, ResourceBody, ResourceHeader}; -use crate::message::{DnsClass, DnsType}; - -// A Parser allows incrementally parsing a DNS message. -// -// When parsing is started, the Header is parsed. Next, each question can be -// either parsed or skipped. Alternatively, all Questions can be skipped at -// once. When all Questions have been parsed, attempting to parse Questions -// will return (nil, nil) and attempting to skip Questions will return -// (true, nil). After all Questions have been either parsed or skipped, all -// Answers, Authorities and Additionals can be either parsed or skipped in the -// same way, and each type of Resource must be fully parsed or skipped before -// proceeding to the next type of Resource. -// -// Note that there is no requirement to fully skip or parse the message. -#[derive(Default)] -pub struct Parser<'a> { - pub msg: &'a [u8], - pub header: HeaderInternal, - - pub section: Section, - pub off: usize, - pub index: usize, - pub res_header_valid: bool, - pub res_header: ResourceHeader, -} - -impl<'a> Parser<'a> { - // start parses the header and enables the parsing of Questions. - pub fn start(&mut self, msg: &'a [u8]) -> Result
{ - *self = Parser { - msg, - ..Default::default() - }; - self.off = self.header.unpack(msg, 0)?; - self.section = Section::Questions; - Ok(self.header.header()) - } - - fn check_advance(&mut self, sec: Section) -> Result<()> { - if self.section < sec { - return Err(Error::ErrNotStarted); - } - if self.section > sec { - return Err(Error::ErrSectionDone); - } - self.res_header_valid = false; - if self.index == self.header.count(sec) as usize { - self.index = 0; - self.section = Section::from(1 + self.section as u8); - return Err(Error::ErrSectionDone); - } - Ok(()) - } - - fn resource(&mut self, sec: Section) -> Result { - let header = self.resource_header(sec)?; - self.res_header_valid = false; - let (body, off) = - unpack_resource_body(header.typ, self.msg, self.off, header.length as usize)?; - self.off = off; - self.index += 1; - Ok(Resource { - header, - body: Some(body), - }) - } - - fn resource_header(&mut self, sec: Section) -> Result { - if self.res_header_valid { - return Ok(self.res_header.clone()); - } - self.check_advance(sec)?; - let mut hdr = ResourceHeader::default(); - let off = hdr.unpack(self.msg, self.off, 0)?; - - self.res_header_valid = true; - self.res_header = hdr.clone(); - self.off = off; - Ok(hdr) - } - - fn skip_resource(&mut self, sec: Section) -> Result<()> { - if self.res_header_valid { - let new_off = self.off + self.res_header.length as usize; - if new_off > self.msg.len() { - return Err(Error::ErrResourceLen); - } - self.off = new_off; - self.res_header_valid = false; - self.index += 1; - return Ok(()); - } - self.check_advance(sec)?; - - self.off = Resource::skip(self.msg, self.off)?; - self.index += 1; - Ok(()) - } - - // question parses a single question. - pub fn question(&mut self) -> Result { - self.check_advance(Section::Questions)?; - let mut name = Name::new("")?; - let mut off = name.unpack(self.msg, self.off)?; - let mut typ = DnsType::Unsupported; - off = typ.unpack(self.msg, off)?; - let mut class = DnsClass::default(); - off = class.unpack(self.msg, off)?; - self.off = off; - self.index += 1; - Ok(Question { name, typ, class }) - } - - // all_questions parses all Questions. - pub fn all_questions(&mut self) -> Result> { - // Multiple questions are valid according to the spec, - // but servers don't actually support them. There will - // be at most one question here. - // - // Do not pre-allocate based on info in self.header, since - // the data is untrusted. - let mut qs = vec![]; - loop { - match self.question() { - Err(err) => { - if Error::ErrSectionDone == err { - return Ok(qs); - } else { - return Err(err); - } - } - Ok(q) => qs.push(q), - } - } - } - - // skip_question skips a single question. - pub fn skip_question(&mut self) -> Result<()> { - self.check_advance(Section::Questions)?; - let mut off = Name::skip(self.msg, self.off)?; - off = DnsType::skip(self.msg, off)?; - off = DnsClass::skip(self.msg, off)?; - self.off = off; - self.index += 1; - Ok(()) - } - - // skip_all_questions skips all Questions. - pub fn skip_all_questions(&mut self) -> Result<()> { - loop { - if let Err(err) = self.skip_question() { - if Error::ErrSectionDone == err { - return Ok(()); - } else { - return Err(err); - } - } - } - } - - // answer_header parses a single answer ResourceHeader. - pub fn answer_header(&mut self) -> Result { - self.resource_header(Section::Answers) - } - - // answer parses a single answer Resource. - pub fn answer(&mut self) -> Result { - self.resource(Section::Answers) - } - - // all_answers parses all answer Resources. - pub fn all_answers(&mut self) -> Result> { - // The most common query is for A/AAAA, which usually returns - // a handful of IPs. - // - // Pre-allocate up to a certain limit, since self.header is - // untrusted data. - let mut n = self.header.answers as usize; - if n > 20 { - n = 20 - } - let mut a = Vec::with_capacity(n); - loop { - match self.answer() { - Err(err) => { - if Error::ErrSectionDone == err { - return Ok(a); - } else { - return Err(err); - } - } - Ok(r) => a.push(r), - } - } - } - - // skip_answer skips a single answer Resource. - pub fn skip_answer(&mut self) -> Result<()> { - self.skip_resource(Section::Answers) - } - - // skip_all_answers skips all answer Resources. - pub fn skip_all_answers(&mut self) -> Result<()> { - loop { - if let Err(err) = self.skip_answer() { - if Error::ErrSectionDone == err { - return Ok(()); - } else { - return Err(err); - } - } - } - } - - // authority_header parses a single authority ResourceHeader. - pub fn authority_header(&mut self) -> Result { - self.resource_header(Section::Authorities) - } - - // authority parses a single authority Resource. - pub fn authority(&mut self) -> Result { - self.resource(Section::Authorities) - } - - // all_authorities parses all authority Resources. - pub fn all_authorities(&mut self) -> Result> { - // Authorities contains SOA in case of NXDOMAIN and friends, - // otherwise it is empty. - // - // Pre-allocate up to a certain limit, since self.header is - // untrusted data. - let mut n = self.header.authorities as usize; - if n > 10 { - n = 10; - } - let mut a = Vec::with_capacity(n); - loop { - match self.authority() { - Err(err) => { - if Error::ErrSectionDone == err { - return Ok(a); - } else { - return Err(err); - } - } - Ok(r) => a.push(r), - } - } - } - - // skip_authority skips a single authority Resource. - pub fn skip_authority(&mut self) -> Result<()> { - self.skip_resource(Section::Authorities) - } - - // skip_all_authorities skips all authority Resources. - pub fn skip_all_authorities(&mut self) -> Result<()> { - loop { - if let Err(err) = self.skip_authority() { - if Error::ErrSectionDone == err { - return Ok(()); - } else { - return Err(err); - } - } - } - } - - // additional_header parses a single additional ResourceHeader. - pub fn additional_header(&mut self) -> Result { - self.resource_header(Section::Additionals) - } - - // additional parses a single additional Resource. - pub fn additional(&mut self) -> Result { - self.resource(Section::Additionals) - } - - // all_additionals parses all additional Resources. - pub fn all_additionals(&mut self) -> Result> { - // Additionals usually contain OPT, and sometimes A/AAAA - // glue records. - // - // Pre-allocate up to a certain limit, since self.header is - // untrusted data. - let mut n = self.header.additionals as usize; - if n > 10 { - n = 10; - } - let mut a = Vec::with_capacity(n); - loop { - match self.additional() { - Err(err) => { - if Error::ErrSectionDone == err { - return Ok(a); - } else { - return Err(err); - } - } - Ok(r) => a.push(r), - } - } - } - - // skip_additional skips a single additional Resource. - pub fn skip_additional(&mut self) -> Result<()> { - self.skip_resource(Section::Additionals) - } - - // skip_all_additionals skips all additional Resources. - pub fn skip_all_additionals(&mut self) -> Result<()> { - loop { - if let Err(err) = self.skip_additional() { - if Error::ErrSectionDone == err { - return Ok(()); - } else { - return Err(err); - } - } - } - } - - // resource_body parses a single resource_boy. - // - // One of the XXXHeader methods must have been called before calling this - // method. - pub fn resource_body(&mut self) -> Result> { - if !self.res_header_valid { - return Err(Error::ErrNotStarted); - } - let (rb, _off) = unpack_resource_body( - self.res_header.typ, - self.msg, - self.off, - self.res_header.length as usize, - )?; - self.off += self.res_header.length as usize; - self.res_header_valid = false; - self.index += 1; - Ok(rb) - } -} diff --git a/mdns/src/message/question.rs b/mdns/src/message/question.rs deleted file mode 100644 index ef2023244..000000000 --- a/mdns/src/message/question.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::collections::HashMap; -use std::fmt; - -use super::name::*; -use super::*; -use crate::error::Result; - -// A question is a DNS query. -#[derive(Default, Debug, PartialEq, Eq, Clone)] -pub struct Question { - pub name: Name, - pub typ: DnsType, - pub class: DnsClass, -} - -impl fmt::Display for Question { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.question{{Name: {}, Type: {}, Class: {}}}", - self.name, self.typ, self.class - ) - } -} - -impl Question { - // pack appends the wire format of the question to msg. - pub fn pack( - &self, - mut msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - msg = self.name.pack(msg, compression, compression_off)?; - msg = self.typ.pack(msg); - Ok(self.class.pack(msg)) - } -} diff --git a/mdns/src/message/resource/a.rs b/mdns/src/message/resource/a.rs deleted file mode 100644 index dedb1d942..000000000 --- a/mdns/src/message/resource/a.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::*; -use crate::message::packer::*; - -// An AResource is an A Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct AResource { - pub a: [u8; 4], -} - -impl fmt::Display for AResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dnsmessage.AResource{{A: {:?}}}", self.a) - } -} - -impl ResourceBody for AResource { - fn real_type(&self) -> DnsType { - DnsType::A - } - - // pack appends the wire format of the AResource to msg. - fn pack( - &self, - msg: Vec, - _compression: &mut Option>, - _compression_off: usize, - ) -> Result> { - Ok(pack_bytes(msg, &self.a)) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - unpack_bytes(msg, off, &mut self.a) - } -} diff --git a/mdns/src/message/resource/aaaa.rs b/mdns/src/message/resource/aaaa.rs deleted file mode 100644 index 6a23da84a..000000000 --- a/mdns/src/message/resource/aaaa.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::*; -use crate::message::packer::*; - -// An AAAAResource is an aaaa Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct AaaaResource { - pub aaaa: [u8; 16], -} - -impl fmt::Display for AaaaResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dnsmessage.AAAAResource{{aaaa: {:?}}}", self.aaaa) - } -} - -impl ResourceBody for AaaaResource { - fn real_type(&self) -> DnsType { - DnsType::Aaaa - } - - // pack appends the wire format of the AAAAResource to msg. - fn pack( - &self, - msg: Vec, - _compression: &mut Option>, - _compression_off: usize, - ) -> Result> { - Ok(pack_bytes(msg, &self.aaaa)) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - unpack_bytes(msg, off, &mut self.aaaa) - } -} diff --git a/mdns/src/message/resource/cname.rs b/mdns/src/message/resource/cname.rs deleted file mode 100644 index b2e2281dd..000000000 --- a/mdns/src/message/resource/cname.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::*; -use crate::message::name::*; - -// A cnameresource is a cname Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct CnameResource { - pub cname: Name, -} - -impl fmt::Display for CnameResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dnsmessage.cnameresource{{cname: {}}}", self.cname) - } -} - -impl ResourceBody for CnameResource { - fn real_type(&self) -> DnsType { - DnsType::Cname - } - - // pack appends the wire format of the cnameresource to msg. - fn pack( - &self, - msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - self.cname.pack(msg, compression, compression_off) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - self.cname.unpack(msg, off) - } -} diff --git a/mdns/src/message/resource/mod.rs b/mdns/src/message/resource/mod.rs deleted file mode 100644 index 6ca5e57bd..000000000 --- a/mdns/src/message/resource/mod.rs +++ /dev/null @@ -1,273 +0,0 @@ -pub mod a; -pub mod aaaa; -pub mod cname; -pub mod mx; -pub mod ns; -pub mod opt; -pub mod ptr; -pub mod soa; -pub mod srv; -pub mod txt; - -use std::collections::HashMap; -use std::fmt; - -use a::*; -use aaaa::*; -use cname::*; -use mx::*; -use ns::*; -use opt::*; -use ptr::*; -use soa::*; -use srv::*; -use txt::*; - -use super::name::*; -use super::packer::*; -use super::*; -use crate::error::*; - -// EDNS(0) wire constants. - -const EDNS0_VERSION: u32 = 0; -const EDNS0_DNSSEC_OK: u32 = 0x00008000; -const EDNS_VERSION_MASK: u32 = 0x00ff0000; -const EDNS0_DNSSEC_OK_MASK: u32 = 0x00ff8000; - -// A Resource is a DNS resource record. -#[derive(Default, Debug)] -pub struct Resource { - pub header: ResourceHeader, - pub body: Option>, -} - -impl fmt::Display for Resource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.Resource{{Header: {}, Body: {}}}", - self.header, - if let Some(body) = &self.body { - body.to_string() - } else { - "None".to_owned() - } - ) - } -} - -impl Resource { - // pack appends the wire format of the Resource to msg. - pub fn pack( - &mut self, - msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - if let Some(body) = &self.body { - self.header.typ = body.real_type(); - } else { - return Err(Error::ErrNilResourceBody); - } - let (mut msg, len_off) = self.header.pack(msg, compression, compression_off)?; - let pre_len = msg.len(); - if let Some(body) = &self.body { - msg = body.pack(msg, compression, compression_off)?; - self.header.fix_len(&mut msg, len_off, pre_len)?; - } - Ok(msg) - } - - pub fn unpack(&mut self, msg: &[u8], mut off: usize) -> Result { - off = self.header.unpack(msg, off, 0)?; - let (rb, off) = - unpack_resource_body(self.header.typ, msg, off, self.header.length as usize)?; - self.body = Some(rb); - Ok(off) - } - - pub(crate) fn skip(msg: &[u8], off: usize) -> Result { - let mut new_off = Name::skip(msg, off)?; - new_off = DnsType::skip(msg, new_off)?; - new_off = DnsClass::skip(msg, new_off)?; - new_off = skip_uint32(msg, new_off)?; - let (length, mut new_off) = unpack_uint16(msg, new_off)?; - new_off += length as usize; - if new_off > msg.len() { - return Err(Error::ErrResourceLen); - } - Ok(new_off) - } -} - -// A ResourceHeader is the header of a DNS resource record. There are -// many types of DNS resource records, but they all share the same header. -#[derive(Clone, Default, PartialEq, Eq, Debug)] -pub struct ResourceHeader { - // Name is the domain name for which this resource record pertains. - pub name: Name, - - // Type is the type of DNS resource record. - // - // This field will be set automatically during packing. - pub typ: DnsType, - - // Class is the class of network to which this DNS resource record - // pertains. - pub class: DnsClass, - - // TTL is the length of time (measured in seconds) which this resource - // record is valid for (time to live). All Resources in a set should - // have the same TTL (RFC 2181 Section 5.2). - pub ttl: u32, - - // Length is the length of data in the resource record after the header. - // - // This field will be set automatically during packing. - pub length: u16, -} - -impl fmt::Display for ResourceHeader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.ResourceHeader{{Name: {}, Type: {}, Class: {}, TTL: {}, Length: {}}}", - self.name, self.typ, self.class, self.ttl, self.length, - ) - } -} - -impl ResourceHeader { - // pack appends the wire format of the ResourceHeader to oldMsg. - // - // lenOff is the offset in msg where the Length field was packed. - pub fn pack( - &self, - mut msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result<(Vec, usize)> { - msg = self.name.pack(msg, compression, compression_off)?; - msg = self.typ.pack(msg); - msg = self.class.pack(msg); - msg = pack_uint32(msg, self.ttl); - let len_off = msg.len(); - msg = pack_uint16(msg, self.length); - Ok((msg, len_off)) - } - - pub fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - let mut new_off = off; - new_off = self.name.unpack(msg, new_off)?; - new_off = self.typ.unpack(msg, new_off)?; - new_off = self.class.unpack(msg, new_off)?; - let (ttl, new_off) = unpack_uint32(msg, new_off)?; - self.ttl = ttl; - let (l, new_off) = unpack_uint16(msg, new_off)?; - self.length = l; - - Ok(new_off) - } - - // fixLen updates a packed ResourceHeader to include the length of the - // ResourceBody. - // - // lenOff is the offset of the ResourceHeader.Length field in msg. - // - // preLen is the length that msg was before the ResourceBody was packed. - pub fn fix_len(&mut self, msg: &mut [u8], len_off: usize, pre_len: usize) -> Result<()> { - if msg.len() < pre_len || msg.len() > pre_len + u16::MAX as usize { - return Err(Error::ErrResTooLong); - } - - let con_len = msg.len() - pre_len; - - // Fill in the length now that we know how long the content is. - msg[len_off] = ((con_len >> 8) & 0xFF) as u8; - msg[len_off + 1] = (con_len & 0xFF) as u8; - self.length = con_len as u16; - - Ok(()) - } - - // set_edns0 configures h for EDNS(0). - // - // The provided ext_rcode must be an extended RCode. - pub fn set_edns0( - &mut self, - udp_payload_len: u16, - ext_rcode: u32, - dnssec_ok: bool, - ) -> Result<()> { - self.name = Name { - data: ".".to_owned(), - }; // RFC 6891 section 6.1.2 - self.typ = DnsType::Opt; - self.class = DnsClass(udp_payload_len); - self.ttl = (ext_rcode >> 4) << 24; - if dnssec_ok { - self.ttl |= EDNS0_DNSSEC_OK; - } - Ok(()) - } - - // dnssec_allowed reports whether the DNSSEC OK bit is set. - pub fn dnssec_allowed(&self) -> bool { - self.ttl & EDNS0_DNSSEC_OK_MASK == EDNS0_DNSSEC_OK // RFC 6891 section 6.1.3 - } - - // extended_rcode returns an extended RCode. - // - // The provided rcode must be the RCode in DNS message header. - pub fn extended_rcode(&self, rcode: RCode) -> RCode { - if self.ttl & EDNS_VERSION_MASK == EDNS0_VERSION { - // RFC 6891 section 6.1.3 - let ttl = ((self.ttl >> 24) << 4) as u8 | rcode as u8; - return RCode::from(ttl); - } - rcode - } -} - -// A ResourceBody is a DNS resource record minus the header. -pub trait ResourceBody: fmt::Display + fmt::Debug { - // real_type returns the actual type of the Resource. This is used to - // fill in the header Type field. - fn real_type(&self) -> DnsType; - - // pack packs a Resource except for its header. - fn pack( - &self, - msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result>; - - fn unpack(&mut self, msg: &[u8], off: usize, length: usize) -> Result; -} - -pub fn unpack_resource_body( - typ: DnsType, - msg: &[u8], - mut off: usize, - length: usize, -) -> Result<(Box, usize)> { - let mut rb: Box = match typ { - DnsType::A => Box::::default(), - DnsType::Ns => Box::::default(), - DnsType::Cname => Box::::default(), - DnsType::Soa => Box::::default(), - DnsType::Ptr => Box::::default(), - DnsType::Mx => Box::::default(), - DnsType::Txt => Box::::default(), - DnsType::Aaaa => Box::::default(), - DnsType::Srv => Box::::default(), - DnsType::Opt => Box::::default(), - _ => return Err(Error::ErrNilResourceBody), - }; - - off = rb.unpack(msg, off, length)?; - - Ok((rb, off)) -} diff --git a/mdns/src/message/resource/mx.rs b/mdns/src/message/resource/mx.rs deleted file mode 100644 index b9bcfdfff..000000000 --- a/mdns/src/message/resource/mx.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::*; -use crate::error::Result; -use crate::message::name::*; -use crate::message::packer::*; - -// An MXResource is an mx Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct MxResource { - pub pref: u16, - pub mx: Name, -} - -impl fmt::Display for MxResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.MXResource{{pref: {}, mx: {}}}", - self.pref, self.mx - ) - } -} - -impl ResourceBody for MxResource { - fn real_type(&self) -> DnsType { - DnsType::Mx - } - - // pack appends the wire format of the MXResource to msg. - fn pack( - &self, - mut msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - msg = pack_uint16(msg, self.pref); - msg = self.mx.pack(msg, compression, compression_off)?; - Ok(msg) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - let (pref, off) = unpack_uint16(msg, off)?; - self.pref = pref; - self.mx.unpack(msg, off) - } -} diff --git a/mdns/src/message/resource/ns.rs b/mdns/src/message/resource/ns.rs deleted file mode 100644 index bf2819429..000000000 --- a/mdns/src/message/resource/ns.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; -use crate::error::Result; -use crate::message::name::*; - -// An NSResource is an NS Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct NsResource { - pub ns: Name, -} - -impl fmt::Display for NsResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dnsmessage.NSResource{{NS: {}}}", self.ns) - } -} - -impl ResourceBody for NsResource { - fn real_type(&self) -> DnsType { - DnsType::Ns - } - - // pack appends the wire format of the NSResource to msg. - fn pack( - &self, - msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - self.ns.pack(msg, compression, compression_off) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _txt_length: usize) -> Result { - self.ns.unpack(msg, off) - } -} diff --git a/mdns/src/message/resource/opt.rs b/mdns/src/message/resource/opt.rs deleted file mode 100644 index 70d0f9e1f..000000000 --- a/mdns/src/message/resource/opt.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::*; -use crate::error::{Result, *}; -use crate::message::packer::*; - -// An OPTResource is an OPT pseudo Resource record. -// -// The pseudo resource record is part of the extension mechanisms for DNS -// as defined in RFC 6891. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct OptResource { - pub options: Vec, -} - -// An Option represents a DNS message option within OPTResource. -// -// The message option is part of the extension mechanisms for DNS as -// defined in RFC 6891. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct DnsOption { - pub code: u16, // option code - pub data: Vec, -} - -impl fmt::Display for DnsOption { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.Option{{Code: {}, Data: {:?}}}", - self.code, self.data - ) - } -} - -impl fmt::Display for OptResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s: Vec = self.options.iter().map(|o| o.to_string()).collect(); - write!(f, "dnsmessage.OPTResource{{options: {}}}", s.join(",")) - } -} - -impl ResourceBody for OptResource { - fn real_type(&self) -> DnsType { - DnsType::Opt - } - - fn pack( - &self, - mut msg: Vec, - _compression: &mut Option>, - _compression_off: usize, - ) -> Result> { - for opt in &self.options { - msg = pack_uint16(msg, opt.code); - msg = pack_uint16(msg, opt.data.len() as u16); - msg = pack_bytes(msg, &opt.data); - } - Ok(msg) - } - - fn unpack(&mut self, msg: &[u8], mut off: usize, length: usize) -> Result { - let mut opts = vec![]; - let old_off = off; - while off < old_off + length { - let (code, new_off) = unpack_uint16(msg, off)?; - off = new_off; - - let (l, new_off) = unpack_uint16(msg, off)?; - off = new_off; - - let mut opt = DnsOption { - code, - data: vec![0; l as usize], - }; - if off + l as usize > msg.len() { - return Err(Error::ErrCalcLen); - } - opt.data.copy_from_slice(&msg[off..off + l as usize]); - off += l as usize; - opts.push(opt); - } - self.options = opts; - Ok(off) - } -} diff --git a/mdns/src/message/resource/ptr.rs b/mdns/src/message/resource/ptr.rs deleted file mode 100644 index 24d427500..000000000 --- a/mdns/src/message/resource/ptr.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; -use crate::error::Result; -use crate::message::name::*; - -// A PTRResource is a PTR Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct PtrResource { - pub ptr: Name, -} - -impl fmt::Display for PtrResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dnsmessage.PTRResource{{PTR: {}}}", self.ptr) - } -} - -impl ResourceBody for PtrResource { - fn real_type(&self) -> DnsType { - DnsType::Ptr - } - - // pack appends the wire format of the PTRResource to msg. - fn pack( - &self, - msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - self.ptr.pack(msg, compression, compression_off) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - self.ptr.unpack(msg, off) - } -} diff --git a/mdns/src/message/resource/soa.rs b/mdns/src/message/resource/soa.rs deleted file mode 100644 index ea7d92873..000000000 --- a/mdns/src/message/resource/soa.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::*; -use crate::error::Result; -use crate::message::name::*; -use crate::message::packer::*; - -// An SOAResource is an SOA Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct SoaResource { - pub ns: Name, - pub mbox: Name, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - - // min_ttl the is the default TTL of Resources records which did not - // contain a TTL value and the TTL of negative responses. (RFC 2308 - // Section 4) - pub min_ttl: u32, -} - -impl fmt::Display for SoaResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.SOAResource{{ns: {}, mbox: {}, serial: {}, refresh: {}, retry: {}, expire: {}, min_ttl: {}}}", - self.ns, - self.mbox, - self.serial, - self.refresh, - self.retry, - self.expire, - self.min_ttl, - ) - } -} - -impl ResourceBody for SoaResource { - fn real_type(&self) -> DnsType { - DnsType::Soa - } - - // pack appends the wire format of the SOAResource to msg. - fn pack( - &self, - mut msg: Vec, - compression: &mut Option>, - compression_off: usize, - ) -> Result> { - msg = self.ns.pack(msg, compression, compression_off)?; - msg = self.mbox.pack(msg, compression, compression_off)?; - msg = pack_uint32(msg, self.serial); - msg = pack_uint32(msg, self.refresh); - msg = pack_uint32(msg, self.retry); - msg = pack_uint32(msg, self.expire); - Ok(pack_uint32(msg, self.min_ttl)) - } - - fn unpack(&mut self, msg: &[u8], mut off: usize, _length: usize) -> Result { - off = self.ns.unpack(msg, off)?; - off = self.mbox.unpack(msg, off)?; - - let (serial, off) = unpack_uint32(msg, off)?; - self.serial = serial; - - let (refresh, off) = unpack_uint32(msg, off)?; - self.refresh = refresh; - - let (retry, off) = unpack_uint32(msg, off)?; - self.retry = retry; - - let (expire, off) = unpack_uint32(msg, off)?; - self.expire = expire; - - let (min_ttl, off) = unpack_uint32(msg, off)?; - self.min_ttl = min_ttl; - - Ok(off) - } -} diff --git a/mdns/src/message/resource/srv.rs b/mdns/src/message/resource/srv.rs deleted file mode 100644 index 5299bb4fe..000000000 --- a/mdns/src/message/resource/srv.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::*; -use crate::error::Result; -use crate::message::name::*; -use crate::message::packer::*; - -// An SRVResource is an SRV Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct SrvResource { - pub priority: u16, - pub weight: u16, - pub port: u16, - pub target: Name, // Not compressed as per RFC 2782. -} - -impl fmt::Display for SrvResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "dnsmessage.SRVResource{{priority: {}, weight: {}, port: {}, target: {}}}", - self.priority, self.weight, self.port, self.target - ) - } -} - -impl ResourceBody for SrvResource { - fn real_type(&self) -> DnsType { - DnsType::Srv - } - - // pack appends the wire format of the SRVResource to msg. - fn pack( - &self, - mut msg: Vec, - _compression: &mut Option>, - compression_off: usize, - ) -> Result> { - msg = pack_uint16(msg, self.priority); - msg = pack_uint16(msg, self.weight); - msg = pack_uint16(msg, self.port); - msg = self.target.pack(msg, &mut None, compression_off)?; - Ok(msg) - } - - fn unpack(&mut self, msg: &[u8], off: usize, _length: usize) -> Result { - let (priority, off) = unpack_uint16(msg, off)?; - self.priority = priority; - - let (weight, off) = unpack_uint16(msg, off)?; - self.weight = weight; - - let (port, off) = unpack_uint16(msg, off)?; - self.port = port; - - let off = self - .target - .unpack_compressed(msg, off, false /* allowCompression */)?; - - Ok(off) - } -} diff --git a/mdns/src/message/resource/txt.rs b/mdns/src/message/resource/txt.rs deleted file mode 100644 index 64888ff84..000000000 --- a/mdns/src/message/resource/txt.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::*; -use crate::error::*; -use crate::message::packer::*; - -// A TXTResource is a txt Resource record. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct TxtResource { - pub txt: Vec, -} - -impl fmt::Display for TxtResource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.txt.is_empty() { - write!(f, "dnsmessage.TXTResource{{txt: {{}}}}",) - } else { - write!(f, "dnsmessage.TXTResource{{txt: {{{}}}", self.txt.join(",")) - } - } -} - -impl ResourceBody for TxtResource { - fn real_type(&self) -> DnsType { - DnsType::Txt - } - - // pack appends the wire format of the TXTResource to msg. - fn pack( - &self, - mut msg: Vec, - _compression: &mut Option>, - _compression_off: usize, - ) -> Result> { - for s in &self.txt { - msg = pack_str(msg, s)? - } - Ok(msg) - } - - fn unpack(&mut self, msg: &[u8], mut off: usize, length: usize) -> Result { - let mut txts = vec![]; - let mut n = 0; - while n < length { - let (t, new_off) = unpack_str(msg, off)?; - off = new_off; - // Check if we got too many bytes. - if length < n + t.as_bytes().len() + 1 { - return Err(Error::ErrCalcLen); - } - n += t.len() + 1; - txts.push(t); - } - self.txt = txts; - - Ok(off) - } -} diff --git a/media/.gitignore b/media/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/media/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/media/CHANGELOG.md b/media/CHANGELOG.md deleted file mode 100644 index 0f56fba7f..000000000 --- a/media/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# webrtc-media changelog - -## Unreleased - -## v0.5.0 - -* Improve handling of padding packets in `SampleBuiler`. Prior to this `SampleBuilder` would sometimes, incorrectly, drop packets that carry media when they appeared adjacent to runs of padding packets. Contributed by [@k0nserv](https://github.com/k0nserv) in [#309](https://github.com/webrtc-rs/webrtc/pull/309) -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - -### Breaking - -* Introduced a new field in `Sample`, `prev_padding_packets`, that reflects the number of observed padding only packets while building the Sample. This can be use to differentiate inconsequential padding packets being dropped from those carrying media. Contributed by [@k0nserv](https://github.com/k0nserv) in [#303](https://github.com/webrtc-rs/webrtc/pull/303). - -## v0.4.7 - -* Bumped util dependency to `0.6.0`. -* Bumped rtp dependency to `0.6.0`. - - -## Prior to 0.4.7 - -Before 0.4.7 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/media/releases). diff --git a/media/Cargo.toml b/media/Cargo.toml deleted file mode 100644 index b89f3d5b9..000000000 --- a/media/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "webrtc-media" -version = "0.8.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of WebRTC Media API" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-media" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/media" - -[dependencies] -rtp = { version = "0.11.0", path = "../rtp" } - -byteorder = "1" -bytes = "1" -thiserror = "1" -rand = "0.8" - -[dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } -nearly_eq = "0.2" - -[[bench]] -name = "audio_buffer" -harness = false diff --git a/media/LICENSE-APACHE b/media/LICENSE-APACHE deleted file mode 100644 index b2e847a43..000000000 --- a/media/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/media/LICENSE-MIT b/media/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/media/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/media/README.md b/media/README.md deleted file mode 100644 index ca8d135ac..000000000 --- a/media/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of WebRTC Media. Rewrite Pion MediaDevices in Rust -

diff --git a/media/benches/audio_buffer.rs b/media/benches/audio_buffer.rs deleted file mode 100644 index a7164658d..000000000 --- a/media/benches/audio_buffer.rs +++ /dev/null @@ -1,36 +0,0 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use webrtc_media::audio::buffer::layout::{Deinterleaved, Interleaved}; -use webrtc_media::audio::buffer::Buffer; - -fn benchmark_from(c: &mut Criterion) { - type Sample = i32; - let channels = 4; - let frames = 100_000; - let deinterleaved_buffer: Buffer = { - let samples = (0..(channels * frames)).map(|i| i as i32).collect(); - Buffer::new(samples, channels) - }; - let interleaved_buffer: Buffer = { - let samples = (0..(channels * frames)).map(|i| i as i32).collect(); - Buffer::new(samples, channels) - }; - - c.bench_function("Buffer => Buffer", |b| { - b.iter(|| { - black_box(Buffer::::from( - deinterleaved_buffer.as_ref(), - )); - }) - }); - - c.bench_function("Buffer => Buffer", |b| { - b.iter(|| { - black_box(Buffer::::from( - interleaved_buffer.as_ref(), - )); - }) - }); -} - -criterion_group!(benches, benchmark_from); -criterion_main!(benches); diff --git a/media/codecov.yml b/media/codecov.yml deleted file mode 100644 index 788310caa..000000000 --- a/media/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 87b3c77b-91fc-48bd-8560-0c9dfdb774e8 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/media/doc/webrtc.rs.png b/media/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/media/src/audio/buffer.rs b/media/src/audio/buffer.rs deleted file mode 100644 index 07ddb804d..000000000 --- a/media/src/audio/buffer.rs +++ /dev/null @@ -1,443 +0,0 @@ -pub mod info; -pub mod layout; - -use std::mem::{ManuallyDrop, MaybeUninit}; -use std::ops::Range; - -use byteorder::ByteOrder; -pub use info::BufferInfo; -pub use layout::BufferLayout; -use layout::{Deinterleaved, Interleaved}; -use thiserror::Error; - -pub trait FromBytes: Sized { - type Error; - - fn from_bytes(bytes: &[u8], channels: usize) -> Result; -} - -pub trait ToByteBufferRef: Sized { - type Error; - - fn bytes_len(&self); - fn to_bytes( - &self, - bytes: &mut [u8], - channels: usize, - ) -> Result; -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum Error { - #[error("Unexpected end of buffer: (expected: {expected}, actual: {actual})")] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct BufferRef<'a, T, L> { - samples: &'a [T], - info: BufferInfo, -} - -impl<'a, T, L> BufferRef<'a, T, L> { - pub fn new(samples: &'a [T], channels: usize) -> Self { - debug_assert_eq!(samples.len() % channels, 0); - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Self { samples, info } - } -} - -/// Buffer multi-channel interlaced Audio. -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct Buffer { - samples: Vec, - info: BufferInfo, -} - -impl Buffer { - pub fn new(samples: Vec, channels: usize) -> Self { - debug_assert_eq!(samples.len() % channels, 0); - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Self { samples, info } - } - - pub fn as_ref(&'_ self) -> BufferRef<'_, T, L> { - BufferRef { - samples: &self.samples[..], - info: self.info, - } - } - - pub fn sub_range(&'_ self, range: Range) -> BufferRef<'_, T, L> { - let samples_len = range.len(); - let samples = &self.samples[range]; - let info = { - let channels = self.info.channels(); - assert_eq!(samples_len % channels, 0); - let frames = samples_len / channels; - BufferInfo::new(channels, frames) - }; - BufferRef { samples, info } - } -} - -impl From> for Buffer -where - T: Default + Copy, -{ - fn from(buffer: Buffer) -> Self { - Self::from(buffer.as_ref()) - } -} - -impl<'a, T> From> for Buffer -where - T: Default + Copy, -{ - fn from(buffer: BufferRef<'a, T, Deinterleaved>) -> Self { - // Writing into a vec of uninitialized `samples` is about 10% faster than - // cloning it or creating a default-initialized one and over-writing it. - // - // # Safety - // - // The performance boost comes with a cost though: - // At the end of the block each and every single item in - // `samples` needs to have been initialized, or else you get UB! - let samples = { - // Create a vec of uninitialized samples. - let mut samples: Vec> = - vec![MaybeUninit::uninit(); buffer.samples.len()]; - - // Initialize all of its values: - layout::interleaved_by( - buffer.samples, - &mut samples[..], - buffer.info.channels(), - |sample| MaybeUninit::new(*sample), - ); - - // Transmute the vec to the initialized type. - unsafe { std::mem::transmute::>, Vec>(samples) } - }; - - let info = buffer.info.into(); - Self { samples, info } - } -} - -impl From> for Buffer -where - T: Default + Copy, -{ - fn from(buffer: Buffer) -> Self { - Self::from(buffer.as_ref()) - } -} - -impl<'a, T> From> for Buffer -where - T: Default + Copy, -{ - fn from(buffer: BufferRef<'a, T, Interleaved>) -> Self { - // Writing into a vec of uninitialized `samples` is about 10% faster than - // cloning it or creating a default-initialized one and over-writing it. - // - // # Safety - // - // The performance boost comes with a cost though: - // At the end of the block each and every single item in - // `samples` needs to have been initialized, or else you get UB! - let samples = { - // Create a vec of uninitialized samples. - let mut samples: Vec> = - vec![MaybeUninit::uninit(); buffer.samples.len()]; - - // Initialize the vec's values: - layout::deinterleaved_by( - buffer.samples, - &mut samples[..], - buffer.info.channels(), - |sample| MaybeUninit::new(*sample), - ); - - // Everything is initialized. Transmute the vec to the initialized type. - unsafe { std::mem::transmute::>, Vec>(samples) } - }; - - let info = buffer.info.into(); - Self { samples, info } - } -} - -impl FromBytes for Buffer { - type Error = (); - - fn from_bytes(bytes: &[u8], channels: usize) -> Result { - const STRIDE: usize = std::mem::size_of::(); - assert_eq!(bytes.len() % STRIDE, 0); - - let chunks = { - let chunks_ptr = bytes.as_ptr() as *const [u8; STRIDE]; - let chunks_len = bytes.len() / STRIDE; - unsafe { std::slice::from_raw_parts(chunks_ptr, chunks_len) } - }; - - let samples: Vec<_> = chunks.iter().map(|chunk| B::read_i16(&chunk[..])).collect(); - - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Ok(Self { samples, info }) - } -} - -impl FromBytes for Buffer { - type Error = (); - - fn from_bytes(bytes: &[u8], channels: usize) -> Result { - const STRIDE: usize = std::mem::size_of::(); - assert_eq!(bytes.len() % STRIDE, 0); - - let chunks = { - let chunks_ptr = bytes.as_ptr() as *const [u8; STRIDE]; - let chunks_len = bytes.len() / STRIDE; - unsafe { std::slice::from_raw_parts(chunks_ptr, chunks_len) } - }; - - // Writing into a vec of uninitialized `samples` is about 10% faster than - // cloning it or creating a default-initialized one and over-writing it. - // - // # Safety - // - // The performance boost comes with a cost though: - // At the end of the block each and every single item in - // `samples` needs to have been initialized, or else you get UB! - let samples = unsafe { - init_vec(chunks.len(), |samples| { - layout::interleaved_by(chunks, samples, channels, |chunk| { - MaybeUninit::new(B::read_i16(&chunk[..])) - }); - }) - }; - - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Ok(Self { samples, info }) - } -} - -impl FromBytes for Buffer { - type Error = (); - - fn from_bytes(bytes: &[u8], channels: usize) -> Result { - const STRIDE: usize = std::mem::size_of::(); - assert_eq!(bytes.len() % STRIDE, 0); - - let chunks = { - let chunks_ptr = bytes.as_ptr() as *const [u8; STRIDE]; - let chunks_len = bytes.len() / STRIDE; - unsafe { std::slice::from_raw_parts(chunks_ptr, chunks_len) } - }; - - let samples: Vec<_> = chunks.iter().map(|chunk| B::read_i16(&chunk[..])).collect(); - - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Ok(Self { samples, info }) - } -} - -impl FromBytes for Buffer { - type Error = (); - - fn from_bytes(bytes: &[u8], channels: usize) -> Result { - const STRIDE: usize = std::mem::size_of::(); - assert_eq!(bytes.len() % STRIDE, 0); - - let chunks = { - let chunks_ptr = bytes.as_ptr() as *const [u8; STRIDE]; - let chunks_len = bytes.len() / STRIDE; - unsafe { std::slice::from_raw_parts(chunks_ptr, chunks_len) } - }; - - // Writing into a vec of uninitialized `samples` is about 10% faster than - // cloning it or creating a default-initialized one and over-writing it. - // - // # Safety - // - // The performance boost comes with a cost though: - // At the end of the block each and every single item in - // `samples` needs to have been initialized, or else you get UB! - let samples = unsafe { - init_vec(chunks.len(), |samples| { - layout::deinterleaved_by(chunks, samples, channels, |chunk| { - MaybeUninit::new(B::read_i16(&chunk[..])) - }); - }) - }; - - let info = { - let frames = samples.len() / channels; - BufferInfo::new(channels, frames) - }; - Ok(Self { samples, info }) - } -} - -/// Creates a vec with deferred initialization. -/// -/// # Safety -/// -/// The closure `f` MUST initialize every single item in the provided slice. -unsafe fn init_vec(len: usize, f: F) -> Vec -where - MaybeUninit: Clone, - F: FnOnce(&mut [MaybeUninit]), -{ - // Create a vec of uninitialized values. - let mut vec: Vec> = vec![MaybeUninit::uninit(); len]; - - // Initialize values: - f(&mut vec[..]); - - // Take owner-ship away from `vec`: - let mut manually_drop: ManuallyDrop<_> = ManuallyDrop::new(vec); - - // Create vec of proper type from `vec`'s raw parts. - let ptr = manually_drop.as_mut_ptr() as *mut T; - let len = manually_drop.len(); - let cap = manually_drop.capacity(); - Vec::from_raw_parts(ptr, len, cap) -} - -#[cfg(test)] -mod tests { - use byteorder::NativeEndian; - - use super::*; - - #[test] - fn deinterleaved_from_interleaved() { - let channels = 3; - - let input_samples: Vec = vec![0, 5, 10, 1, 6, 11, 2, 7, 12, 3, 8, 13, 4, 9, 14]; - let input: Buffer = Buffer::new(input_samples, channels); - - let output = Buffer::::from(input); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn interleaved_from_deinterleaved() { - let channels = 3; - - let input_samples: Vec = vec![0, 3, 6, 9, 12, 1, 4, 7, 10, 13, 2, 5, 8, 11, 14]; - let input: Buffer = Buffer::new(input_samples, channels); - - let output = Buffer::::from(input); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn deinterleaved_from_deinterleaved_bytes() { - let channels = 3; - let stride = 2; - - let input_samples: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - let input_bytes: &[u8] = { - let bytes_ptr = input_samples.as_ptr() as *const u8; - let bytes_len = input_samples.len() * stride; - unsafe { std::slice::from_raw_parts(bytes_ptr, bytes_len) } - }; - - let output: Buffer = - FromBytes::::from_bytes::(input_bytes, channels).unwrap(); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn deinterleaved_from_interleaved_bytes() { - let channels = 3; - let stride = 2; - - let input_samples: Vec = vec![0, 5, 10, 1, 6, 11, 2, 7, 12, 3, 8, 13, 4, 9, 14]; - let input_bytes: &[u8] = { - let bytes_ptr = input_samples.as_ptr() as *const u8; - let bytes_len = input_samples.len() * stride; - unsafe { std::slice::from_raw_parts(bytes_ptr, bytes_len) } - }; - - let output: Buffer = - FromBytes::::from_bytes::(input_bytes, channels).unwrap(); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn interleaved_from_interleaved_bytes() { - let channels = 3; - let stride = 2; - - let input_samples: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - let input_bytes: &[u8] = { - let bytes_ptr = input_samples.as_ptr() as *const u8; - let bytes_len = input_samples.len() * stride; - unsafe { std::slice::from_raw_parts(bytes_ptr, bytes_len) } - }; - - let output: Buffer = - FromBytes::::from_bytes::(input_bytes, channels).unwrap(); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn interleaved_from_deinterleaved_bytes() { - let channels = 3; - let stride = 2; - - let input_samples: Vec = vec![0, 3, 6, 9, 12, 1, 4, 7, 10, 13, 2, 5, 8, 11, 14]; - let input_bytes: &[u8] = { - let bytes_ptr = input_samples.as_ptr() as *const u8; - let bytes_len = input_samples.len() * stride; - unsafe { std::slice::from_raw_parts(bytes_ptr, bytes_len) } - }; - - let output: Buffer = - FromBytes::::from_bytes::(input_bytes, channels).unwrap(); - - let actual = output.samples; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } -} diff --git a/media/src/audio/buffer/info.rs b/media/src/audio/buffer/info.rs deleted file mode 100644 index bd70e12de..000000000 --- a/media/src/audio/buffer/info.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::marker::PhantomData; - -use crate::audio::buffer::layout::{Deinterleaved, Interleaved}; - -#[derive(Eq, PartialEq, Debug)] -pub struct BufferInfo { - channels: usize, - frames: usize, - _phantom: PhantomData, -} - -impl BufferInfo { - pub fn new(channels: usize, frames: usize) -> Self { - Self { - channels, - frames, - _phantom: PhantomData, - } - } - - /// Get a reference to the buffer info's channels. - pub fn channels(&self) -> usize { - self.channels - } - - /// Set the buffer info's channels. - pub fn set_channels(&mut self, channels: usize) { - self.channels = channels; - } - - /// Get a reference to the buffer info's frames. - pub fn frames(&self) -> usize { - self.frames - } - - /// Set the buffer info's frames. - pub fn set_frames(&mut self, frames: usize) { - self.frames = frames; - } - - pub fn samples(&self) -> usize { - self.channels * self.frames - } -} - -impl Copy for BufferInfo {} - -impl Clone for BufferInfo { - fn clone(&self) -> Self { - *self - } -} - -macro_rules! impl_from_buffer_info { - ($in_layout:ty => $out_layout:ty) => { - impl From> for BufferInfo<$out_layout> { - fn from(info: BufferInfo<$in_layout>) -> Self { - Self { - channels: info.channels, - frames: info.frames, - _phantom: PhantomData, - } - } - } - }; -} - -impl_from_buffer_info!(Interleaved => Deinterleaved); -impl_from_buffer_info!(Deinterleaved => Interleaved); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn new() { - let channels = 3; - let frames = 100; - - let interleaved = BufferInfo::::new(channels, frames); - - assert_eq!(interleaved.channels, channels); - assert_eq!(interleaved.frames, frames); - - let deinterleaved = BufferInfo::::new(channels, frames); - - assert_eq!(deinterleaved.channels, channels); - assert_eq!(deinterleaved.frames, frames); - } - - #[test] - fn clone() { - let channels = 3; - let frames = 100; - - let interleaved = BufferInfo::::new(channels, frames); - - assert_eq!(interleaved.clone(), interleaved); - - let deinterleaved = BufferInfo::::new(channels, frames); - - assert_eq!(deinterleaved.clone(), deinterleaved); - } - - #[test] - fn samples() { - let channels = 3; - let frames = 100; - - let interleaved = BufferInfo::::new(channels, frames); - - assert_eq!(interleaved.samples(), channels * frames); - - let deinterleaved = BufferInfo::::new(channels, frames); - - assert_eq!(deinterleaved.samples(), channels * frames); - } -} diff --git a/media/src/audio/buffer/layout.rs b/media/src/audio/buffer/layout.rs deleted file mode 100644 index d26f1fe78..000000000 --- a/media/src/audio/buffer/layout.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::audio::buffer::BufferInfo; -use crate::audio::sealed::Sealed; - -pub trait BufferLayout: Sized + Sealed { - fn index_of(info: &BufferInfo, channel: usize, frame: usize) -> usize; -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Deinterleaved {} - -impl Sealed for Deinterleaved {} - -impl BufferLayout for Deinterleaved { - #[inline] - fn index_of(info: &BufferInfo, channel: usize, frame: usize) -> usize { - (channel * info.frames()) + frame - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Interleaved {} - -impl Sealed for Interleaved {} - -impl BufferLayout for Interleaved { - #[inline] - fn index_of(info: &BufferInfo, channel: usize, frame: usize) -> usize { - (frame * info.channels()) + channel - } -} - -#[cfg(test)] -#[inline(always)] -pub(crate) fn deinterleaved(input: &[T], output: &mut [T], channels: usize) -where - T: Copy, -{ - deinterleaved_by(input, output, channels, |sample| *sample) -} - -/// De-interleaves an interleaved slice using a memory access pattern -/// that's optimized for efficient cached (i.e. sequential) reads. -pub(crate) fn deinterleaved_by(input: &[T], output: &mut [U], channels: usize, f: F) -where - F: Fn(&T) -> U, -{ - assert_eq!(input.len(), output.len()); - assert_eq!(input.len() % channels, 0); - - let frames = input.len() / channels; - let mut interleaved_index = 0; - for frame in 0..frames { - let mut deinterleaved_index = frame; - for _channel in 0..channels { - output[deinterleaved_index] = f(&input[interleaved_index]); - interleaved_index += 1; - deinterleaved_index += frames; - } - } -} - -#[cfg(test)] -#[inline(always)] -pub(crate) fn interleaved(input: &[T], output: &mut [T], channels: usize) -where - T: Copy, -{ - interleaved_by(input, output, channels, |sample| *sample) -} - -/// Interleaves an de-interleaved slice using a memory access pattern -/// that's optimized for efficient cached (i.e. sequential) reads. -pub(crate) fn interleaved_by(input: &[T], output: &mut [U], channels: usize, f: F) -where - F: Fn(&T) -> U, -{ - assert_eq!(input.len(), output.len()); - assert_eq!(input.len() % channels, 0); - - let frames = input.len() / channels; - let mut deinterleaved_index = 0; - for channel in 0..channels { - let mut interleaved_index = channel; - for _frame in 0..frames { - output[interleaved_index] = f(&input[deinterleaved_index]); - deinterleaved_index += 1; - interleaved_index += channels; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn interleaved_1_channel() { - let input: Vec<_> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - let mut output = vec![0; input.len()]; - let channels = 1; - - interleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - assert_eq!(actual, expected); - } - - #[test] - fn deinterleaved_1_channel() { - let input: Vec<_> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - let mut output = vec![0; input.len()]; - let channels = 1; - - deinterleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - assert_eq!(actual, expected); - } - - #[test] - fn interleaved_2_channel() { - let input: Vec<_> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - let mut output = vec![0; input.len()]; - let channels = 2; - - interleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]; - - assert_eq!(actual, expected); - } - - #[test] - fn deinterleaved_2_channel() { - let input: Vec<_> = vec![0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]; - let mut output = vec![0; input.len()]; - let channels = 2; - - deinterleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - assert_eq!(actual, expected); - } - - #[test] - fn interleaved_3_channel() { - let input: Vec<_> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - let mut output = vec![0; input.len()]; - let channels = 3; - - interleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 5, 10, 1, 6, 11, 2, 7, 12, 3, 8, 13, 4, 9, 14]; - - assert_eq!(actual, expected); - } - - #[test] - fn deinterleaved_3_channel() { - let input: Vec<_> = vec![0, 5, 10, 1, 6, 11, 2, 7, 12, 3, 8, 13, 4, 9, 14]; - let mut output = vec![0; input.len()]; - let channels = 3; - - deinterleaved(&input[..], &mut output[..], channels); - - let actual = output; - let expected = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; - - assert_eq!(actual, expected); - } -} diff --git a/media/src/audio/mod.rs b/media/src/audio/mod.rs deleted file mode 100644 index e259ae9c4..000000000 --- a/media/src/audio/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod buffer; -mod sample; - -pub use sample::Sample; - -mod sealed { - pub trait Sealed {} -} diff --git a/media/src/audio/sample.rs b/media/src/audio/sample.rs deleted file mode 100644 index a98910a1e..000000000 --- a/media/src/audio/sample.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::io::{Cursor, Read}; - -use byteorder::{ByteOrder, ReadBytesExt}; -#[cfg(test)] -use nearly_eq::NearlyEq; - -#[derive(Eq, PartialEq, Copy, Clone, Default, Debug)] -#[repr(transparent)] -pub struct Sample(Raw); - -impl From for Sample { - #[inline] - fn from(raw: i16) -> Self { - Self(raw) - } -} - -impl From for Sample { - #[inline] - fn from(raw: f32) -> Self { - Self(raw.clamp(-1.0, 1.0)) - } -} - -macro_rules! impl_from_sample_for_raw { - ($raw:ty) => { - impl From> for $raw { - #[inline] - fn from(sample: Sample<$raw>) -> $raw { - sample.0 - } - } - }; -} - -impl_from_sample_for_raw!(i16); -impl_from_sample_for_raw!(f32); - -// impl From> for Sample { -// #[inline] -// fn from(sample: Sample) -> Self { -// // Fast but imprecise approach: -// // Perform crude but fast upsample by bit-shifting the raw value: -// Self::from((sample.0 as i64) << 16) - -// // Slow but precise approach: -// // Perform a proper but expensive lerp from -// // i16::MIN..i16::MAX to i32::MIN..i32::MAX: - -// // let value = sample.0 as i64; - -// // let from = if value <= 0 { i16::MIN } else { i16::MAX } as i64; -// // let to = if value <= 0 { i32::MIN } else { i32::MAX } as i64; - -// // Self::from((value * to + from / 2) / from) -// } -// } - -impl From> for Sample { - #[inline] - fn from(sample: Sample) -> Self { - let divisor = if sample.0 < 0 { - i16::MIN as f32 - } else { - i16::MAX as f32 - } - .abs(); - Self::from((sample.0 as f32) / divisor) - } -} - -impl From> for Sample { - #[inline] - fn from(sample: Sample) -> Self { - let multiplier = if sample.0 < 0.0 { - i16::MIN as f32 - } else { - i16::MAX as f32 - } - .abs(); - Self::from((sample.0 * multiplier) as i16) - } -} - -trait FromBytes: Sized { - fn from_reader(reader: &mut R) -> Result; - - fn from_bytes(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - Self::from_reader::(&mut cursor) - } -} - -impl FromBytes for Sample { - fn from_reader(reader: &mut R) -> Result { - reader.read_i16::().map(Self::from) - } -} - -impl FromBytes for Sample { - fn from_reader(reader: &mut R) -> Result { - reader.read_f32::().map(Self::from) - } -} - -#[cfg(test)] -impl NearlyEq for Sample -where - Raw: NearlyEq, -{ - fn eps() -> Raw { - Raw::eps() - } - - fn eq(&self, other: &Self, eps: &Raw) -> bool { - NearlyEq::eq(&self.0, &other.0, eps) - } -} - -#[cfg(test)] -mod tests { - use nearly_eq::assert_nearly_eq; - - use super::*; - - #[test] - fn sample_i16_from_i16() { - // i16: - assert_eq!(Sample::::from(i16::MIN).0, i16::MIN); - assert_eq!(Sample::::from(i16::MIN / 2).0, i16::MIN / 2); - assert_eq!(Sample::::from(0).0, 0); - assert_eq!(Sample::::from(i16::MAX / 2).0, i16::MAX / 2); - assert_eq!(Sample::::from(i16::MAX).0, i16::MAX); - } - - #[test] - fn sample_f32_from_f32() { - assert_eq!(Sample::::from(-1.0).0, -1.0); - assert_eq!(Sample::::from(-0.5).0, -0.5); - assert_eq!(Sample::::from(0.0).0, 0.0); - assert_eq!(Sample::::from(0.5).0, 0.5); - assert_eq!(Sample::::from(1.0).0, 1.0); - - // For any values outside of -1.0..=1.0 we expect clamping: - assert_eq!(Sample::::from(f32::MIN).0, -1.0); - assert_eq!(Sample::::from(f32::MAX).0, 1.0); - } - - #[test] - fn sample_i16_from_sample_f32() { - assert_nearly_eq!( - Sample::::from(Sample::::from(-1.0)), - Sample::from(i16::MIN) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(-0.5)), - Sample::from(i16::MIN / 2) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(0.0)), - Sample::from(0) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(0.5)), - Sample::from(i16::MAX / 2) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(1.0)), - Sample::from(i16::MAX) - ); - } - - #[test] - fn sample_f32_from_sample_i16() { - assert_nearly_eq!( - Sample::::from(Sample::::from(i16::MIN)), - Sample::from(-1.0) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(i16::MIN / 2)), - Sample::from(-0.5) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(0)), - Sample::from(0.0) - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(i16::MAX / 2)), - Sample::from(0.5), - 0.0001 // rounding error due to i16::MAX being odd - ); - assert_nearly_eq!( - Sample::::from(Sample::::from(i16::MAX)), - Sample::from(1.0) - ); - } -} diff --git a/media/src/error.rs b/media/src/error.rs deleted file mode 100644 index 3c5272a4a..000000000 --- a/media/src/error.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::io; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("stream is nil")] - ErrNilStream, - #[error("incomplete frame header")] - ErrIncompleteFrameHeader, - #[error("incomplete frame data")] - ErrIncompleteFrameData, - #[error("incomplete file header")] - ErrIncompleteFileHeader, - #[error("IVF signature mismatch")] - ErrSignatureMismatch, - #[error("IVF version unknown, parser may not parse correctly")] - ErrUnknownIVFVersion, - - #[error("file not opened")] - ErrFileNotOpened, - #[error("invalid nil packet")] - ErrInvalidNilPacket, - - #[error("bad header signature")] - ErrBadIDPageSignature, - #[error("wrong header, expected beginning of stream")] - ErrBadIDPageType, - #[error("payload for id page must be 19 bytes")] - ErrBadIDPageLength, - #[error("bad payload signature")] - ErrBadIDPagePayloadSignature, - #[error("not enough data for payload header")] - ErrShortPageHeader, - #[error("expected and actual checksum do not match")] - ErrChecksumMismatch, - - #[error("data is not a H264 bitstream")] - ErrDataIsNotH264Stream, - #[error("Io EOF")] - ErrIoEOF, - - #[allow(non_camel_case_types)] - #[error("{0}")] - Io(#[source] IoError), - #[error("{0}")] - Rtp(#[from] rtp::Error), - - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} diff --git a/media/src/io/h264_reader/h264_reader_test.rs b/media/src/io/h264_reader/h264_reader_test.rs deleted file mode 100644 index 5bbb3ff7a..000000000 --- a/media/src/io/h264_reader/h264_reader_test.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::io::Cursor; - -use super::*; - -#[test] -fn test_data_does_not_start_with_h264header() -> Result<()> { - let test_function = |input: &[u8]| { - let mut reader = H264Reader::new(Cursor::new(input), 1_048_576); - if let Err(err) = reader.next_nal() { - assert_eq!(err, Error::ErrDataIsNotH264Stream); - } else { - panic!(); - } - }; - - test_function(&[2]); - test_function(&[0, 2]); - test_function(&[0, 0, 2]); - test_function(&[0, 0, 2, 0]); - test_function(&[0, 0, 0, 2]); - - Ok(()) -} - -#[test] -fn test_parse_header() -> Result<()> { - let h264bytes = &[0x0, 0x0, 0x1, 0xAB]; - let mut reader = H264Reader::new(Cursor::new(h264bytes), 1_048_576); - - let nal = reader.next_nal()?; - - assert_eq!(nal.data.len(), 1); - assert!(nal.forbidden_zero_bit); - assert_eq!(nal.picture_order_count, 0); - assert_eq!(nal.ref_idc, 1); - assert_eq!(NalUnitType::EndOfStream, nal.unit_type); - - Ok(()) -} - -#[test] -fn test_eof() -> Result<()> { - let test_function = |input: &[u8]| { - let mut reader = H264Reader::new(Cursor::new(input), 1_048_576); - if let Err(err) = reader.next_nal() { - assert_eq!(Error::ErrIoEOF, err); - } else { - panic!(); - } - }; - - test_function(&[0, 0, 0, 1]); - test_function(&[0, 0, 1]); - test_function(&[]); - - Ok(()) -} - -#[test] -fn test_skip_sei() -> Result<()> { - let h264bytes = &[ - 0x0, 0x0, 0x0, 0x1, 0xAA, 0x0, 0x0, 0x0, 0x1, 0x6, // SEI - 0x0, 0x0, 0x0, 0x1, 0xAB, - ]; - - let mut reader = H264Reader::new(Cursor::new(h264bytes), 1_048_576); - - let nal = reader.next_nal()?; - assert_eq!(nal.data[0], 0xAA); - - let nal = reader.next_nal()?; - assert_eq!(nal.data[0], 0xAB); - - Ok(()) -} - -#[test] -fn test_issue1734_next_nal() -> Result<()> { - let tests: Vec<&[u8]> = vec![ - &[0x00, 0x00, 0x010, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01], - &[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01], - ]; - - for test in tests { - let mut reader = H264Reader::new(Cursor::new(test), 1_048_576); - - // Just make sure it doesn't crash - while reader.next_nal().is_ok() { - //do nothing - } - } - - Ok(()) -} - -#[test] -fn test_trailing01after_start_code() -> Result<()> { - let test = vec![0x0, 0x0, 0x0, 0x1, 0x01, 0x0, 0x0, 0x0, 0x1, 0x01]; - let mut r = H264Reader::new(Cursor::new(test), 1_048_576); - - for _ in 0..=1 { - let _nal = r.next_nal()?; - } - - Ok(()) -} diff --git a/media/src/io/h264_reader/mod.rs b/media/src/io/h264_reader/mod.rs deleted file mode 100644 index 7b2b974b8..000000000 --- a/media/src/io/h264_reader/mod.rs +++ /dev/null @@ -1,334 +0,0 @@ -#[cfg(test)] -mod h264_reader_test; - -use std::fmt; -use std::io::Read; - -use bytes::{BufMut, BytesMut}; - -use crate::error::{Error, Result}; - -/// NalUnitType is the type of a NAL -/// Enums for NalUnitTypes -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum NalUnitType { - /// Unspecified - #[default] - Unspecified = 0, - /// Coded slice of a non-IDR picture - CodedSliceNonIdr = 1, - /// Coded slice data partition A - CodedSliceDataPartitionA = 2, - /// Coded slice data partition B - CodedSliceDataPartitionB = 3, - /// Coded slice data partition C - CodedSliceDataPartitionC = 4, - /// Coded slice of an IDR picture - CodedSliceIdr = 5, - /// Supplemental enhancement information (SEI) - SEI = 6, - /// Sequence parameter set - SPS = 7, - /// Picture parameter set - PPS = 8, - /// Access unit delimiter - AUD = 9, - /// End of sequence - EndOfSequence = 10, - /// End of stream - EndOfStream = 11, - /// Filler data - Filler = 12, - /// Sequence parameter set extension - SpsExt = 13, - /// Coded slice of an auxiliary coded picture without partitioning - CodedSliceAux = 19, - ///Reserved - Reserved, - // 14..18 // Reserved - // 20..23 // Reserved - // 24..31 // Unspecified -} - -impl fmt::Display for NalUnitType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - NalUnitType::Unspecified => "Unspecified", - NalUnitType::CodedSliceNonIdr => "CodedSliceNonIdr", - NalUnitType::CodedSliceDataPartitionA => "CodedSliceDataPartitionA", - NalUnitType::CodedSliceDataPartitionB => "CodedSliceDataPartitionB", - NalUnitType::CodedSliceDataPartitionC => "CodedSliceDataPartitionC", - NalUnitType::CodedSliceIdr => "CodedSliceIdr", - NalUnitType::SEI => "SEI", - NalUnitType::SPS => "SPS", - NalUnitType::PPS => "PPS", - NalUnitType::AUD => "AUD", - NalUnitType::EndOfSequence => "EndOfSequence", - NalUnitType::EndOfStream => "EndOfStream", - NalUnitType::Filler => "Filler", - NalUnitType::SpsExt => "SpsExt", - NalUnitType::CodedSliceAux => "NalUnitTypeCodedSliceAux", - _ => "Reserved", - }; - write!(f, "{}({})", s, *self as u8) - } -} - -impl From for NalUnitType { - fn from(v: u8) -> Self { - match v { - 0 => NalUnitType::Unspecified, - 1 => NalUnitType::CodedSliceNonIdr, - 2 => NalUnitType::CodedSliceDataPartitionA, - 3 => NalUnitType::CodedSliceDataPartitionB, - 4 => NalUnitType::CodedSliceDataPartitionC, - 5 => NalUnitType::CodedSliceIdr, - 6 => NalUnitType::SEI, - 7 => NalUnitType::SPS, - 8 => NalUnitType::PPS, - 9 => NalUnitType::AUD, - 10 => NalUnitType::EndOfSequence, - 11 => NalUnitType::EndOfStream, - 12 => NalUnitType::Filler, - 13 => NalUnitType::SpsExt, - 19 => NalUnitType::CodedSliceAux, - _ => NalUnitType::Reserved, - } - } -} - -/// NAL H.264 Network Abstraction Layer -pub struct NAL { - pub picture_order_count: u32, - - /// NAL header - pub forbidden_zero_bit: bool, - pub ref_idc: u8, - pub unit_type: NalUnitType, - - /// header byte + rbsp - pub data: BytesMut, -} - -impl NAL { - fn new(data: BytesMut) -> Self { - NAL { - picture_order_count: 0, - forbidden_zero_bit: false, - ref_idc: 0, - unit_type: NalUnitType::Unspecified, - data, - } - } - - fn parse_header(&mut self) { - let first_byte = self.data[0]; - self.forbidden_zero_bit = ((first_byte & 0x80) >> 7) == 1; // 0x80 = 0b10000000 - self.ref_idc = (first_byte & 0x60) >> 5; // 0x60 = 0b01100000 - self.unit_type = NalUnitType::from(first_byte & 0x1F); // 0x1F = 0b00011111 - } -} - -const NAL_PREFIX_3BYTES: [u8; 3] = [0, 0, 1]; -const NAL_PREFIX_4BYTES: [u8; 4] = [0, 0, 0, 1]; - -/// Wrapper class around reading buffer -struct ReadBuffer { - buffer: Box<[u8]>, - read_end: usize, - filled_end: usize, -} - -impl ReadBuffer { - fn new(capacity: usize) -> ReadBuffer { - Self { - buffer: vec![0u8; capacity].into_boxed_slice(), - read_end: 0, - filled_end: 0, - } - } - - #[inline] - fn in_buffer(&self) -> usize { - self.filled_end - self.read_end - } - - fn consume(&mut self, consume: usize) -> &[u8] { - debug_assert!(self.read_end + consume <= self.filled_end); - let result = &self.buffer[self.read_end..][..consume]; - self.read_end += consume; - result - } - - pub(crate) fn fill_buffer(&mut self, reader: &mut impl Read) -> Result<()> { - debug_assert_eq!(self.read_end, self.filled_end); - - self.read_end = 0; - self.filled_end = reader.read(&mut self.buffer)?; - - Ok(()) - } -} - -/// H264Reader reads data from stream and constructs h264 nal units -pub struct H264Reader { - reader: R, - // reading buffers - buffer: ReadBuffer, - // for reading - nal_prefix_parsed: bool, - count_of_consecutive_zero_bytes: usize, - nal_buffer: BytesMut, -} - -impl H264Reader { - /// new creates new `H264Reader` with `capacity` sized read buffer. - pub fn new(reader: R, capacity: usize) -> H264Reader { - H264Reader { - reader, - nal_prefix_parsed: false, - buffer: ReadBuffer::new(capacity), - count_of_consecutive_zero_bytes: 0, - nal_buffer: BytesMut::new(), - } - } - - fn read4(&mut self) -> Result<([u8; 4], usize)> { - let mut result = [0u8; 4]; - let mut result_filled = 0; - loop { - let in_buffer = self.buffer.in_buffer(); - - if in_buffer + result_filled >= 4 { - let consume = 4 - result_filled; - result[result_filled..].copy_from_slice(self.buffer.consume(consume)); - return Ok((result, 4)); - } - - result[result_filled..][..in_buffer].copy_from_slice(self.buffer.consume(in_buffer)); - result_filled += in_buffer; - - self.buffer.fill_buffer(&mut self.reader)?; - - if self.buffer.in_buffer() == 0 { - return Ok((result, result_filled)); - } - } - } - - fn read1(&mut self) -> Result> { - if self.buffer.in_buffer() == 0 { - self.buffer.fill_buffer(&mut self.reader)?; - - if self.buffer.in_buffer() == 0 { - return Ok(None); - } - } - - Ok(Some(self.buffer.consume(1)[0])) - } - - fn bit_stream_starts_with_h264prefix(&mut self) -> Result { - let (prefix_buffer, n) = self.read4()?; - - if n == 0 { - return Err(Error::ErrIoEOF); - } - - if n < 3 { - return Err(Error::ErrDataIsNotH264Stream); - } - - let nal_prefix3bytes_found = NAL_PREFIX_3BYTES[..] == prefix_buffer[..3]; - if n == 3 { - if nal_prefix3bytes_found { - return Err(Error::ErrIoEOF); - } - return Err(Error::ErrDataIsNotH264Stream); - } - - // n == 4 - if nal_prefix3bytes_found { - self.nal_buffer.put_u8(prefix_buffer[3]); - return Ok(3); - } - - let nal_prefix4bytes_found = NAL_PREFIX_4BYTES[..] == prefix_buffer; - if nal_prefix4bytes_found { - Ok(4) - } else { - Err(Error::ErrDataIsNotH264Stream) - } - } - - /// next_nal reads from stream and returns then next NAL, - /// and an error if there is incomplete frame data. - /// Returns all nil values when no more NALs are available. - pub fn next_nal(&mut self) -> Result { - if !self.nal_prefix_parsed { - self.bit_stream_starts_with_h264prefix()?; - - self.nal_prefix_parsed = true; - } - - loop { - let Some(read_byte) = self.read1()? else { - break; - }; - - let nal_found = self.process_byte(read_byte); - if nal_found { - let nal_unit_type = NalUnitType::from(self.nal_buffer[0] & 0x1F); - if nal_unit_type == NalUnitType::SEI { - self.nal_buffer.clear(); - continue; - } else { - break; - } - } - - self.nal_buffer.put_u8(read_byte); - } - - if self.nal_buffer.is_empty() { - return Err(Error::ErrIoEOF); - } - - let mut nal = NAL::new(self.nal_buffer.split()); - nal.parse_header(); - - Ok(nal) - } - - fn process_byte(&mut self, read_byte: u8) -> bool { - let mut nal_found = false; - - match read_byte { - 0 => { - self.count_of_consecutive_zero_bytes += 1; - } - 1 => { - if self.count_of_consecutive_zero_bytes >= 2 { - let count_of_consecutive_zero_bytes_in_prefix = - if self.count_of_consecutive_zero_bytes > 2 { - 3 - } else { - 2 - }; - let nal_unit_length = - self.nal_buffer.len() - count_of_consecutive_zero_bytes_in_prefix; - if nal_unit_length > 0 { - let _ = self.nal_buffer.split_off(nal_unit_length); - nal_found = true; - } - } - self.count_of_consecutive_zero_bytes = 0; - } - _ => { - self.count_of_consecutive_zero_bytes = 0; - } - } - - nal_found - } -} diff --git a/media/src/io/h264_writer/h264_writer_test.rs b/media/src/io/h264_writer/h264_writer_test.rs deleted file mode 100644 index 45e4bbc0f..000000000 --- a/media/src/io/h264_writer/h264_writer_test.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::io::Cursor; - -use bytes::Bytes; - -use super::*; - -#[test] -fn test_is_key_frame() -> Result<()> { - let tests = vec![ - ( - "When given a non-keyframe; it should return false", - vec![0x27, 0x90, 0x90], - false, - ), - ( - "When given a SPS packetized with STAP-A;; it should return true", - vec![ - 0x38, 0x00, 0x03, 0x27, 0x90, 0x90, 0x00, 0x05, 0x28, 0x90, 0x90, 0x90, 0x90, - ], - true, - ), - ( - "When given a SPS with no packetization; it should return true", - vec![0x27, 0x90, 0x90, 0x00], - true, - ), - ]; - - for (name, payload, want) in tests { - let got = is_key_frame(&payload); - assert_eq!(got, want, "{name} failed"); - } - - Ok(()) -} - -#[test] -fn test_write_rtp() -> Result<()> { - let tests = vec![ - ( - "When given an empty payload; it should return nil", - vec![], - false, - vec![], - false, - ), - ( - "When no keyframe is defined; it should discard the packet", - vec![0x25, 0x90, 0x90], - false, - vec![], - false, - ), - ( - "When a valid Single NAL Unit packet is given; it should unpack it without error", - vec![0x27, 0x90, 0x90], - true, - vec![0x00, 0x00, 0x00, 0x01, 0x27, 0x90, 0x90], - false, - ), - ( - "When a valid STAP-A packet is given; it should unpack it without error", - vec![ - 0x38, 0x00, 0x03, 0x27, 0x90, 0x90, 0x00, 0x05, 0x28, 0x90, 0x90, 0x90, 0x90, - ], - true, - vec![ - 0x00, 0x00, 0x00, 0x01, 0x27, 0x90, 0x90, 0x00, 0x00, 0x00, 0x01, 0x28, 0x90, 0x90, - 0x90, 0x90, - ], - false, - ), - ]; - - for (_name, payload, has_key_frame, want_bytes, _reuse) in tests { - let mut writer = vec![]; - { - let w = Cursor::new(&mut writer); - let mut h264writer = H264Writer::new(w); - h264writer.has_key_frame = has_key_frame; - - let packet = rtp::packet::Packet { - payload: Bytes::from(payload), - ..Default::default() - }; - - h264writer.write_rtp(&packet)?; - h264writer.close()?; - } - - assert_eq!(writer, want_bytes); - } - - Ok(()) -} - -#[test] -fn test_write_rtp_fu() -> Result<()> { - let tests = vec![ - vec![0x3C, 0x85, 0x90, 0x90, 0x90], - vec![0x3C, 0x45, 0x90, 0x90, 0x90], - ]; - - let want_bytes = vec![ - 0x00, 0x00, 0x00, 0x01, 0x25, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, - ]; - - let mut writer = vec![]; - { - let w = Cursor::new(&mut writer); - let mut h264writer = H264Writer::new(w); - h264writer.has_key_frame = true; - - for payload in tests { - let packet = rtp::packet::Packet { - payload: Bytes::from(payload), - ..Default::default() - }; - - h264writer.write_rtp(&packet)?; - } - h264writer.close()?; - } - assert_eq!(writer, want_bytes); - - Ok(()) -} diff --git a/media/src/io/h264_writer/mod.rs b/media/src/io/h264_writer/mod.rs deleted file mode 100644 index a79db29a6..000000000 --- a/media/src/io/h264_writer/mod.rs +++ /dev/null @@ -1,83 +0,0 @@ -#[cfg(test)] -mod h264_writer_test; - -use std::io::{Seek, Write}; - -use rtp::codecs::h264::H264Packet; -use rtp::packetizer::Depacketizer; - -use crate::error::Result; -use crate::io::Writer; - -const NALU_TTYPE_STAP_A: u32 = 24; -const NALU_TTYPE_SPS: u32 = 7; -const NALU_TYPE_BITMASK: u32 = 0x1F; - -fn is_key_frame(data: &[u8]) -> bool { - if data.len() < 4 { - false - } else { - let word = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); - let nalu_type = (word >> 24) & NALU_TYPE_BITMASK; - (nalu_type == NALU_TTYPE_STAP_A && (word & NALU_TYPE_BITMASK) == NALU_TTYPE_SPS) - || (nalu_type == NALU_TTYPE_SPS) - } -} - -/// H264Writer is used to take RTP packets, parse them and -/// write the data to an io.Writer. -/// Currently it only supports non-interleaved mode -/// Therefore, only 1-23, 24 (STAP-A), 28 (FU-A) NAL types are allowed. -/// -pub struct H264Writer { - writer: W, - has_key_frame: bool, - cached_packet: Option, -} - -impl H264Writer { - // new initializes a new H264 writer with an io.Writer output - pub fn new(writer: W) -> Self { - H264Writer { - writer, - has_key_frame: false, - cached_packet: None, - } - } -} - -impl Writer for H264Writer { - /// write_rtp adds a new packet and writes the appropriate headers for it - fn write_rtp(&mut self, packet: &rtp::packet::Packet) -> Result<()> { - if packet.payload.is_empty() { - return Ok(()); - } - - if !self.has_key_frame { - self.has_key_frame = is_key_frame(&packet.payload); - if !self.has_key_frame { - // key frame not defined yet. discarding packet - return Ok(()); - } - } - - if self.cached_packet.is_none() { - self.cached_packet = Some(H264Packet::default()); - } - - if let Some(cached_packet) = &mut self.cached_packet { - let payload = cached_packet.depacketize(&packet.payload)?; - - self.writer.write_all(&payload)?; - } - - Ok(()) - } - - /// close closes the underlying writer - fn close(&mut self) -> Result<()> { - self.cached_packet = None; - self.writer.flush()?; - Ok(()) - } -} diff --git a/media/src/io/ivf_reader/ivf_reader_test.rs b/media/src/io/ivf_reader/ivf_reader_test.rs deleted file mode 100644 index 4cc2d6836..000000000 --- a/media/src/io/ivf_reader/ivf_reader_test.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::io::BufReader; - -use bytes::Bytes; - -use super::*; - -/// build_ivf_container takes frames and prepends valid IVF file header -fn build_ivf_container(frames: &[Bytes]) -> Bytes { - // Valid IVF file header taken from: https://github.com/webmproject/... - // vp8-test-vectors/blob/master/vp80-00-comprehensive-001.ivf - // Video Image Width - 176 - // Video Image Height - 144 - // Frame Rate Rate - 30000 - // Frame Rate Scale - 1000 - // Video Length in Frames - 29 - // BitRate: 64.01 kb/s - let header = Bytes::from_static(&[ - 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x38, 0x30, 0xb0, 0x00, 0x90, - 0x00, 0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]); - - let mut ivf = BytesMut::new(); - ivf.extend(header); - - for frame in frames { - ivf.extend(frame); - } - - ivf.freeze() -} - -#[test] -fn test_ivf_reader_parse_valid_file_header() -> Result<()> { - let ivf = build_ivf_container(&[]); - - let r = BufReader::new(&ivf[..]); - let (_, header) = IVFReader::new(r)?; - - assert_eq!(&header.signature, b"DKIF", "signature is 'DKIF'"); - assert_eq!(header.version, 0, "version should be 0"); - assert_eq!(&header.four_cc, b"VP80", "FourCC should be 'VP80'"); - assert_eq!(header.width, 176, "width should be 176"); - assert_eq!(header.height, 144, "height should be 144"); - assert_eq!( - header.timebase_denominator, 30000, - "timebase denominator should be 30000" - ); - assert_eq!( - header.timebase_numerator, 1000, - "timebase numerator should be 1000" - ); - assert_eq!(header.num_frames, 29, "number of frames should be 29"); - assert_eq!(header.unused, 0, "bytes should be unused"); - - Ok(()) -} - -#[test] -fn test_ivf_reader_parse_valid_frames() -> Result<()> { - // Frame Length - 4 - // Timestamp - None - // Frame Payload - 0xDEADBEEF - let valid_frame1 = Bytes::from_static(&[ - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, - 0xEF, - ]); - - // Frame Length - 12 - // Timestamp - None - // Frame Payload - 0xDEADBEEFDEADBEEF - let valid_frame2 = Bytes::from_static(&[ - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, - 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, - ]); - - let ivf = build_ivf_container(&[valid_frame1, valid_frame2]); - let r = BufReader::new(&ivf[..]); - let (mut reader, _) = IVFReader::new(r)?; - - // Parse Frame #1 - let (payload, header) = reader.parse_next_frame()?; - - assert_eq!(header.frame_size, 4, "Frame header frameSize should be 4"); - assert_eq!(payload.len(), 4, "Payload should be length 4"); - assert_eq!( - payload, - Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF,]), - "Payload value should be 0xDEADBEEF" - ); - assert_eq!( - reader.bytes_read, - IVF_FILE_HEADER_SIZE + IVF_FRAME_HEADER_SIZE + header.frame_size as usize - ); - let previous_bytes_read = reader.bytes_read; - - // Parse Frame #2 - let (payload, header) = reader.parse_next_frame()?; - - assert_eq!(header.frame_size, 12, "Frame header frameSize should be 4"); - assert_eq!(payload.len(), 12, "Payload should be length 12"); - assert_eq!( - payload, - Bytes::from_static(&[ - 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, - ]), - "Payload value should be 0xDEADBEEFDEADBEEF" - ); - assert_eq!( - reader.bytes_read, - previous_bytes_read + IVF_FRAME_HEADER_SIZE + header.frame_size as usize, - ); - - Ok(()) -} - -#[test] -fn test_ivf_reader_parse_incomplete_frame_header() -> Result<()> { - // frame with 11-byte header (missing 1 byte) - let incomplete_frame = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]); - - let ivf = build_ivf_container(&[incomplete_frame]); - let r = BufReader::new(&ivf[..]); - let (mut reader, _) = IVFReader::new(r)?; - - // Parse Frame #1 - let result = reader.parse_next_frame(); - assert!(result.is_err(), "Expected Error but got Ok"); - - Ok(()) -} - -#[test] -fn test_ivf_reader_parse_incomplete_frame_payload() -> Result<()> { - // frame with header defining frameSize of 4 - // but only 2 bytes available (missing 2 bytes) - let incomplete_frame = Bytes::from_static(&[ - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, - ]); - - let ivf = build_ivf_container(&[incomplete_frame]); - let r = BufReader::new(&ivf[..]); - let (mut reader, _) = IVFReader::new(r)?; - - // Parse Frame #1 - let result = reader.parse_next_frame(); - assert!(result.is_err(), "Expected Error but got Ok"); - - Ok(()) -} - -#[test] -fn test_ivf_reader_eof_when_no_frames_left() -> Result<()> { - let ivf = build_ivf_container(&[]); - let r = BufReader::new(&ivf[..]); - let (mut reader, _) = IVFReader::new(r)?; - - let result = reader.parse_next_frame(); - assert!(result.is_err(), "Expected Error but got Ok"); - - Ok(()) -} diff --git a/media/src/io/ivf_reader/mod.rs b/media/src/io/ivf_reader/mod.rs deleted file mode 100644 index 62f314917..000000000 --- a/media/src/io/ivf_reader/mod.rs +++ /dev/null @@ -1,127 +0,0 @@ -#[cfg(test)] -mod ivf_reader_test; - -use std::io::Read; - -use byteorder::{LittleEndian, ReadBytesExt}; -use bytes::BytesMut; - -use crate::error::{Error, Result}; -use crate::io::ResetFn; - -pub const IVF_FILE_HEADER_SIGNATURE: &[u8] = b"DKIF"; -pub const IVF_FILE_HEADER_SIZE: usize = 32; -pub const IVF_FRAME_HEADER_SIZE: usize = 12; - -/// IVFFileHeader 32-byte header for IVF files -/// -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct IVFFileHeader { - pub signature: [u8; 4], // 0-3 - pub version: u16, // 4-5 - pub header_size: u16, // 6-7 - pub four_cc: [u8; 4], // 8-11 - pub width: u16, // 12-13 - pub height: u16, // 14-15 - pub timebase_denominator: u32, // 16-19 - pub timebase_numerator: u32, // 20-23 - pub num_frames: u32, // 24-27 - pub unused: u32, // 28-31 -} - -/// IVFFrameHeader 12-byte header for IVF frames -/// -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct IVFFrameHeader { - pub frame_size: u32, // 0-3 - pub timestamp: u64, // 4-11 -} - -/// IVFReader is used to read IVF files and return frame payloads -pub struct IVFReader { - reader: R, - bytes_read: usize, -} - -impl IVFReader { - /// new returns a new IVF reader and IVF file header - /// with an io.Reader input - pub fn new(reader: R) -> Result<(IVFReader, IVFFileHeader)> { - let mut r = IVFReader { - reader, - bytes_read: 0, - }; - - let header = r.parse_file_header()?; - - Ok((r, header)) - } - - /// reset_reader resets the internal stream of IVFReader. This is useful - /// for live streams, where the end of the file might be read without the - /// data being finished. - pub fn reset_reader(&mut self, mut reset: ResetFn) { - self.reader = reset(self.bytes_read); - } - - /// parse_next_frame reads from stream and returns IVF frame payload, header, - /// and an error if there is incomplete frame data. - /// Returns all nil values when no more frames are available. - pub fn parse_next_frame(&mut self) -> Result<(BytesMut, IVFFrameHeader)> { - let frame_size = self.reader.read_u32::()?; - let timestamp = self.reader.read_u64::()?; - let header = IVFFrameHeader { - frame_size, - timestamp, - }; - - let mut payload = BytesMut::with_capacity(header.frame_size as usize); - payload.resize(header.frame_size as usize, 0); - self.reader.read_exact(&mut payload)?; - - self.bytes_read += IVF_FRAME_HEADER_SIZE + header.frame_size as usize; - - Ok((payload, header)) - } - - /// parse_file_header reads 32 bytes from stream and returns - /// IVF file header. This is always called before parse_next_frame() - fn parse_file_header(&mut self) -> Result { - let mut signature = [0u8; 4]; - let mut four_cc = [0u8; 4]; - - self.reader.read_exact(&mut signature)?; - let version = self.reader.read_u16::()?; - let header_size = self.reader.read_u16::()?; - self.reader.read_exact(&mut four_cc)?; - let width = self.reader.read_u16::()?; - let height = self.reader.read_u16::()?; - let timebase_denominator = self.reader.read_u32::()?; - let timebase_numerator = self.reader.read_u32::()?; - let num_frames = self.reader.read_u32::()?; - let unused = self.reader.read_u32::()?; - - let header = IVFFileHeader { - signature, - version, - header_size, - four_cc, - width, - height, - timebase_denominator, - timebase_numerator, - num_frames, - unused, - }; - - if header.signature != IVF_FILE_HEADER_SIGNATURE { - return Err(Error::ErrSignatureMismatch); - } else if header.version != 0 { - return Err(Error::ErrUnknownIVFVersion); - } - - self.bytes_read += IVF_FILE_HEADER_SIZE; - - Ok(header) - } -} diff --git a/media/src/io/ivf_writer/ivf_writer_test.rs b/media/src/io/ivf_writer/ivf_writer_test.rs deleted file mode 100644 index 9a67199e6..000000000 --- a/media/src/io/ivf_writer/ivf_writer_test.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::io::Cursor; - -use super::*; -use crate::error::Error; - -#[test] -fn test_ivf_writer_add_packet_and_close() -> Result<()> { - // Construct valid packet - let raw_valid_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, - 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x89, 0x9e, - ]); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - //payloadOffset: 20, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_valid_pkt.slice(20..), - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - // Construct mid partition packet - let raw_mid_part_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, - 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x88, 0x36, 0xbe, 0x89, 0x9e, - ]); - - let mut mid_part_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - //PayloadOffset: 20, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: raw_mid_part_pkt.len() % 4 != 0, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_mid_part_pkt.slice(20..), - }; - mid_part_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - // Construct keyframe packet - let raw_keyframe_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, - 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - - let mut keyframe_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - //PayloadOffset: 20, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: raw_keyframe_pkt.len() % 4 != 0, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_keyframe_pkt.slice(20..), - }; - keyframe_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - // Check valid packet parameters - let mut vp8packet = rtp::codecs::vp8::Vp8Packet::default(); - let payload = vp8packet.depacketize(&valid_packet.payload)?; - assert_eq!(1, vp8packet.s, "Start packet S value should be 1"); - assert_eq!( - payload[0] & 0x01, - 1, - "Non Keyframe packet P value should be 1" - ); - - // Check mid partition packet parameters - let mut vp8packet = rtp::codecs::vp8::Vp8Packet::default(); - let payload = vp8packet.depacketize(&mid_part_packet.payload)?; - assert_eq!(vp8packet.s, 0, "Mid Partition packet S value should be 0"); - assert_eq!( - payload[0] & 0x01, - 1, - "Non Keyframe packet P value should be 1" - ); - - // Check keyframe packet parameters - let mut vp8packet = rtp::codecs::vp8::Vp8Packet::default(); - let payload = vp8packet.depacketize(&keyframe_packet.payload)?; - assert_eq!(vp8packet.s, 1, "Start packet S value should be 1"); - assert_eq!(payload[0] & 0x01, 0, "Keyframe packet P value should be 0"); - - let add_packet_test_case = vec![ - ( - "IVFWriter shouldn't be able to write something an empty packet", - "IVFWriter should be able to close the file", - rtp::packet::Packet::default(), - Some(Error::ErrInvalidNilPacket), - false, - 0, - ), - ( - "IVFWriter should be able to write an IVF packet", - "IVFWriter should be able to close the file", - valid_packet.clone(), - None, - false, - 1, - ), - ( - "IVFWriter should be able to write a Keframe IVF packet", - "IVFWriter should be able to close the file", - keyframe_packet, - None, - true, - 2, - ), - ]; - - let header = IVFFileHeader { - signature: *b"DKIF", // DKIF - version: 0, // version - header_size: 32, // Header size - four_cc: *b"VP80", // FOURCC - width: 640, // Width in pixels - height: 480, // Height in pixels - timebase_denominator: 30, // Framerate denominator - timebase_numerator: 1, // Framerate numerator - num_frames: 900, // Frame count, will be updated on first Close() call - unused: 0, // Unused - }; - - for (msg1, _msg2, packet, err, seen_key_frame, count) in add_packet_test_case { - let mut writer = IVFWriter::new(Cursor::new(Vec::::new()), &header)?; - assert!( - !writer.seen_key_frame, - "Writer's seenKeyFrame should initialize false" - ); - assert_eq!(writer.count, 0, "Writer's packet count should initialize 0"); - let result = writer.write_rtp(&packet); - if err.is_some() { - assert!(result.is_err(), "{}", msg1); - continue; - } else { - assert!(result.is_ok(), "{}", msg1); - } - - assert_eq!(seen_key_frame, writer.seen_key_frame, "{msg1} failed"); - if count == 1 { - assert_eq!(writer.count, 0); - } else if count == 2 { - assert_eq!(writer.count, 1); - } - - writer.write_rtp(&mid_part_packet)?; - if count == 1 { - assert_eq!(writer.count, 0); - } else if count == 2 { - assert_eq!(writer.count, 1); - - writer.write_rtp(&valid_packet)?; - assert_eq!(writer.count, 2); - } - - writer.close()?; - } - - Ok(()) -} diff --git a/media/src/io/ivf_writer/mod.rs b/media/src/io/ivf_writer/mod.rs deleted file mode 100644 index 3e2476e73..000000000 --- a/media/src/io/ivf_writer/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -#[cfg(test)] -mod ivf_writer_test; - -use std::io::{Seek, SeekFrom, Write}; - -use byteorder::{LittleEndian, WriteBytesExt}; -use bytes::{Bytes, BytesMut}; -use rtp::packetizer::Depacketizer; - -use crate::error::Result; -use crate::io::ivf_reader::IVFFileHeader; -use crate::io::Writer; - -/// IVFWriter is used to take RTP packets and write them to an IVF on disk -pub struct IVFWriter { - writer: W, - count: u64, - seen_key_frame: bool, - current_frame: Option, - is_vp9: bool, -} - -impl IVFWriter { - /// new initialize a new IVF writer with an io.Writer output - pub fn new(writer: W, header: &IVFFileHeader) -> Result { - let mut w = IVFWriter { - writer, - count: 0, - seen_key_frame: false, - current_frame: None, - is_vp9: &header.four_cc != b"VP80", - }; - - w.write_header(header)?; - - Ok(w) - } - - fn write_header(&mut self, header: &IVFFileHeader) -> Result<()> { - self.writer.write_all(&header.signature)?; // DKIF - self.writer.write_u16::(header.version)?; // version - self.writer.write_u16::(header.header_size)?; // Header size - self.writer.write_all(&header.four_cc)?; // FOURCC - self.writer.write_u16::(header.width)?; // Width in pixels - self.writer.write_u16::(header.height)?; // Height in pixels - self.writer - .write_u32::(header.timebase_denominator)?; // Framerate denominator - self.writer - .write_u32::(header.timebase_numerator)?; // Framerate numerator - self.writer.write_u32::(header.num_frames)?; // Frame count, will be updated on first Close() call - self.writer.write_u32::(header.unused)?; // Unused - - Ok(()) - } -} - -impl Writer for IVFWriter { - /// write_rtp adds a new packet and writes the appropriate headers for it - fn write_rtp(&mut self, packet: &rtp::packet::Packet) -> Result<()> { - let mut depacketizer: Box = if self.is_vp9 { - Box::::default() - } else { - Box::::default() - }; - - let payload = depacketizer.depacketize(&packet.payload)?; - - let is_key_frame = payload[0] & 0x01; - - if (!self.seen_key_frame && is_key_frame == 1) - || (self.current_frame.is_none() && !depacketizer.is_partition_head(&packet.payload)) - { - return Ok(()); - } - - self.seen_key_frame = true; - let frame_length = if let Some(current_frame) = &mut self.current_frame { - current_frame.extend(payload); - current_frame.len() - } else { - let mut current_frame = BytesMut::new(); - current_frame.extend(payload); - let frame_length = current_frame.len(); - self.current_frame = Some(current_frame); - frame_length - }; - - if !packet.header.marker { - return Ok(()); - } else if let Some(current_frame) = &self.current_frame { - if current_frame.is_empty() { - return Ok(()); - } - } else { - return Ok(()); - } - - self.writer.write_u32::(frame_length as u32)?; // Frame length - self.writer.write_u64::(self.count)?; // PTS - self.count += 1; - - let frame_content = if let Some(current_frame) = self.current_frame.take() { - current_frame.freeze() - } else { - Bytes::new() - }; - - self.writer.write_all(&frame_content)?; - - Ok(()) - } - - /// close stops the recording - fn close(&mut self) -> Result<()> { - // Update the frame count - self.writer.seek(SeekFrom::Start(24))?; - self.writer.write_u32::(self.count as u32)?; - - self.writer.flush()?; - Ok(()) - } -} diff --git a/media/src/io/mod.rs b/media/src/io/mod.rs deleted file mode 100644 index ade982656..000000000 --- a/media/src/io/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod h264_reader; -pub mod h264_writer; -use crate::error::Result; - -pub mod ivf_reader; -pub mod ivf_writer; -pub mod ogg_reader; -pub mod ogg_writer; -pub mod sample_builder; - -pub type ResetFn = Box R>; - -// Writer defines an interface to handle -// the creation of media files -pub trait Writer { - // Add the content of an RTP packet to the media - fn write_rtp(&mut self, pkt: &rtp::packet::Packet) -> Result<()>; - // close the media - // Note: close implementation must be idempotent - fn close(&mut self) -> Result<()>; -} diff --git a/media/src/io/ogg_reader/mod.rs b/media/src/io/ogg_reader/mod.rs deleted file mode 100644 index 4a1051622..000000000 --- a/media/src/io/ogg_reader/mod.rs +++ /dev/null @@ -1,204 +0,0 @@ -#[cfg(test)] -mod ogg_reader_test; - -use std::io::{Cursor, Read}; - -use byteorder::{LittleEndian, ReadBytesExt}; -use bytes::BytesMut; - -use crate::error::{Error, Result}; -use crate::io::ResetFn; - -pub const PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM: u8 = 0x00; -pub const PAGE_HEADER_TYPE_BEGINNING_OF_STREAM: u8 = 0x02; -pub const PAGE_HEADER_TYPE_END_OF_STREAM: u8 = 0x04; -pub const DEFAULT_PRE_SKIP: u16 = 3840; // 3840 recommended in the RFC -pub const PAGE_HEADER_SIGNATURE: &[u8] = b"OggS"; -pub const ID_PAGE_SIGNATURE: &[u8] = b"OpusHead"; -pub const COMMENT_PAGE_SIGNATURE: &[u8] = b"OpusTags"; -pub const PAGE_HEADER_SIZE: usize = 27; -pub const ID_PAGE_PAYLOAD_SIZE: usize = 19; - -/// OggReader is used to read Ogg files and return page payloads -pub struct OggReader { - reader: R, - bytes_read: usize, - checksum_table: [u32; 256], - do_checksum: bool, -} - -/// OggHeader is the metadata from the first two pages -/// in the file (ID and Comment) -/// -pub struct OggHeader { - pub channel_map: u8, - pub channels: u8, - pub output_gain: u16, - pub pre_skip: u16, - pub sample_rate: u32, - pub version: u8, -} - -/// OggPageHeader is the metadata for a Page -/// Pages are the fundamental unit of multiplexing in an Ogg stream -/// -pub struct OggPageHeader { - pub granule_position: u64, - - sig: [u8; 4], - version: u8, - header_type: u8, - serial: u32, - index: u32, - segments_count: u8, -} - -impl OggReader { - /// new returns a new Ogg reader and Ogg header - /// with an io.Reader input - pub fn new(reader: R, do_checksum: bool) -> Result<(OggReader, OggHeader)> { - let mut r = OggReader { - reader, - bytes_read: 0, - checksum_table: generate_checksum_table(), - do_checksum, - }; - - let header = r.read_headers()?; - - Ok((r, header)) - } - - fn read_headers(&mut self) -> Result { - let (payload, page_header) = self.parse_next_page()?; - - if page_header.sig != PAGE_HEADER_SIGNATURE { - return Err(Error::ErrBadIDPageSignature); - } - - if page_header.header_type != PAGE_HEADER_TYPE_BEGINNING_OF_STREAM { - return Err(Error::ErrBadIDPageType); - } - - if payload.len() != ID_PAGE_PAYLOAD_SIZE { - return Err(Error::ErrBadIDPageLength); - } - - let s = &payload[..8]; - if s != ID_PAGE_SIGNATURE { - return Err(Error::ErrBadIDPagePayloadSignature); - } - - let mut reader = Cursor::new(&payload[8..]); - let version = reader.read_u8()?; //8 - let channels = reader.read_u8()?; //9 - let pre_skip = reader.read_u16::()?; //10-11 - let sample_rate = reader.read_u32::()?; //12-15 - let output_gain = reader.read_u16::()?; //16-17 - let channel_map = reader.read_u8()?; //18 - - Ok(OggHeader { - channel_map, - channels, - output_gain, - pre_skip, - sample_rate, - version, - }) - } - - // parse_next_page reads from stream and returns Ogg page payload, header, - // and an error if there is incomplete page data. - pub fn parse_next_page(&mut self) -> Result<(BytesMut, OggPageHeader)> { - let mut h = [0u8; PAGE_HEADER_SIZE]; - self.reader.read_exact(&mut h)?; - - let mut head_reader = Cursor::new(h); - let mut sig = [0u8; 4]; //0-3 - head_reader.read_exact(&mut sig)?; - let version = head_reader.read_u8()?; //4 - let header_type = head_reader.read_u8()?; //5 - let granule_position = head_reader.read_u64::()?; //6-13 - let serial = head_reader.read_u32::()?; //14-17 - let index = head_reader.read_u32::()?; //18-21 - let checksum = head_reader.read_u32::()?; //22-25 - let segments_count = head_reader.read_u8()?; //26 - - let mut size_buffer = vec![0u8; segments_count as usize]; - self.reader.read_exact(&mut size_buffer)?; - - let mut payload_size = 0usize; - for s in &size_buffer { - payload_size += *s as usize; - } - - let mut payload = BytesMut::with_capacity(payload_size); - payload.resize(payload_size, 0); - self.reader.read_exact(&mut payload)?; - - if self.do_checksum { - let mut sum = 0; - - for (index, v) in h.iter().enumerate() { - // Don't include expected checksum in our generation - if index > 21 && index < 26 { - sum = self.update_checksum(0, sum); - continue; - } - sum = self.update_checksum(*v, sum); - } - - for v in &size_buffer { - sum = self.update_checksum(*v, sum); - } - for v in &payload[..] { - sum = self.update_checksum(*v, sum); - } - - if sum != checksum { - return Err(Error::ErrChecksumMismatch); - } - } - - let page_header = OggPageHeader { - granule_position, - sig, - version, - header_type, - serial, - index, - segments_count, - }; - - Ok((payload, page_header)) - } - - /// reset_reader resets the internal stream of OggReader. This is useful - /// for live streams, where the end of the file might be read without the - /// data being finished. - pub fn reset_reader(&mut self, mut reset: ResetFn) { - self.reader = reset(self.bytes_read); - } - - fn update_checksum(&self, v: u8, sum: u32) -> u32 { - (sum << 8) ^ self.checksum_table[(((sum >> 24) as u8) ^ v) as usize] - } -} - -pub(crate) fn generate_checksum_table() -> [u32; 256] { - let mut table = [0u32; 256]; - const POLY: u32 = 0x04c11db7; - - for (i, t) in table.iter_mut().enumerate() { - let mut r = (i as u32) << 24; - for _ in 0..8 { - if (r & 0x80000000) != 0 { - r = (r << 1) ^ POLY; - } else { - r <<= 1; - } - } - *t = r; - } - table -} diff --git a/media/src/io/ogg_reader/ogg_reader_test.rs b/media/src/io/ogg_reader/ogg_reader_test.rs deleted file mode 100644 index 311f35e8e..000000000 --- a/media/src/io/ogg_reader/ogg_reader_test.rs +++ /dev/null @@ -1,111 +0,0 @@ -use bytes::Bytes; - -use super::*; - -// generates a valid ogg file that can be used for tests -fn build_ogg_container() -> Vec { - vec![ - 0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, - 0x9b, 0x20, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x61, 0xee, 0x61, 0x17, 0x01, 0x13, 0x4f, 0x70, - 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x00, 0x0f, 0x80, 0xbb, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00, 0xda, 0x93, 0xc2, 0xd9, 0x00, 0x00, 0x00, - 0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x02, 0x00, 0x00, 0x00, 0x49, 0x97, 0x03, 0x37, 0x01, 0x05, - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ] -} - -#[test] -fn test_ogg_reader_parse_valid_header() -> Result<()> { - let ogg = build_ogg_container(); - let r = Cursor::new(&ogg); - let (_reader, header) = OggReader::new(r, true)?; - - assert_eq!(header.channel_map, 0); - assert_eq!(header.channels, 2); - assert_eq!(header.output_gain, 0); - assert_eq!(header.pre_skip, 0xf00); - assert_eq!(header.sample_rate, 48000); - assert_eq!(header.version, 1); - - Ok(()) -} - -#[test] -fn test_ogg_reader_parse_next_page() -> Result<()> { - let ogg = build_ogg_container(); - let r = Cursor::new(&ogg); - let (mut reader, _header) = OggReader::new(r, true)?; - - let (payload, _) = reader.parse_next_page()?; - assert_eq!(payload, Bytes::from_static(&[0x98, 0x36, 0xbe, 0x88, 0x9e])); - - let result = reader.parse_next_page(); - assert!(result.is_err()); - - Ok(()) -} - -#[test] -fn test_ogg_reader_parse_errors() -> Result<()> { - //"Invalid ID Page Header Signature" - { - let mut ogg = build_ogg_container(); - ogg[0] = 0; - - let result = OggReader::new(Cursor::new(ogg), false); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBadIDPageSignature); - } - } - - //"Invalid ID Page Header Type" - { - let mut ogg = build_ogg_container(); - ogg[5] = 0; - - let result = OggReader::new(Cursor::new(ogg), false); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBadIDPageType); - } - } - - //"Invalid ID Page Payload Length" - { - let mut ogg = build_ogg_container(); - ogg[27] = 0; - - let result = OggReader::new(Cursor::new(ogg), false); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBadIDPageLength); - } - } - - //"Invalid ID Page Payload Length" - { - let mut ogg = build_ogg_container(); - ogg[35] = 0; - - let result = OggReader::new(Cursor::new(ogg), false); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBadIDPagePayloadSignature); - } - } - - //"Invalid Page Checksum" - { - let mut ogg = build_ogg_container(); - ogg[22] = 0; - - let result = OggReader::new(Cursor::new(ogg), true); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrChecksumMismatch); - } - } - - Ok(()) -} diff --git a/media/src/io/ogg_writer/mod.rs b/media/src/io/ogg_writer/mod.rs deleted file mode 100644 index 18530528d..000000000 --- a/media/src/io/ogg_writer/mod.rs +++ /dev/null @@ -1,206 +0,0 @@ -#[cfg(test)] -mod ogg_writer_test; - -use std::io::{BufWriter, Seek, Write}; - -use byteorder::{LittleEndian, WriteBytesExt}; -use bytes::Bytes; -use rtp::packetizer::Depacketizer; - -use crate::error::Result; -use crate::io::ogg_reader::*; -use crate::io::Writer; - -/// OggWriter is used to take RTP packets and write them to an OGG on disk -pub struct OggWriter { - writer: W, - sample_rate: u32, - channel_count: u8, - serial: u32, - page_index: u32, - checksum_table: [u32; 256], - previous_granule_position: u64, - previous_timestamp: u32, - last_payload_size: usize, - last_payload: Bytes, -} - -impl OggWriter { - /// new initialize a new OGG Opus writer with an io.Writer output - pub fn new(writer: W, sample_rate: u32, channel_count: u8) -> Result { - let mut w = OggWriter { - writer, - sample_rate, - channel_count, - serial: rand::random::(), - page_index: 0, - checksum_table: generate_checksum_table(), - - // Timestamp and Granule MUST start from 1 - // Only headers can have 0 values - previous_timestamp: 1, - previous_granule_position: 1, - last_payload_size: 0, - last_payload: Bytes::new(), - }; - - w.write_headers()?; - - Ok(w) - } - - /* - ref: https://tools.ietf.org/html/rfc7845.html - https://git.xiph.org/?p=opus-tools.git;a=blob;f=src/opus_header.c#l219 - - Page 0 Pages 1 ... n Pages (n+1) ... - +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +-- - | | | | | | | | | | | | | - |+----------+| |+-----------------+| |+-------------------+ +----- - |||ID Header|| || Comment Header || ||Audio Data Packet 1| | ... - |+----------+| |+-----------------+| |+-------------------+ +----- - | | | | | | | | | | | | | - +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +-- - ^ ^ ^ - | | | - | | Mandatory Page Break - | | - | ID header is contained on a single page - | - 'Beginning Of Stream' - - Figure 1: Example Packet Organization for a Logical Ogg Opus Stream - */ - - fn write_headers(&mut self) -> Result<()> { - // ID Header - let mut ogg_id_header = Vec::with_capacity(19); - { - let mut header_writer = BufWriter::new(&mut ogg_id_header); - header_writer.write_all(ID_PAGE_SIGNATURE)?; // Magic Signature 'OpusHead' - header_writer.write_u8(1)?; // Version //8 - header_writer.write_u8(self.channel_count)?; // Channel count //9 - header_writer.write_u16::(DEFAULT_PRE_SKIP)?; // pre-skip //10-11 - header_writer.write_u32::(self.sample_rate)?; // original sample rate, any valid sample e.g 48000, //12-15 - header_writer.write_u16::(0)?; // output gain // 16-17 - header_writer.write_u8(0)?; // channel map 0 = one stream: mono or stereo, //18 - } - - // Reference: https://tools.ietf.org/html/rfc7845.html#page-6 - // RFC specifies that the ID Header page should have a granule position of 0 and a Header Type set to 2 (StartOfStream) - self.write_page( - &Bytes::from(ogg_id_header), - PAGE_HEADER_TYPE_BEGINNING_OF_STREAM, - 0, - self.page_index, - )?; - self.page_index += 1; - - // Comment Header - let mut ogg_comment_header = Vec::with_capacity(25); - { - let mut header_writer = BufWriter::new(&mut ogg_comment_header); - header_writer.write_all(COMMENT_PAGE_SIGNATURE)?; // Magic Signature 'OpusTags' //0-7 - header_writer.write_u32::(10)?; // Vendor Length //8-11 - header_writer.write_all(b"WebRTC.rs")?; // Vendor name 'WebRTC.rs' //12-20 - header_writer.write_u32::(0)?; // User Comment List Length //21-24 - } - - // RFC specifies that the page where the CommentHeader completes should have a granule position of 0 - self.write_page( - &Bytes::from(ogg_comment_header), - PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM, - 0, - self.page_index, - )?; - self.page_index += 1; - - Ok(()) - } - - fn write_page( - &mut self, - payload: &Bytes, - header_type: u8, - granule_pos: u64, - page_index: u32, - ) -> Result<()> { - self.last_payload_size = payload.len(); - self.last_payload = payload.clone(); - let n_segments = (self.last_payload_size + 255 - 1) / 255; - - let mut page = - Vec::with_capacity(PAGE_HEADER_SIZE + 1 + self.last_payload_size + n_segments); - { - let mut header_writer = BufWriter::new(&mut page); - header_writer.write_all(PAGE_HEADER_SIGNATURE)?; // page headers starts with 'OggS'//0-3 - header_writer.write_u8(0)?; // Version//4 - header_writer.write_u8(header_type)?; // 1 = continuation, 2 = beginning of stream, 4 = end of stream//5 - header_writer.write_u64::(granule_pos)?; // granule position //6-13 - header_writer.write_u32::(self.serial)?; // Bitstream serial number//14-17 - header_writer.write_u32::(page_index)?; // Page sequence number//18-21 - header_writer.write_u32::(0)?; //Checksum reserve //22-25 - header_writer.write_u8(n_segments as u8)?; // Number of segments in page //26 - - // Filling the segment table with the lacing values. - // First (n_segments - 1) values will always be 255. - for _ in 0..n_segments - 1 { - header_writer.write_u8(255)?; - } - // The last value will be the remainder. - header_writer.write_u8((self.last_payload_size - (n_segments * 255 - 255)) as u8)?; - - header_writer.write_all(payload)?; // inserting at 28th since Segment Table(1) + header length(27) - } - - let mut checksum = 0u32; - for v in &page { - checksum = - (checksum << 8) ^ self.checksum_table[(((checksum >> 24) as u8) ^ (*v)) as usize]; - } - page[22..26].copy_from_slice(&checksum.to_le_bytes()); // Checksum - generating for page data and inserting at 22th position into 32 bits - - self.writer.write_all(&page)?; - - Ok(()) - } -} - -impl Writer for OggWriter { - /// write_rtp adds a new packet and writes the appropriate headers for it - fn write_rtp(&mut self, packet: &rtp::packet::Packet) -> Result<()> { - let mut opus_packet = rtp::codecs::opus::OpusPacket; - let payload = opus_packet.depacketize(&packet.payload)?; - - // Should be equivalent to sample_rate * duration - if self.previous_timestamp != 1 { - let increment = packet.header.timestamp - self.previous_timestamp; - self.previous_granule_position += increment as u64; - } - self.previous_timestamp = packet.header.timestamp; - - self.write_page( - &payload, - PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM, - self.previous_granule_position, - self.page_index, - )?; - self.page_index += 1; - - Ok(()) - } - - /// close stops the recording - fn close(&mut self) -> Result<()> { - let payload = self.last_payload.clone(); - self.write_page( - &payload, - PAGE_HEADER_TYPE_END_OF_STREAM, - self.previous_granule_position, - self.page_index - 1, - )?; - - self.writer.flush()?; - Ok(()) - } -} diff --git a/media/src/io/ogg_writer/ogg_writer_test.rs b/media/src/io/ogg_writer/ogg_writer_test.rs deleted file mode 100644 index 1c0e43868..000000000 --- a/media/src/io/ogg_writer/ogg_writer_test.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::io::Cursor; - -use super::*; -use crate::error::Error; - -#[test] -fn test_ogg_writer_add_packet_and_close() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, - 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - //PayloadOffset: 20, - payload_type: 111, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_pkt.slice(20..), - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - // The linter misbehave and thinks this code is the same as the tests in ivf-writer_test - // nolint:dupl - let add_packet_test_case = vec![ - ( - "OggWriter shouldn't be able to write an empty packet", - "OggWriter should be able to close the file", - rtp::packet::Packet::default(), - Some(Error::ErrInvalidNilPacket), - ), - ( - "OggWriter should be able to write an Opus packet", - "OggWriter should be able to close the file", - valid_packet, - None, - ), - ]; - - for (msg1, _msg2, packet, err) in add_packet_test_case { - let mut writer = OggWriter::new(Cursor::new(Vec::::new()), 4800, 2)?; - let result = writer.write_rtp(&packet); - if err.is_some() { - assert!(result.is_err(), "{}", msg1); - continue; - } else { - assert!(result.is_ok(), "{}", msg1); - } - writer.close()?; - } - - Ok(()) -} - -#[test] -fn test_ogg_writer_add_packet() -> Result<()> { - let raw_pkt = Bytes::from_iter(std::iter::repeat(0x45).take(235)); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - payload_type: 111, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_pkt, - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - let buffer = Cursor::new(Vec::::new()); - let mut writer = OggWriter::new(buffer, 48000, 2)?; - let result = writer.write_rtp(&valid_packet); - - assert!( - result.is_ok(), - "OggWriter should be able to write an Opus packet smaller than 255 bytes" - ); - assert!( - writer.writer.into_inner()[126..128] == [1, 235], - "OggWriter should be able to write an Opus packet smaller than 255 bytes" - ); - - Ok(()) -} - -#[test] -fn test_ogg_writer_add_packet_of_255() -> Result<()> { - let raw_pkt = Bytes::from_iter(std::iter::repeat(0x45).take(255)); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - payload_type: 111, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_pkt, - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - let buffer = Cursor::new(Vec::::new()); - let mut writer = OggWriter::new(buffer, 48000, 2)?; - let result = writer.write_rtp(&valid_packet); - - assert!( - result.is_ok(), - "OggWriter should be able to write an Opus packet of exactly 255" - ); - assert!( - writer.writer.into_inner()[126..128] == [1, 255], - "OggWriter should be able to write an Opus packet of exactly 255" - ); - - Ok(()) -} - -#[test] -fn test_ogg_writer_add_large_packet() -> Result<()> { - let raw_pkt = Bytes::from_iter(std::iter::repeat(0x45).take(1000)); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - payload_type: 111, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_pkt, - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - let buffer = Cursor::new(Vec::::new()); - let mut writer = OggWriter::new(buffer, 48000, 2)?; - let result = writer.write_rtp(&valid_packet); - - assert!( - result.is_ok(), - "OggWriter should be able to write a large (> 255 bytes) Opus packet" - ); - assert!( - writer.writer.into_inner()[126..131] == [4, 255, 255, 255, 235], - "OggWriter should be able to write multiple segments per page, for 1000 bytes, 4 segments of 255, 255, 255 and 235 long" - ); - - Ok(()) -} - -#[test] -fn test_ogg_writer_add_large_packet_with_multiple_of_255() -> Result<()> { - let raw_pkt = Bytes::from_iter(std::iter::repeat(0x45).take(255 * 4)); - - let mut valid_packet = rtp::packet::Packet { - header: rtp::header::Header { - marker: true, - extension: true, - extension_profile: 1, - version: 2, - payload_type: 111, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - padding: false, - extensions: vec![], - extensions_padding: 0, - }, - payload: raw_pkt, - }; - valid_packet - .header - .set_extension(0, Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]))?; - - let buffer = Cursor::new(Vec::::new()); - let mut writer = OggWriter::new(buffer, 48000, 2)?; - let result = writer.write_rtp(&valid_packet); - - assert!( - result.is_ok(), - "OggWriter should be able to write a large (> 255 bytes) Opus packet" - ); - assert!( - writer.writer.into_inner()[126..131] == [4, 255, 255, 255, 255], - "OggWriter should be able to write multiple segments per page, for 1020 bytes, 4 segments of 255 each" - ); - - Ok(()) -} diff --git a/media/src/io/sample_builder/mod.rs b/media/src/io/sample_builder/mod.rs deleted file mode 100644 index 9bf5e9328..000000000 --- a/media/src/io/sample_builder/mod.rs +++ /dev/null @@ -1,433 +0,0 @@ -#[cfg(test)] -mod sample_builder_test; -#[cfg(test)] -mod sample_sequence_location_test; - -pub mod sample_sequence_location; - -use std::time::{Duration, SystemTime}; - -use bytes::Bytes; -use rtp::packet::Packet; -use rtp::packetizer::Depacketizer; - -use self::sample_sequence_location::{Comparison, SampleSequenceLocation}; -use crate::Sample; - -/// SampleBuilder buffers packets until media frames are complete. -pub struct SampleBuilder { - /// how many packets to wait until we get a valid Sample - max_late: u16, - /// max timestamp between old and new timestamps before dropping packets - max_late_timestamp: u32, - buffer: Vec>, - prepared_samples: Vec>, - last_sample_timestamp: Option, - - /// Interface that allows us to take RTP packets to samples - depacketizer: T, - - /// sample_rate allows us to compute duration of media.SamplecA - sample_rate: u32, - - /// filled contains the head/tail of the packets inserted into the buffer - filled: SampleSequenceLocation, - - /// active contains the active head/tail of the timestamp being actively processed - active: SampleSequenceLocation, - - /// prepared contains the samples that have been processed to date - prepared: SampleSequenceLocation, - - /// number of packets forced to be dropped - dropped_packets: u16, - - /// number of padding packets detected and dropped. This number will be a subset of - /// `dropped_packets` - padding_packets: u16, -} - -impl SampleBuilder { - /// Constructs a new SampleBuilder. - /// `max_late` is how long to wait until we can construct a completed [`Sample`]. - /// `max_late` is measured in RTP packet sequence numbers. - /// A large max_late will result in less packet loss but higher latency. - /// The depacketizer extracts media samples from RTP packets. - /// Several depacketizers are available in package [github.com/pion/rtp/codecs](https://github.com/webrtc-rs/rtp/tree/main/src/codecs). - pub fn new(max_late: u16, depacketizer: T, sample_rate: u32) -> Self { - Self { - max_late, - max_late_timestamp: 0, - buffer: vec![None; u16::MAX as usize + 1], - prepared_samples: (0..=u16::MAX as usize).map(|_| None).collect(), - last_sample_timestamp: None, - depacketizer, - sample_rate, - filled: SampleSequenceLocation::new(), - active: SampleSequenceLocation::new(), - prepared: SampleSequenceLocation::new(), - dropped_packets: 0, - padding_packets: 0, - } - } - - pub fn with_max_time_delay(mut self, max_late_duration: Duration) -> Self { - self.max_late_timestamp = - (self.sample_rate as u128 * max_late_duration.as_millis() / 1000) as u32; - self - } - - fn too_old(&self, location: &SampleSequenceLocation) -> bool { - if self.max_late_timestamp == 0 { - return false; - } - - let mut found_head: Option = None; - let mut found_tail: Option = None; - - let mut i = location.head; - while i != location.tail { - if let Some(ref packet) = self.buffer[i as usize] { - found_head = Some(packet.header.timestamp); - break; - } - i = i.wrapping_add(1); - } - - if found_head.is_none() { - return false; - } - - let mut i = location.tail.wrapping_sub(1); - while i != location.head { - if let Some(ref packet) = self.buffer[i as usize] { - found_tail = Some(packet.header.timestamp); - break; - } - i = i.wrapping_sub(1); - } - - if found_tail.is_none() { - return false; - } - - found_tail.unwrap() - found_head.unwrap() > self.max_late_timestamp - } - - /// Returns the timestamp associated with a given sample location - fn fetch_timestamp(&self, location: &SampleSequenceLocation) -> Option { - if location.empty() { - None - } else { - Some( - (self.buffer[location.head as usize]) - .as_ref()? - .header - .timestamp, - ) - } - } - - fn release_packet(&mut self, i: u16) { - self.buffer[i as usize] = None; - } - - /// Clears all buffers that have already been consumed by - /// popping. - fn purge_consumed_buffers(&mut self) { - let active = self.active; - self.purge_consumed_location(&active, false); - } - - /// Clears all buffers that have already been consumed - /// during a sample building method. - fn purge_consumed_location(&mut self, consume: &SampleSequenceLocation, force_consume: bool) { - if !self.filled.has_data() { - return; - } - match consume.compare(self.filled.head) { - Comparison::Inside if force_consume => { - self.release_packet(self.filled.head); - self.filled.head = self.filled.head.wrapping_add(1); - } - Comparison::Before => { - self.release_packet(self.filled.head); - self.filled.head = self.filled.head.wrapping_add(1); - } - _ => {} - } - } - - /// Flushes all buffers that are already consumed or those buffers - /// that are too late to consume. - fn purge_buffers(&mut self) { - self.purge_consumed_buffers(); - - while (self.too_old(&self.filled) || (self.filled.count() > self.max_late)) - && self.filled.has_data() - { - if self.active.empty() { - // refill the active based on the filled packets - self.active = self.filled; - } - - if self.active.has_data() && (self.active.head == self.filled.head) { - // attempt to force the active packet to be consumed even though - // outstanding data may be pending arrival - let err = match self.build_sample(true) { - Ok(_) => continue, - Err(e) => e, - }; - - if !matches!(err, BuildError::InvalidPartition(_)) { - // In the InvalidPartition case `build_sample` will have already adjusted `dropped_packets`. - self.dropped_packets += 1; - } - - // could not build the sample so drop it - self.active.head = self.active.head.wrapping_add(1); - } - - self.release_packet(self.filled.head); - self.filled.head = self.filled.head.wrapping_add(1); - } - } - - /// Adds an RTP Packet to self's buffer. - /// - /// Push does not copy the input. If you wish to reuse - /// this memory make sure to copy before calling push - pub fn push(&mut self, p: Packet) { - let sequence_number = p.header.sequence_number; - self.buffer[sequence_number as usize] = Some(p); - match self.filled.compare(sequence_number) { - Comparison::Void => { - self.filled.head = sequence_number; - self.filled.tail = sequence_number.wrapping_add(1); - } - Comparison::Before => { - self.filled.head = sequence_number; - } - Comparison::After => { - self.filled.tail = sequence_number.wrapping_add(1); - } - _ => {} - } - self.purge_buffers(); - } - - /// Creates a sample from a valid collection of RTP Packets by - /// walking forwards building a sample if everything looks good clear and - /// update buffer+values - fn build_sample( - &mut self, - purging_buffers: bool, - ) -> Result { - if self.active.empty() { - self.active = self.filled; - } - - if self.active.empty() { - return Err(BuildError::NoActiveSegment); - } - - if self.filled.compare(self.active.tail) == Comparison::Inside { - self.active.tail = self.filled.tail; - } - - let mut consume = SampleSequenceLocation::new(); - - let mut i = self.active.head; - // `self.active` isn't modified in the loop, fetch the timestamp once and cache it. - let head_timestamp = self.fetch_timestamp(&self.active); - while let Some(ref packet) = self.buffer[i as usize] { - if self.active.compare(i) == Comparison::After { - break; - } - let is_same_timestamp = head_timestamp.map(|t| packet.header.timestamp == t); - let is_different_timestamp = is_same_timestamp.map(std::ops::Not::not); - let is_partition_tail = self - .depacketizer - .is_partition_tail(packet.header.marker, &packet.payload); - - // If the timestamp is not the same it might be because the next packet is both a start - // and end of the next partition in which case a sample should be generated now. This - // can happen when padding packets are used .e.g: - // - // p1(t=1), p2(t=1), p3(t=1), p4(t=2, marker=true, start=true) - // - // In thic case the generated sample should be p1 through p3, but excluding p4 which is - // its own sample. - if is_partition_tail && is_same_timestamp.unwrap_or(true) { - consume.head = self.active.head; - consume.tail = i.wrapping_add(1); - break; - } - - if is_different_timestamp.unwrap_or(false) { - consume.head = self.active.head; - consume.tail = i; - break; - } - i = i.wrapping_add(1); - } - - if consume.empty() { - return Err(BuildError::NothingToConsume); - } - - if !purging_buffers && self.buffer[consume.tail as usize].is_none() { - // wait for the next packet after this set of packets to arrive - // to ensure at least one post sample timestamp is known - // (unless we have to release right now) - return Err(BuildError::PendingTimestampPacket); - } - - let sample_timestamp = self.fetch_timestamp(&self.active).unwrap_or(0); - let mut after_timestamp = sample_timestamp; - - // scan for any packet after the current and use that time stamp as the diff point - for i in consume.tail..self.active.tail { - if let Some(ref packet) = self.buffer[i as usize] { - after_timestamp = packet.header.timestamp; - break; - } - } - - // prior to decoding all the packets, check if this packet - // would end being disposed anyway - let head_payload = self.buffer[consume.head as usize] - .as_ref() - .map(|p| &p.payload) - .ok_or(BuildError::GapInSegment)?; - if !self.depacketizer.is_partition_head(head_payload) { - // libWebRTC will sometimes send several empty padding packets to smooth out send - // rate. These packets don't carry any media payloads. - let is_padding = consume.range(&self.buffer).all(|p| { - p.map(|p| { - self.last_sample_timestamp == Some(p.header.timestamp) && p.payload.is_empty() - }) - .unwrap_or(false) - }); - - self.dropped_packets += consume.count(); - if is_padding { - self.padding_packets += consume.count(); - } - self.purge_consumed_location(&consume, true); - self.purge_consumed_buffers(); - - self.active.head = consume.tail; - return Err(BuildError::InvalidPartition(consume)); - } - - // the head set of packets is now fully consumed - self.active.head = consume.tail; - - // merge all the buffers into a sample - let mut data: Vec = Vec::new(); - let mut i = consume.head; - while i != consume.tail { - let payload = self.buffer[i as usize] - .as_ref() - .map(|p| &p.payload) - .ok_or(BuildError::GapInSegment)?; - - let p = self - .depacketizer - .depacketize(payload) - .map_err(|_| BuildError::DepacketizerFailed)?; - - data.extend_from_slice(&p); - i = i.wrapping_add(1); - } - let samples = after_timestamp - sample_timestamp; - - let sample = Sample { - data: Bytes::copy_from_slice(&data), - timestamp: SystemTime::now(), - duration: Duration::from_secs_f64((samples as f64) / (self.sample_rate as f64)), - packet_timestamp: sample_timestamp, - prev_dropped_packets: self.dropped_packets, - prev_padding_packets: self.padding_packets, - }; - - self.dropped_packets = 0; - self.padding_packets = 0; - self.last_sample_timestamp = Some(sample_timestamp); - - self.prepared_samples[self.prepared.tail as usize] = Some(sample); - self.prepared.tail = self.prepared.tail.wrapping_add(1); - - self.purge_consumed_location(&consume, true); - self.purge_consumed_buffers(); - - Ok(consume) - } - - /// Compiles pushed RTP packets into media samples and then - /// returns the next valid sample (or None if no sample is compiled). - pub fn pop(&mut self) -> Option { - let _ = self.build_sample(false); - - if self.prepared.empty() { - return None; - } - let result = self.prepared_samples[self.prepared.head as usize].take(); - self.prepared.head = self.prepared.head.wrapping_add(1); - result - } - - /// Compiles pushed RTP packets into media samples and then - /// returns the next valid sample with its associated RTP timestamp (or `None` if - /// no sample is compiled). - pub fn pop_with_timestamp(&mut self) -> Option<(Sample, u32)> { - if let Some(sample) = self.pop() { - let timestamp = sample.packet_timestamp; - Some((sample, timestamp)) - } else { - None - } - } -} - -/// Computes the distance between two sequence numbers -/*pub(crate) fn seqnum_distance(head: u16, tail: u16) -> u16 { - if head > tail { - head.wrapping_add(tail) - } else { - tail - head - } -}*/ - -pub(crate) fn seqnum_distance(x: u16, y: u16) -> u16 { - let diff = x.wrapping_sub(y); - if diff > 0xFFFF / 2 { - 0xFFFF - diff + 1 - } else { - diff - } -} - -#[derive(Debug)] -enum BuildError { - /// There's no active segment of RTP packets to consider yet. - NoActiveSegment, - - /// No sample partition could be found in the active segment. - NothingToConsume, - - /// A segment to consume was identified, but a subsequent packet is needed to determine the - /// duration of the sample. - PendingTimestampPacket, - - /// The active segment's head was not aligned with a sample partition head. Some packets were - /// dropped. - InvalidPartition(SampleSequenceLocation), - - /// There was a gap in the active segment because of one or more missing RTP packets. - GapInSegment, - - /// We failed to depacketize an RTP packet. - DepacketizerFailed, -} diff --git a/media/src/io/sample_builder/sample_builder_test.rs b/media/src/io/sample_builder/sample_builder_test.rs deleted file mode 100644 index 097a82d6d..000000000 --- a/media/src/io/sample_builder/sample_builder_test.rs +++ /dev/null @@ -1,1499 +0,0 @@ -use rtp::header::Header; -use rtp::packet::Packet; -use rtp::packetizer::Depacketizer; - -use super::*; - -// Turns u8 integers into Bytes Array -macro_rules! bytes { - ($($item:expr),*) => ({ - static STATIC_SLICE: &'static [u8] = &[$($item), *]; - Bytes::from_static(STATIC_SLICE) - }); -} -#[derive(Default)] -pub struct SampleBuilderTest { - message: String, - packets: Vec, - with_head_checker: bool, - head_bytes: Vec, - samples: Vec, - max_late: u16, - max_late_timestamp: Duration, - extra_pop_attempts: usize, -} - -pub struct FakeDepacketizer { - head_checker: bool, - head_bytes: Vec, -} - -impl FakeDepacketizer { - fn new() -> Self { - Self { - head_checker: false, - head_bytes: vec![], - } - } -} - -impl Depacketizer for FakeDepacketizer { - fn depacketize(&mut self, b: &Bytes) -> std::result::Result { - Ok(b.clone()) - } - - /// Checks if the packet is at the beginning of a partition. This - /// should return false if the result could not be determined, in - /// which case the caller will detect timestamp discontinuities. - fn is_partition_head(&self, payload: &Bytes) -> bool { - if !self.head_checker { - // from .go: simulates a bug in 3.0 version, the tests should not assume the bug - return true; - } - - for b in &self.head_bytes { - if *payload == b { - return true; - } - } - false - } - - /// Checks if the packet is at the end of a partition. This should - /// return false if the result could not be determined. - fn is_partition_tail(&self, marker: bool, _payload: &Bytes) -> bool { - marker - } -} - -#[test] -pub fn test_sample_builder() { - #![allow(clippy::needless_update)] - let test_data: Vec = vec![ - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit anything if only one RTP packet has been pushed".into(), - packets: vec![Packet { - header: Header { - sequence_number: 5000, - timestamp: 5, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }], - samples: vec![], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit anything if only one RTP packet has been pushed even if the marker bit is set".into(), - packets: vec![Packet { - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }], - samples: vec![], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should emit two packets, we had three packets with unique timestamps".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 6, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1), - duration: Duration::from_secs(1), // technically this is the default value, but since it was in .go source.... - packet_timestamp: 5, - ..Default::default() - }, - Sample { - // Second sample - data: bytes!(2), - duration: Duration::from_secs(1), - packet_timestamp: 6, - ..Default::default() - }, - ], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should emit one packet, we had a packet end of sequence marker and run out of space".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5002, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5004, - timestamp: 9, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5006, - timestamp: 11, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Fifth packet - header: Header { - sequence_number: 5008, - timestamp: 13, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5010, - timestamp: 15, - ..Default::default() - }, - payload: bytes!(6), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5012, - timestamp: 17, - ..Default::default() - }, - payload: bytes!(7), - ..Default::default() - }, - ], - samples: vec![Sample { - // First sample - data: bytes!(1), - duration: Duration::from_secs(2), - packet_timestamp: 5, - ..Default::default() - }], - max_late: 5, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit any packet, we do not have a valid end of sequence and run out of space".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5002, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5004, - timestamp: 9, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5006, - timestamp: 11, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Fifth packet - header: Header { - sequence_number: 5008, - timestamp: 13, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5010, - timestamp: 15, - ..Default::default() - }, - payload: bytes!(6), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5012, - timestamp: 17, - ..Default::default() - }, - payload: bytes!(7), - ..Default::default() - }, - ], - samples: vec![], - max_late: 5, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should emit one packet, we had a packet end of sequence marker and run out of space".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5002, - timestamp: 7, - marker: true, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5004, - timestamp: 9, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5006, - timestamp: 11, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Fifth packet - header: Header { - sequence_number: 5008, - timestamp: 13, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5010, - timestamp: 15, - ..Default::default() - }, - payload: bytes!(6), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5012, - timestamp: 17, - ..Default::default() - }, - payload: bytes!(7), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First (dropped) sample - data: bytes!(1), - duration: Duration::from_secs(2), - packet_timestamp: 5, - ..Default::default() - }, - Sample { - // First correct sample - data: bytes!(2), - duration: Duration::from_secs(2), - packet_timestamp: 7, - prev_dropped_packets: 1, - ..Default::default() - }, - ], - max_late: 5, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should emit one packet, we had two packets but with duplicate timestamps".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 6, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 6, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5003, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1), - duration: Duration::from_secs(1), - packet_timestamp: 5, - ..Default::default() - }, - Sample { - // Second (duplicate) correct sample - data: bytes!(2, 3), - duration: Duration::from_secs(1), - packet_timestamp: 6, - ..Default::default() - }, - ], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit a packet because we have a gap before a valid one".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5007, - timestamp: 6, - marker: true, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5008, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - ], - samples: vec![], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit a packet after a gap as there are gaps and have not reached maxLate yet".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5007, - timestamp: 6, - marker: true, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5008, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - ], - with_head_checker: true, - head_bytes: vec![bytes!(2)], - samples: vec![], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder shouldn't emit a packet after a gap if PartitionHeadChecker doesn't assume it head".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 5, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5007, - timestamp: 6, - marker: true, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5008, - timestamp: 7, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - ], - with_head_checker: true, - head_bytes: vec![], - samples: vec![], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should emit multiple valid packets".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 2, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5003, - timestamp: 4, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Fifth packet - header: Header { - sequence_number: 5004, - timestamp: 5, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5005, - timestamp: 6, - ..Default::default() - }, - payload: bytes!(6), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1), - duration: Duration::from_secs(1), - packet_timestamp: 1, - ..Default::default() - }, - Sample { - // Second sample - data: bytes!(2), - duration: Duration::from_secs(1), - packet_timestamp: 2, - ..Default::default() - }, - Sample { - // Third sample - data: bytes!(3), - duration: Duration::from_secs(1), - packet_timestamp: 3, - ..Default::default() - }, - Sample { - // Fourth sample - data: bytes!(4), - duration: Duration::from_secs(1), - packet_timestamp: 4, - ..Default::default() - }, - Sample { - // Fifth sample - data: bytes!(5), - duration: Duration::from_secs(1), - packet_timestamp: 5, - ..Default::default() - }, - ], - max_late: 50, - max_late_timestamp: Duration::from_secs(0), - ..Default::default() - }, - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should skip timestamps too old".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 2, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Fourth packet - header: Header { - sequence_number: 5013, - timestamp: 4000, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Fifth packet - header: Header { - sequence_number: 5014, - timestamp: 4000, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5015, - timestamp: 4002, - ..Default::default() - }, - payload: bytes!(6), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5016, - timestamp: 7000, - ..Default::default() - }, - payload: bytes!(4), - ..Default::default() - }, - Packet { - // Eighth packet - header: Header { - sequence_number: 5017, - timestamp: 7001, - ..Default::default() - }, - payload: bytes!(5), - ..Default::default() - }, - ], - samples: vec![Sample { - // First sample - data: bytes!(4, 5), - duration: Duration::from_secs(2), - packet_timestamp: 4000, - prev_dropped_packets: 12, - ..Default::default() - }], - with_head_checker: true, - head_bytes: vec![bytes!(4)], - max_late: 50, - max_late_timestamp: Duration::from_secs(2000), - ..Default::default() - }, - // This test is based on observed RTP packet streams from Chrome. libWebRTC inserts padding - // packets to keep send rates steady, these are not important for sample building but we - // should identify them as padding packets to differentiate them from lost packets. - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should recognise padding packets".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 1, - marker: true, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Padding packet 1 - header: Header { - sequence_number: 5003, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::from_static(&[]), - ..Default::default() - }, - Packet { - // Padding packet 2 - header: Header { - sequence_number: 5004, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::from_static(&[]), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5005, - timestamp: 2, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5006, - timestamp: 2, - marker: true, - ..Default::default() - }, - payload: bytes!(7), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5007, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1, 2, 3), - duration: Duration::from_secs(0), - packet_timestamp: 1, - prev_dropped_packets: 0, - ..Default::default() - }, - Sample { - // Second sample - data: bytes!(1, 7), - duration: Duration::from_secs(1), - packet_timestamp: 2, - prev_dropped_packets: 2, - prev_padding_packets: 2, - ..Default::default() - }, - ], - with_head_checker: true, - head_bytes: vec![bytes!(1)], - max_late: 50, - max_late_timestamp: Duration::from_secs(2000), - extra_pop_attempts: 1, - ..Default::default() - }, - // This test is based on observed RTP packet streams when screen sharing in Chrome. - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should recognise padding packets when combined with max_late_timestamp".into(), - packets: vec![ - Packet { - // First packet - header: Header { - sequence_number: 5000, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Second packet - header: Header { - sequence_number: 5001, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - Packet { - // Third packet - header: Header { - sequence_number: 5002, - timestamp: 1, - marker: true, - ..Default::default() - }, - payload: bytes!(3), - ..Default::default() - }, - Packet { - // Padding packet 1 - header: Header { - sequence_number: 5003, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::from_static(&[]), - ..Default::default() - }, - Packet { - // Padding packet 2 - header: Header { - sequence_number: 5004, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::from_static(&[]), - ..Default::default() - }, - Packet { - // Sixth packet - header: Header { - sequence_number: 5005, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5006, - timestamp: 3, - marker: true, - ..Default::default() - }, - payload: bytes!(7), - ..Default::default() - }, - Packet { - // Seventh packet - header: Header { - sequence_number: 5007, - timestamp: 4, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1, 2, 3), - duration: Duration::from_secs(0), - packet_timestamp: 1, - prev_dropped_packets: 0, - ..Default::default() - }, - Sample { - // Second sample - data: bytes!(1, 7), - duration: Duration::from_secs(1), - packet_timestamp: 3, - prev_dropped_packets: 2, - prev_padding_packets: 2, - ..Default::default() - }, - ], - with_head_checker: true, - head_bytes: vec![bytes!(1)], - max_late: 50, - max_late_timestamp: Duration::from_millis(1050), - extra_pop_attempts: 1, - ..Default::default() - }, - // This test is based on observed RTP packet streams when screen sharing in Chrome. - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should build a sample out of a packet that's both start and end".into(), - packets: vec![ - Packet { - header: Header { - sequence_number: 5000, - timestamp: 1, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - Packet { - header: Header { - sequence_number: 5001, - timestamp: 2, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - ], - samples: vec![Sample { - // First sample - data: bytes!(1), - duration: Duration::from_secs(1), - packet_timestamp: 1, - prev_dropped_packets: 0, - ..Default::default() - }], - with_head_checker: true, - head_bytes: vec![bytes!(1)], - max_late: 50, - max_late_timestamp: Duration::from_millis(1050), - ..Default::default() - }, - // This test is based on observed RTP packet streams when screen sharing in Chrome. In - // particular the scenario used involved no movement on screen which causes Chrome to - // generate padding packets. - SampleBuilderTest { - #[rustfmt::skip] - message: "Sample builder should build a sample out of a packet that's both start and end following a run of padding packets".into(), - packets: vec![ - // First valid packet - Packet { - header: Header { - sequence_number: 5000, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - // Second valid packet - Packet { - header: Header { - sequence_number: 5001, - timestamp: 1, - marker: true, - ..Default::default() - }, - payload: bytes!(2), - ..Default::default() - }, - // Padding packet 1 - Packet { - header: Header { - sequence_number: 5002, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::default(), - ..Default::default() - }, - // Padding packet 2 - Packet { - header: Header { - sequence_number: 5003, - timestamp: 1, - ..Default::default() - }, - payload: Bytes::default(), - ..Default::default() - }, - // Third valid packet - Packet { - header: Header { - sequence_number: 5004, - timestamp: 2, - marker: true, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - // Fourth valid packet, start of next sample - Packet { - header: Header { - sequence_number: 5005, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(1), - ..Default::default() - }, - ], - samples: vec![ - Sample { - // First sample - data: bytes!(1, 2), - duration: Duration::from_secs(0), - packet_timestamp: 1, - prev_dropped_packets: 0, - ..Default::default() - }, - Sample { - // Second sample - data: bytes!(1), - duration: Duration::from_secs(1), - packet_timestamp: 2, - prev_dropped_packets: 2, - prev_padding_packets: 2, - ..Default::default() - }, - ], - with_head_checker: true, - head_bytes: vec![bytes!(1)], - extra_pop_attempts: 1, - max_late: 50, - ..Default::default() - }, - ]; - - for t in test_data { - let d = FakeDepacketizer { - head_checker: t.with_head_checker, - head_bytes: t.head_bytes, - }; - - let mut s = { - let sample_builder = SampleBuilder::new(t.max_late, d, 1); - if t.max_late_timestamp != Duration::from_secs(0) { - sample_builder.with_max_time_delay(t.max_late_timestamp) - } else { - sample_builder - } - }; - - let mut samples = Vec::::new(); - for p in t.packets { - s.push(p) - } - - while let Some(sample) = s.pop() { - samples.push(sample) - } - - for _ in 0..t.extra_pop_attempts { - // Pop some more - while let Some(sample) = s.pop() { - samples.push(sample) - } - } - - // Current problem: Sample does not implement Eq. Either implement myself or find another way of comparison. (Derive does not work) - assert_eq!(t.samples, samples, "{}", t.message); - } -} - -// SampleBuilder should respect maxLate if we popped successfully but then have a gap larger then maxLate -#[test] -fn test_sample_builder_max_late() { - let mut s = SampleBuilder::new(50, FakeDepacketizer::new(), 1); - - s.push(Packet { - header: Header { - sequence_number: 0, - timestamp: 1, - ..Default::default() - }, - payload: bytes!(0x01), - }); - s.push(Packet { - header: Header { - sequence_number: 1, - timestamp: 2, - ..Default::default() - }, - payload: bytes!(0x01), - }); - s.push(Packet { - header: Header { - sequence_number: 2, - timestamp: 3, - ..Default::default() - }, - payload: bytes!(0x01), - }); - assert_eq!( - s.pop(), - Some(Sample { - data: bytes!(0x01), - duration: Duration::from_secs(1), - packet_timestamp: 1, - ..Default::default() - }), - "Failed to build samples before gap" - ); - - s.push(Packet { - header: Header { - sequence_number: 5000, - timestamp: 500, - ..Default::default() - }, - payload: bytes!(0x02), - }); - s.push(Packet { - header: Header { - sequence_number: 5001, - timestamp: 501, - ..Default::default() - }, - payload: bytes!(0x02), - }); - s.push(Packet { - header: Header { - sequence_number: 5002, - timestamp: 502, - ..Default::default() - }, - payload: bytes!(0x02), - }); - - assert_eq!( - s.pop(), - Some(Sample { - data: bytes!(0x01), - duration: Duration::from_secs(1), - packet_timestamp: 2, - ..Default::default() - }), - "Failed to build samples after large gap" - ); - assert_eq!(None, s.pop(), "Failed to build samples after large gap"); - - s.push(Packet { - header: Header { - sequence_number: 6000, - timestamp: 600, - ..Default::default() - }, - payload: bytes!(0x03), - }); - assert_eq!( - s.pop(), - Some(Sample { - data: bytes!(0x02), - duration: Duration::from_secs(1), - packet_timestamp: 500, - prev_dropped_packets: 4998, - ..Default::default() - }), - "Failed to build samples after large gap" - ); - assert_eq!( - s.pop(), - Some(Sample { - data: bytes!(0x02), - duration: Duration::from_secs(1), - packet_timestamp: 501, - ..Default::default() - }), - "Failed to build samples after large gap" - ); -} - -#[test] -fn test_seqnum_distance() { - struct TestData { - x: u16, - y: u16, - d: u16, - } - let test_data = vec![ - TestData { - x: 0x0001, - y: 0x0003, - d: 0x0002, - }, - TestData { - x: 0x0003, - y: 0x0001, - d: 0x0002, - }, - TestData { - x: 0xFFF3, - y: 0xFFF1, - d: 0x0002, - }, - TestData { - x: 0xFFF1, - y: 0xFFF3, - d: 0x0002, - }, - TestData { - x: 0xFFFF, - y: 0x0001, - d: 0x0002, - }, - TestData { - x: 0x0001, - y: 0xFFFF, - d: 0x0002, - }, - ]; - - for data in test_data { - assert_eq!( - seqnum_distance(data.x, data.y), - data.d, - "seqnum_distance({}, {}) returned {} which must be {}", - data.x, - data.y, - seqnum_distance(data.x, data.y), - data.d - ); - } -} - -#[test] -fn test_sample_builder_clean_reference() { - for seq_start in [0_u16, 0xfff8, 0xfffe] { - let mut s = SampleBuilder::new(10, FakeDepacketizer::new(), 1); - s.push(Packet { - header: Header { - sequence_number: seq_start, - timestamp: 0, - ..Default::default() - }, - payload: bytes!(0x01), - }); - s.push(Packet { - header: Header { - sequence_number: seq_start.wrapping_add(1), - timestamp: 0, - ..Default::default() - }, - payload: bytes!(0x02), - }); - s.push(Packet { - header: Header { - sequence_number: seq_start.wrapping_add(2), - timestamp: 0, - ..Default::default() - }, - payload: bytes!(0x03), - }); - let pkt4 = Packet { - header: Header { - sequence_number: seq_start.wrapping_add(14), - timestamp: 120, - ..Default::default() - }, - payload: bytes!(0x04), - }; - s.push(pkt4.clone()); - let pkt5 = Packet { - header: Header { - sequence_number: seq_start.wrapping_add(12), - timestamp: 120, - ..Default::default() - }, - payload: bytes!(0x05), - }; - s.push(pkt5.clone()); - - for i in 0..3 { - assert_eq!( - s.buffer[seq_start.wrapping_add(i) as usize], - None, - "Old packet ({i}) is not unreferenced (seq_start: {seq_start}, max_late: 10, pushed: 12)" - ); - } - assert_eq!(s.buffer[seq_start.wrapping_add(14) as usize], Some(pkt4)); - assert_eq!(s.buffer[seq_start.wrapping_add(12) as usize], Some(pkt5)); - } -} - -#[test] -fn test_sample_builder_push_max_zero() { - let pkt = Packet { - header: Header { - sequence_number: 0, - timestamp: 0, - marker: true, - ..Default::default() - }, - payload: bytes!(0x01), - }; - let d = FakeDepacketizer { - head_checker: true, - head_bytes: vec![bytes!(0x01)], - }; - let mut s = SampleBuilder::new(0, d, 1); - s.push(pkt); - assert!(s.pop().is_some(), "Should expect a popped sample.") -} - -#[test] -fn test_pop_with_timestamp() { - let mut s = SampleBuilder::new(0, FakeDepacketizer::new(), 1); - assert_eq!(s.pop_with_timestamp(), None); -} - -#[test] -fn test_sample_builder_data() { - let mut s = SampleBuilder::new(10, FakeDepacketizer::new(), 1); - let mut j: usize = 0; - for i in 0..0x20000_usize { - let p = Packet { - header: Header { - sequence_number: i as u16, - timestamp: (i + 42) as u32, - ..Default::default() - }, - payload: Bytes::copy_from_slice(&[i as u8]), - }; - s.push(p); - while let Some((sample, ts)) = s.pop_with_timestamp() { - assert_eq!(ts, (j + 42) as u32, "timestamp"); - assert_eq!(sample.data.len(), 1, "data length"); - assert_eq!(sample.data[0], j as u8, "timestamp"); - j += 1; - } - } - // only the last packet should be dropped - assert_eq!(j, 0x1FFFF); -} diff --git a/media/src/io/sample_builder/sample_sequence_location.rs b/media/src/io/sample_builder/sample_sequence_location.rs deleted file mode 100644 index b4e10de71..000000000 --- a/media/src/io/sample_builder/sample_sequence_location.rs +++ /dev/null @@ -1,81 +0,0 @@ -use super::seqnum_distance; - -#[derive(Debug, PartialEq)] -pub(crate) enum Comparison { - Void, - Before, - Inside, - After, -} - -pub(crate) struct Iterator<'a, T> { - data: &'a [Option], - sample: SampleSequenceLocation, - i: u16, -} - -impl<'a, T> std::iter::Iterator for Iterator<'a, T> { - type Item = Option<&'a T>; - - fn next(&mut self) -> Option { - if self.sample.compare(self.i) == Comparison::Inside { - let old_i = self.i as usize; - self.i = self.i.wrapping_add(1); - return Some(self.data[old_i].as_ref()); - } - - None - } -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SampleSequenceLocation { - /// head is the first packet in a sequence - pub(crate) head: u16, - /// tail is always set to one after the final sequence number, - /// so if `head == tail` then the sequence is empty - pub(crate) tail: u16, -} - -impl SampleSequenceLocation { - pub(crate) fn new() -> Self { - Self { head: 0, tail: 0 } - } - - pub(crate) fn empty(&self) -> bool { - self.head == self.tail - } - - pub(crate) fn has_data(&self) -> bool { - self.head != self.tail - } - - pub(crate) fn count(&self) -> u16 { - seqnum_distance(self.head, self.tail) - } - - pub(crate) fn compare(&self, pos: u16) -> Comparison { - if self.head == self.tail { - return Comparison::Void; - } - if self.head < self.tail { - if self.head <= pos && pos < self.tail { - return Comparison::Inside; - } - } else if self.head <= pos || pos < self.tail { - return Comparison::Inside; - } - if self.head.wrapping_sub(pos) <= pos.wrapping_sub(self.tail) { - return Comparison::Before; - } - Comparison::After - } - - pub(crate) fn range<'a, T>(&self, data: &'a [Option]) -> Iterator<'a, T> { - Iterator { - data, - sample: *self, - i: self.head, - } - } -} diff --git a/media/src/io/sample_builder/sample_sequence_location_test.rs b/media/src/io/sample_builder/sample_sequence_location_test.rs deleted file mode 100644 index c6e3a3757..000000000 --- a/media/src/io/sample_builder/sample_sequence_location_test.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::sample_sequence_location::*; - -#[test] -fn test_sample_sequence_location_compare() { - let s1 = SampleSequenceLocation { head: 32, tail: 42 }; - assert_eq!(s1.compare(16), Comparison::Before); - assert_eq!(s1.compare(32), Comparison::Inside); - assert_eq!(s1.compare(38), Comparison::Inside); - assert_eq!(s1.compare(41), Comparison::Inside); - assert_eq!(s1.compare(42), Comparison::After); - assert_eq!(s1.compare(0x57), Comparison::After); - - let s2 = SampleSequenceLocation { - head: 0xffa0, - tail: 32, - }; - assert_eq!(s2.compare(0xff00), Comparison::Before); - assert_eq!(s2.compare(0xffa0), Comparison::Inside); - assert_eq!(s2.compare(0xffff), Comparison::Inside); - assert_eq!(s2.compare(0), Comparison::Inside); - assert_eq!(s2.compare(31), Comparison::Inside); - assert_eq!(s2.compare(32), Comparison::After); - assert_eq!(s2.compare(128), Comparison::After); -} - -#[test] -fn test_sample_sequence_location_range() { - let mut data: Vec> = vec![None; u16::MAX as usize + 1]; - - data[65533] = Some(65533); - data[65535] = Some(65535); - data[0] = Some(0); - data[2] = Some(2); - - let s = SampleSequenceLocation { - head: 65533, - tail: 3, - }; - let reconstructed: Vec<_> = s.range(&data).map(|x| x.cloned()).collect(); - - assert_eq!( - reconstructed, - [Some(65533), None, Some(65535), Some(0), None, Some(2)] - ); -} diff --git a/media/src/lib.rs b/media/src/lib.rs deleted file mode 100644 index 264277c56..000000000 --- a/media/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod audio; -mod error; -pub mod io; -pub mod video; - -use std::time::{Duration, SystemTime}; - -use bytes::Bytes; -pub use error::Error; - -/// A Sample contains encoded media and timing information -#[derive(Debug)] -pub struct Sample { - /// The assembled data in the sample, as a bitstream. - /// - /// The format is Codec dependant, but is always a bitstream format - /// rather than the packetized format used when carried over RTP. - /// - /// See: [`rtp::packetizer::Depacketizer`] and implementations of it for more details. - pub data: Bytes, - - /// The wallclock time when this sample was generated. - pub timestamp: SystemTime, - - /// The duration of this sample - pub duration: Duration, - - /// The RTP packet timestamp of this sample. - /// - /// For all RTP packets that contributed to a single sample the timestamp is the same. - pub packet_timestamp: u32, - - /// The number of packets that were dropped prior to building this sample. - /// - /// Packets being dropped doesn't necessarily indicate something wrong, e.g., packets are sometimes - /// dropped because they aren't relevant for sample building. - pub prev_dropped_packets: u16, - - /// The number of packets that were identified as padding prior to building this sample. - /// - /// Some implementations, notably libWebRTC, send padding packets to keep the send rate steady. - /// These packets don't carry media and aren't useful for building samples. - /// - /// This field can be combined with [`Sample::prev_dropped_packets`] to determine if any - /// dropped packets are likely to have detrimental impact on the steadiness of the RTP stream. - /// - /// ## Example adjustment - /// - /// ```rust - /// # use bytes::Bytes; - /// # use std::time::{SystemTime, Duration}; - /// # use webrtc_media::Sample; - /// # let sample = Sample { - /// # data: Bytes::new(), - /// # timestamp: SystemTime::now(), - /// # duration: Duration::from_secs(0), - /// # packet_timestamp: 0, - /// # prev_dropped_packets: 10, - /// # prev_padding_packets: 15 - /// # }; - /// # - /// let adjusted_dropped = - /// sample.prev_dropped_packets.saturating_sub(sample.prev_padding_packets); - /// ``` - pub prev_padding_packets: u16, -} - -impl Default for Sample { - fn default() -> Self { - Sample { - data: Bytes::new(), - timestamp: SystemTime::now(), - duration: Duration::from_secs(0), - packet_timestamp: 0, - prev_dropped_packets: 0, - prev_padding_packets: 0, - } - } -} - -impl PartialEq for Sample { - fn eq(&self, other: &Self) -> bool { - let mut equal: bool = true; - if self.data != other.data { - equal = false; - } - if self.timestamp.elapsed().unwrap().as_secs() - != other.timestamp.elapsed().unwrap().as_secs() - { - equal = false; - } - if self.duration != other.duration { - equal = false; - } - if self.packet_timestamp != other.packet_timestamp { - equal = false; - } - if self.prev_dropped_packets != other.prev_dropped_packets { - equal = false; - } - if self.prev_padding_packets != other.prev_padding_packets { - equal = false; - } - - equal - } -} diff --git a/media/src/video/mod.rs b/media/src/video/mod.rs deleted file mode 100644 index 8b1378917..000000000 --- a/media/src/video/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/rtcp/.gitignore b/rtcp/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/rtcp/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/rtcp/CHANGELOG.md b/rtcp/CHANGELOG.md deleted file mode 100644 index a9e642a8c..000000000 --- a/rtcp/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -# rtcp changelog - -## Unreleased - -## v0.8.0 - -* Fix over-NACK due not resetting lost_packets bitmask [\#372](https://github.com/webrtc-rs/webrtc/pull/372/). -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - -## v0.7.0 - -* [#14 Prevent crash in RTCP NACK writing](https://github.com/webrtc-rs/rtcp/pull/14) by [@pthatcher](https://github.com/pthatcher). -* Adds `IntoIterator` for `NackPair` which iterates over all the sequence numbers specified by the `NackPair`. This is similar to `packet_list` but without requiring the allocation of a Vec. Added in [#225 Add RTP Stats to stats report](https://github.com/webrtc-rs/webrtc/pull/225) by [@k0nserv](https://github.com/k0nserv). - - -## Prior to 0.7.0 - -Before 0.7.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/rtcp/releases). diff --git a/rtcp/Cargo.toml b/rtcp/Cargo.toml deleted file mode 100644 index e5ed6febc..000000000 --- a/rtcp/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "rtcp" -version = "0.11.0" -authors = ["Rain Liu ", "Michael Uti "] -edition = "2021" -description = "A pure Rust implementation of RTCP" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/rtcp" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/rtcp" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["marshal"] } - -bytes = "1" -thiserror = "1" diff --git a/rtcp/LICENSE-APACHE b/rtcp/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/rtcp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/rtcp/LICENSE-MIT b/rtcp/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/rtcp/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/rtcp/README.md b/rtcp/README.md deleted file mode 100644 index 348f180fb..000000000 --- a/rtcp/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of RTCP. Rewrite Pion RTCP in Rust -

diff --git a/rtcp/codecov.yml b/rtcp/codecov.yml deleted file mode 100644 index e72b36629..000000000 --- a/rtcp/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 2971c79d-6f37-4e06-924b-e2325e3c8a06 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/rtcp/doc/webrtc.rs.png b/rtcp/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/rtcp/src/compound_packet/compound_packet_test.rs b/rtcp/src/compound_packet/compound_packet_test.rs deleted file mode 100644 index 84d5fa5a9..000000000 --- a/rtcp/src/compound_packet/compound_packet_test.rs +++ /dev/null @@ -1,329 +0,0 @@ -use super::*; -use crate::goodbye::Goodbye; -use crate::payload_feedbacks::picture_loss_indication::PictureLossIndication; - -// An RTCP packet from a packet dump -const REAL_PACKET: [u8; 116] = [ - // Receiver Report (offset=0) - 0x81, 0xc9, 0x0, 0x7, // v=2, p=0, count=1, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - // Source Description (offset=32) - 0x81, 0xca, 0x0, 0xc, // v=2, p=0, count=1, SDES, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x1, 0x26, // CNAME, len=38 - 0x7b, 0x39, 0x63, 0x30, 0x30, 0x65, 0x62, 0x39, 0x32, 0x2d, 0x31, 0x61, 0x66, 0x62, 0x2d, 0x39, - 0x64, 0x34, 0x39, 0x2d, 0x61, 0x34, 0x37, 0x64, 0x2d, 0x39, 0x31, 0x66, 0x36, 0x34, 0x65, 0x65, - 0x65, 0x36, 0x39, 0x66, 0x35, 0x7d, // text="{9c00eb92-1afb-9d49-a47d-91f64eee69f5}" - 0x0, 0x0, 0x0, 0x0, // END + padding - // Goodbye (offset=84) - 0x81, 0xcb, 0x0, 0x1, // v=2, p=0, count=1, BYE, len=1 - 0x90, 0x2f, 0x9e, 0x2e, // source=0x902f9e2e - 0x81, 0xce, 0x0, 0x2, // Picture Loss Indication (offset=92) - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - 0x85, 0xcd, 0x0, 0x2, // RapidResynchronizationRequest (offset=104) - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e -]; - -#[test] -fn test_read_eof() { - let mut short_header = Bytes::from_static(&[ - 0x81, 0xc9, // missing type & len - ]); - let result = unmarshal(&mut short_header); - assert!(result.is_err(), "missing type & len"); -} - -#[test] -fn test_bad_compound() { - let mut bad_compound = Bytes::copy_from_slice(&REAL_PACKET[..34]); - let result = unmarshal(&mut bad_compound); - assert!(result.is_err(), "trailing data!"); - - let mut bad_compound = Bytes::copy_from_slice(&REAL_PACKET[84..104]); - let p = unmarshal(&mut bad_compound).expect("Error unmarshalling packet"); - let compound = CompoundPacket(p); - - // this should return an error, - // it violates the "must start with RR or SR" rule - match compound.validate() { - Ok(_) => panic!("validation should return an error"), - - Err(err) => { - let a = Error::BadFirstPacket; - assert_eq!( - Error::BadFirstPacket, - err, - "Unmarshal(badcompound) err={err:?}, want {a:?}", - ); - } - }; - - let compound_len = compound.0.len(); - assert_eq!( - compound_len, 2, - "Unmarshal(badcompound) len={}, want {}", - compound_len, 2 - ); - - if compound.0[0].as_any().downcast_ref::().is_none() { - panic!("Unmarshal(badcompound), want Goodbye") - } - - if compound.0[1] - .as_any() - .downcast_ref::() - .is_none() - { - panic!("Unmarshal(badcompound), want PictureLossIndication") - } -} - -#[test] -fn test_valid_packet() { - let cname = SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1234, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"cname"), - }], - }], - }; - - let tests: Vec<(&str, CompoundPacket, Option)> = vec![ - ( - "no cname", - CompoundPacket(vec![Box::::default()]), - Some(Error::MissingCname), - ), - ( - "SDES / no cname", - CompoundPacket(vec![ - Box::::default(), - Box::::default(), - ]), - Some(Error::MissingCname), - ), - ( - "just SR", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname.to_owned()), - ]), - None, - ), - ( - "multiple SRs", - CompoundPacket(vec![ - Box::::default(), - Box::::default(), - Box::new(cname.clone()), - ]), - Some(Error::PacketBeforeCname), - ), - ( - "just RR", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname.clone()), - ]), - None, - ), - ( - "multiple RRs", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname.clone()), - Box::::default(), - ]), - None, - ), - ( - "goodbye", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname), - Box::::default(), - ]), - None, - ), - ]; - - for (name, packet, error) in tests { - let result = packet.validate(); - assert_eq!(result.is_ok(), error.is_none()); - if let (Some(err), Err(got)) = (error, result) { - assert_eq!(err, got, "Valid({name}) = {got:?}, want {err:?}"); - } - } -} - -#[test] -fn test_cname() { - let cname = SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1234, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"cname"), - }], - }], - }; - - let tests: Vec<(&str, CompoundPacket, Option, &str)> = vec![ - ( - "no cname", - CompoundPacket(vec![Box::::default()]), - Some(Error::MissingCname), - "", - ), - ( - "SDES / no cname", - CompoundPacket(vec![ - Box::::default(), - Box::::default(), - ]), - Some(Error::MissingCname), - "", - ), - ( - "just SR", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname.clone()), - ]), - None, - "cname", - ), - ( - "multiple SRs", - CompoundPacket(vec![ - Box::::default(), - Box::::default(), - Box::new(cname.clone()), - ]), - Some(Error::PacketBeforeCname), - "", - ), - ( - "just RR", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname.clone()), - ]), - None, - "cname", - ), - ( - "multiple RRs", - CompoundPacket(vec![ - Box::::default(), - Box::::default(), - Box::new(cname.clone()), - ]), - None, - "cname", - ), - ( - "goodbye", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname), - Box::::default(), - ]), - None, - "cname", - ), - ]; - - for (name, compound_packet, want_error, text) in tests { - let err = compound_packet.validate(); - assert_eq!(err.is_err(), want_error.is_some()); - if let (Some(want), Err(err)) = (&want_error, err) { - assert_eq!(*want, err, "Valid({name}) = {err:?}, want {want:?}"); - } - - let name_result = compound_packet.cname(); - assert_eq!(name_result.is_err(), want_error.is_some()); - - match name_result { - Ok(e) => { - assert_eq!(e, text, "CNAME({name}) = {e:?}, want {text}",); - } - - Err(err) => { - if let Some(want) = &want_error { - assert_eq!(*want, err, "CNAME({name}) = {err:?}, want {want:?}"); - } - } - } - } -} - -#[test] -fn test_compound_packet_roundtrip() { - let cname = SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1234, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"cname"), - }], - }], - }; - - let tests = vec![ - ( - "goodbye", - CompoundPacket(vec![ - Box::::default(), - Box::new(cname), - Box::new(Goodbye { - sources: vec![1234], - ..Default::default() - }), - ]), - None, - ), - ( - "no cname", - CompoundPacket(vec![Box::::default()]), - Some(Error::MissingCname), - ), - ]; - - for (name, packet, marshal_error) in tests { - let result = packet.marshal(); - if let Some(err) = marshal_error { - if let Err(got) = result { - assert_eq!(err, got, "marshal {name} header: err = {got}, want {err}"); - } else { - panic!("want error in test {name}"); - } - continue; - } else { - assert!(result.is_ok(), "must no error in test {name}"); - } - - let data1 = result.unwrap(); - let c = CompoundPacket::unmarshal(&mut data1.clone()) - .unwrap_or_else(|_| panic!("unmarshal {name} error")); - - let data2 = c - .marshal() - .unwrap_or_else(|_| panic!("marshal {name} error")); - - assert_eq!( - data1, data2, - "Unmarshal(Marshal({name:?})) = {data1:?}, want {data2:?}" - ) - } -} diff --git a/rtcp/src/compound_packet/mod.rs b/rtcp/src/compound_packet/mod.rs deleted file mode 100644 index 6a744f177..000000000 --- a/rtcp/src/compound_packet/mod.rs +++ /dev/null @@ -1,195 +0,0 @@ -#[cfg(test)] -mod compound_packet_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::receiver_report::*; -use crate::sender_report::*; -use crate::source_description::*; -use crate::util::*; - -type Result = std::result::Result; - -/// A CompoundPacket is a collection of RTCP packets transmitted as a single packet with -/// the underlying protocol (for example UDP). -/// -/// To maximize the resolution of reception statistics, the first Packet in a CompoundPacket -/// must always be either a SenderReport or a ReceiverReport. This is true even if no data -/// has been sent or received, in which case an empty ReceiverReport must be sent, and even -/// if the only other RTCP packet in the compound packet is a Goodbye. -/// -/// Next, a SourceDescription containing a CNAME item must be included in each CompoundPacket -/// to identify the source and to begin associating media for purposes such as lip-sync. -/// -/// Other RTCP packet types may follow in any order. Packet types may appear more than once. -#[derive(Debug, Default, PartialEq, Clone)] -pub struct CompoundPacket(pub Vec>); - -impl fmt::Display for CompoundPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl Packet for CompoundPacket { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns the synchronization sources associated with this - /// CompoundPacket's reception report. - fn destination_ssrc(&self) -> Vec { - if self.0.is_empty() { - vec![] - } else { - self.0[0].destination_ssrc() - } - } - - fn raw_size(&self) -> usize { - let mut l = 0; - for packet in &self.0 { - l += packet.marshal_size(); - } - l - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for CompoundPacket { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for CompoundPacket { - /// Marshal encodes the CompoundPacket as binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - self.validate()?; - - for packet in &self.0 { - let n = packet.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for CompoundPacket { - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let mut packets = vec![]; - - while raw_packet.has_remaining() { - let p = unmarshaller(raw_packet)?; - packets.push(p); - } - - let c = CompoundPacket(packets); - c.validate()?; - - Ok(c) - } -} - -impl CompoundPacket { - /// Validate returns an error if this is not an RFC-compliant CompoundPacket. - pub fn validate(&self) -> Result<()> { - if self.0.is_empty() { - return Err(Error::EmptyCompound.into()); - } - - // SenderReport and ReceiverReport are the only types that - // are allowed to be the first packet in a compound datagram - if self.0[0].as_any().downcast_ref::().is_none() - && self.0[0] - .as_any() - .downcast_ref::() - .is_none() - { - return Err(Error::BadFirstPacket.into()); - } - - for pkt in &self.0[1..] { - // If the number of RecetpionReports exceeds 31 additional ReceiverReports - // can be included here. - if pkt.as_any().downcast_ref::().is_some() { - continue; - // A SourceDescription containing a CNAME must be included in every - // CompoundPacket. - } else if let Some(e) = pkt.as_any().downcast_ref::() { - let mut has_cname = false; - for c in &e.chunks { - for it in &c.items { - if it.sdes_type == SdesType::SdesCname { - has_cname = true - } - } - } - - if !has_cname { - return Err(Error::MissingCname.into()); - } - - return Ok(()); - - // Other packets are not permitted before the CNAME - } else { - return Err(Error::PacketBeforeCname.into()); - } - } - - // CNAME never reached - Err(Error::MissingCname.into()) - } - - /// CNAME returns the CNAME that *must* be present in every CompoundPacket - pub fn cname(&self) -> Result { - if self.0.is_empty() { - return Err(Error::EmptyCompound.into()); - } - - for pkt in &self.0[1..] { - if let Some(sdes) = pkt.as_any().downcast_ref::() { - for c in &sdes.chunks { - for it in &c.items { - if it.sdes_type == SdesType::SdesCname { - return Ok(it.text.clone()); - } - } - } - } else if pkt.as_any().downcast_ref::().is_none() { - return Err(Error::PacketBeforeCname.into()); - } - } - - Err(Error::MissingCname.into()) - } -} diff --git a/rtcp/src/error.rs b/rtcp/src/error.rs deleted file mode 100644 index d46487904..000000000 --- a/rtcp/src/error.rs +++ /dev/null @@ -1,120 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - /// Wrong marshal size. - #[error("Wrong marshal size")] - WrongMarshalSize, - /// Packet lost exceeds maximum amount of packets - /// that can possibly be lost. - #[error("Invalid total lost count")] - InvalidTotalLost, - /// Packet contains an invalid header. - #[error("Invalid header")] - InvalidHeader, - /// Packet contains empty compound. - #[error("Empty compound packet")] - EmptyCompound, - /// Invalid first packet in compound packets. First packet - /// should either be a SenderReport packet or ReceiverReport - #[error("First packet in compound must be SR or RR")] - BadFirstPacket, - /// CNAME was not defined. - #[error("Compound missing SourceDescription with CNAME")] - MissingCname, - /// Packet was defined before CNAME. - #[error("Feedback packet seen before CNAME")] - PacketBeforeCname, - /// Too many reports. - #[error("Too many reports")] - TooManyReports, - /// Too many chunks. - #[error("Too many chunks")] - TooManyChunks, - /// Too many sources. - #[error("too many sources")] - TooManySources, - /// Packet received is too short. - #[error("Packet status chunk must be 2 bytes")] - PacketTooShort, - /// Buffer is too short. - #[error("Buffer too short to be written")] - BufferTooShort, - /// Wrong packet type. - #[error("Wrong packet type")] - WrongType, - /// SDES received is too long. - #[error("SDES must be < 255 octets long")] - SdesTextTooLong, - /// SDES type is missing. - #[error("SDES item missing type")] - SdesMissingType, - /// Reason is too long. - #[error("Reason must be < 255 octets long")] - ReasonTooLong, - /// Invalid packet version. - #[error("Invalid packet version")] - BadVersion, - /// Invalid padding value. - #[error("Invalid padding value")] - WrongPadding, - /// Wrong feedback message type. - #[error("Wrong feedback message type")] - WrongFeedbackType, - /// Wrong payload type. - #[error("Wrong payload type")] - WrongPayloadType, - /// Header length is too small. - #[error("Header length is too small")] - HeaderTooSmall, - /// Media ssrc was defined as zero. - #[error("Media SSRC must be 0")] - SsrcMustBeZero, - /// Missing REMB identifier. - #[error("Missing REMB identifier")] - MissingRembIdentifier, - /// SSRC number and length mismatches. - #[error("SSRC num and length do not match")] - SsrcNumAndLengthMismatch, - /// Invalid size or start index. - #[error("Invalid size or startIndex")] - InvalidSizeOrStartIndex, - /// Delta exceeds limit. - #[error("Delta exceed limit")] - DeltaExceedLimit, - /// Packet status chunk is not 2 bytes. - #[error("Packet status chunk must be 2 bytes")] - PacketStatusChunkLength, - #[error("Invalid bitrate")] - InvalidBitrate, - #[error("Wrong chunk type")] - WrongChunkType, - #[error("Struct contains unexpected member type")] - BadStructMemberType, - #[error("Cannot read into non-pointer")] - BadReadParameter, - - #[error("{0}")] - Util(#[from] util::Error), - - #[error("{0}")] - Other(String), -} - -impl From for util::Error { - fn from(e: Error) -> Self { - util::Error::from_std(e) - } -} - -impl PartialEq for Error { - fn eq(&self, other: &util::Error) -> bool { - if let Some(down) = other.downcast_ref::() { - return self == down; - } - false - } -} diff --git a/rtcp/src/extended_report/dlrr.rs b/rtcp/src/extended_report/dlrr.rs deleted file mode 100644 index c5d9693d3..000000000 --- a/rtcp/src/extended_report/dlrr.rs +++ /dev/null @@ -1,151 +0,0 @@ -use super::*; - -const DLRR_REPORT_LENGTH: u16 = 12; - -/// DLRRReport encodes a single report inside a DLRRReportBlock. -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct DLRRReport { - pub ssrc: u32, - pub last_rr: u32, - pub dlrr: u32, -} - -impl fmt::Display for DLRRReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -/// DLRRReportBlock encodes a DLRR Report Block as described in -/// RFC 3611 section 4.5. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT=5 | reserved | block length | -/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ -/// | SSRC_1 (ssrc of first receiver) | sub- -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block -/// | last RR (LRR) | 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | delay since last RR (DLRR) | -/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ -/// | SSRC_2 (ssrc of second receiver) | sub- -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block -/// : ... : 2 -/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct DLRRReportBlock { - pub reports: Vec, -} - -impl fmt::Display for DLRRReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl DLRRReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: BlockType::DLRR, - type_specific: 0, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for DLRRReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - let mut ssrc = Vec::with_capacity(self.reports.len()); - for r in &self.reports { - ssrc.push(r.ssrc); - } - ssrc - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + self.reports.len() * 4 * 3 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for DLRRReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for DLRRReportBlock { - /// marshal_to encodes the DLRRReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - for rep in &self.reports { - buf.put_u32(rep.ssrc); - buf.put_u32(rep.last_rr); - buf.put_u32(rep.dlrr); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for DLRRReportBlock { - /// Unmarshal decodes the DLRRReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length % DLRR_REPORT_LENGTH != 0 || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let mut offset = 0; - let mut reports = vec![]; - while offset < block_length { - let ssrc = raw_packet.get_u32(); - let last_rr = raw_packet.get_u32(); - let dlrr = raw_packet.get_u32(); - reports.push(DLRRReport { - ssrc, - last_rr, - dlrr, - }); - offset += DLRR_REPORT_LENGTH; - } - - Ok(DLRRReportBlock { reports }) - } -} diff --git a/rtcp/src/extended_report/extended_report_test.rs b/rtcp/src/extended_report/extended_report_test.rs deleted file mode 100644 index 3e42526e1..000000000 --- a/rtcp/src/extended_report/extended_report_test.rs +++ /dev/null @@ -1,184 +0,0 @@ -use super::*; - -fn decoded_packet() -> ExtendedReport { - ExtendedReport { - sender_ssrc: 0x01020304, - reports: vec![ - Box::new(LossRLEReportBlock { - is_loss_rle: true, - t: 12, - - ssrc: 0x12345689, - begin_seq: 5, - end_seq: 12, - chunks: vec![Chunk(0x4006), Chunk(0x0006), Chunk(0x8765), Chunk(0x0000)], - }), - Box::new(DuplicateRLEReportBlock { - is_loss_rle: false, - t: 6, - - ssrc: 0x12345689, - begin_seq: 5, - end_seq: 12, - chunks: vec![Chunk(0x4123), Chunk(0x3FFF), Chunk(0xFFFF), Chunk(0x0000)], - }), - Box::new(PacketReceiptTimesReportBlock { - t: 3, - - ssrc: 0x98765432, - begin_seq: 15432, - end_seq: 15577, - receipt_time: vec![0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555], - }), - Box::new(ReceiverReferenceTimeReportBlock { - ntp_timestamp: 0x0102030405060708, - }), - Box::new(DLRRReportBlock { - reports: vec![ - DLRRReport { - ssrc: 0x88888888, - last_rr: 0x12345678, - dlrr: 0x99999999, - }, - DLRRReport { - ssrc: 0x09090909, - last_rr: 0x12345678, - dlrr: 0x99999999, - }, - DLRRReport { - ssrc: 0x11223344, - last_rr: 0x12345678, - dlrr: 0x99999999, - }, - ], - }), - Box::new(StatisticsSummaryReportBlock { - loss_reports: true, - duplicate_reports: true, - jitter_reports: true, - ttl_or_hop_limit: TTLorHopLimitType::IPv4, - - ssrc: 0xFEDCBA98, - begin_seq: 0x1234, - end_seq: 0x5678, - lost_packets: 0x11111111, - dup_packets: 0x22222222, - min_jitter: 0x33333333, - max_jitter: 0x44444444, - mean_jitter: 0x55555555, - dev_jitter: 0x66666666, - min_ttl_or_hl: 0x01, - max_ttl_or_hl: 0x02, - mean_ttl_or_hl: 0x03, - dev_ttl_or_hl: 0x04, - }), - Box::new(VoIPMetricsReportBlock { - ssrc: 0x89ABCDEF, - loss_rate: 0x05, - discard_rate: 0x06, - burst_density: 0x07, - gap_density: 0x08, - burst_duration: 0x1111, - gap_duration: 0x2222, - round_trip_delay: 0x3333, - end_system_delay: 0x4444, - signal_level: 0x11, - noise_level: 0x22, - rerl: 0x33, - gmin: 0x44, - rfactor: 0x55, - ext_rfactor: 0x66, - mos_lq: 0x77, - mos_cq: 0x88, - rx_config: 0x99, - reserved: 0x00, - jb_nominal: 0x1122, - jb_maximum: 0x3344, - jb_abs_max: 0x5566, - }), - ], - } -} - -fn encoded_packet() -> Bytes { - Bytes::from_static(&[ - // RTP Header - 0x80, 0xCF, 0x00, 0x33, // byte 0 - 3 - // Sender SSRC - 0x01, 0x02, 0x03, 0x04, // Loss RLE Report Block - 0x01, 0x0C, 0x00, 0x04, // byte 8 - 11 - // Source SSRC - 0x12, 0x34, 0x56, 0x89, // Begin & End Seq - 0x00, 0x05, 0x00, 0x0C, // byte 16 - 19 - // Chunks - 0x40, 0x06, 0x00, 0x06, 0x87, 0x65, 0x00, 0x00, // byte 24 - 27 - // Duplicate RLE Report Block - 0x02, 0x06, 0x00, 0x04, // Source SSRC - 0x12, 0x34, 0x56, 0x89, // byte 32 - 35 - // Begin & End Seq - 0x00, 0x05, 0x00, 0x0C, // Chunks - 0x41, 0x23, 0x3F, 0xFF, // byte 40 - 43 - 0xFF, 0xFF, 0x00, 0x00, // Packet Receipt Times Report Block - 0x03, 0x03, 0x00, 0x07, // byte 48 - 51 - // Source SSRC - 0x98, 0x76, 0x54, 0x32, // Begin & End Seq - 0x3C, 0x48, 0x3C, 0xD9, // byte 56 - 59 - // Receipt times - 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, // byte 64 - 67 - 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, // byte 72 - 75 - 0x55, 0x55, 0x55, 0x55, // Receiver Reference Time Report - 0x04, 0x00, 0x00, 0x02, // byte 80 - 83 - // Timestamp - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // byte 88 - 91 - // DLRR Report - 0x05, 0x00, 0x00, 0x09, // SSRC 1 - 0x88, 0x88, 0x88, 0x88, // byte 96 - 99 - // LastRR 1 - 0x12, 0x34, 0x56, 0x78, // DLRR 1 - 0x99, 0x99, 0x99, 0x99, // byte 104 - 107 - // SSRC 2 - 0x09, 0x09, 0x09, 0x09, // LastRR 2 - 0x12, 0x34, 0x56, 0x78, // byte 112 - 115 - // DLRR 2 - 0x99, 0x99, 0x99, 0x99, // SSRC 3 - 0x11, 0x22, 0x33, 0x44, // byte 120 - 123 - // LastRR 3 - 0x12, 0x34, 0x56, 0x78, // DLRR 3 - 0x99, 0x99, 0x99, 0x99, // byte 128 - 131 - // Statistics Summary Report - 0x06, 0xE8, 0x00, 0x09, // SSRC - 0xFE, 0xDC, 0xBA, 0x98, // byte 136 - 139 - // Various statistics - 0x12, 0x34, 0x56, 0x78, 0x11, 0x11, 0x11, 0x11, // byte 144 - 147 - 0x22, 0x22, 0x22, 0x22, 0x33, 0x33, 0x33, 0x33, // byte 152 - 155 - 0x44, 0x44, 0x44, 0x44, 0x55, 0x55, 0x55, 0x55, // byte 160 - 163 - 0x66, 0x66, 0x66, 0x66, 0x01, 0x02, 0x03, 0x04, // byte 168 - 171 - // VoIP Metrics Report - 0x07, 0x00, 0x00, 0x08, // SSRC - 0x89, 0xAB, 0xCD, 0xEF, // byte 176 - 179 - // Various statistics - 0x05, 0x06, 0x07, 0x08, 0x11, 0x11, 0x22, 0x22, // byte 184 - 187 - 0x33, 0x33, 0x44, 0x44, 0x11, 0x22, 0x33, 0x44, // byte 192 - 195 - 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, // byte 200 - 203 - 0x33, 0x44, 0x55, 0x66, // byte 204 - 207 - ]) -} - -#[test] -fn test_encode() -> Result<()> { - let expected = encoded_packet(); - let packet = decoded_packet(); - let actual = packet.marshal()?; - assert_eq!(actual, expected); - Ok(()) -} - -#[test] -fn test_decode() -> Result<()> { - let mut encoded = encoded_packet(); - let expected = decoded_packet(); - let actual = ExtendedReport::unmarshal(&mut encoded)?; - assert_eq!(actual, expected); - assert_eq!(actual.to_string(), expected.to_string()); - Ok(()) -} diff --git a/rtcp/src/extended_report/mod.rs b/rtcp/src/extended_report/mod.rs deleted file mode 100644 index e8a2f3732..000000000 --- a/rtcp/src/extended_report/mod.rs +++ /dev/null @@ -1,302 +0,0 @@ -#[cfg(test)] -mod extended_report_test; - -pub mod dlrr; -pub mod prt; -pub mod rle; -pub mod rrt; -pub mod ssr; -pub mod unknown; -pub mod vm; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -pub use dlrr::{DLRRReport, DLRRReportBlock}; -pub use prt::PacketReceiptTimesReportBlock; -pub use rle::{Chunk, ChunkType, DuplicateRLEReportBlock, LossRLEReportBlock, RLEReportBlock}; -pub use rrt::ReceiverReferenceTimeReportBlock; -pub use ssr::{StatisticsSummaryReportBlock, TTLorHopLimitType}; -pub use unknown::UnknownReportBlock; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; -pub use vm::VoIPMetricsReportBlock; - -use crate::error; -use crate::header::{Header, PacketType, HEADER_LENGTH, SSRC_LENGTH}; -use crate::packet::Packet; -use crate::util::{get_padding_size, put_padding}; - -type Result = std::result::Result; - -const XR_HEADER_LENGTH: usize = 4; - -/// BlockType specifies the type of report in a report block -/// Extended Report block types from RFC 3611. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum BlockType { - #[default] - Unknown = 0, - LossRLE = 1, // RFC 3611, section 4.1 - DuplicateRLE = 2, // RFC 3611, section 4.2 - PacketReceiptTimes = 3, // RFC 3611, section 4.3 - ReceiverReferenceTime = 4, // RFC 3611, section 4.4 - DLRR = 5, // RFC 3611, section 4.5 - StatisticsSummary = 6, // RFC 3611, section 4.6 - VoIPMetrics = 7, // RFC 3611, section 4.7 -} - -impl From for BlockType { - fn from(v: u8) -> Self { - match v { - 1 => BlockType::LossRLE, - 2 => BlockType::DuplicateRLE, - 3 => BlockType::PacketReceiptTimes, - 4 => BlockType::ReceiverReferenceTime, - 5 => BlockType::DLRR, - 6 => BlockType::StatisticsSummary, - 7 => BlockType::VoIPMetrics, - _ => BlockType::Unknown, - } - } -} - -/// converts the Extended report block types into readable strings -impl fmt::Display for BlockType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - BlockType::LossRLE => "LossRLEReportBlockType", - BlockType::DuplicateRLE => "DuplicateRLEReportBlockType", - BlockType::PacketReceiptTimes => "PacketReceiptTimesReportBlockType", - BlockType::ReceiverReferenceTime => "ReceiverReferenceTimeReportBlockType", - BlockType::DLRR => "DLRRReportBlockType", - BlockType::StatisticsSummary => "StatisticsSummaryReportBlockType", - BlockType::VoIPMetrics => "VoIPMetricsReportBlockType", - _ => "UnknownReportBlockType", - }; - write!(f, "{s}") - } -} - -/// TypeSpecificField as described in RFC 3611 section 4.5. In typical -/// cases, users of ExtendedReports shouldn't need to access this, -/// and should instead use the corresponding fields in the actual -/// report blocks themselves. -pub type TypeSpecificField = u8; - -/// XRHeader defines the common fields that must appear at the start -/// of each report block. In typical cases, users of ExtendedReports -/// shouldn't need to access this. For locally-constructed report -/// blocks, these values will not be accurate until the corresponding -/// packet is marshaled. -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct XRHeader { - pub block_type: BlockType, - pub type_specific: TypeSpecificField, - pub block_length: u16, -} - -impl MarshalSize for XRHeader { - fn marshal_size(&self) -> usize { - XR_HEADER_LENGTH - } -} - -impl Marshal for XRHeader { - /// marshal_to encodes the ExtendedReport in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < XR_HEADER_LENGTH { - return Err(error::Error::BufferTooShort.into()); - } - - buf.put_u8(self.block_type as u8); - buf.put_u8(self.type_specific); - buf.put_u16(self.block_length); - - Ok(XR_HEADER_LENGTH) - } -} - -impl Unmarshal for XRHeader { - /// Unmarshal decodes the ExtendedReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let block_type: BlockType = raw_packet.get_u8().into(); - let type_specific = raw_packet.get_u8(); - let block_length = raw_packet.get_u16(); - - Ok(XRHeader { - block_type, - type_specific, - block_length, - }) - } -} -/// The ExtendedReport packet is an Implementation of RTCP Extended -/// reports defined in RFC 3611. It is used to convey detailed -/// information about an RTP stream. Each packet contains one or -/// more report blocks, each of which conveys a different kind of -/// information. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |V=2|P|reserved | PT=XR=207 | length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ssrc | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// : report blocks : -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, PartialEq, Default, Clone)] -pub struct ExtendedReport { - pub sender_ssrc: u32, - pub reports: Vec>, -} - -impl fmt::Display for ExtendedReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl Packet for ExtendedReport { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: 0, - packet_type: PacketType::ExtendedReport, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of ssrc values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - let mut ssrc = vec![]; - for p in &self.reports { - ssrc.extend(p.destination_ssrc()); - } - ssrc - } - - fn raw_size(&self) -> usize { - let mut reps_length = 0; - for rep in &self.reports { - reps_length += rep.marshal_size(); - } - HEADER_LENGTH + SSRC_LENGTH + reps_length - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for ExtendedReport { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for ExtendedReport { - /// marshal_to encodes the ExtendedReport in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - - for report in &self.reports { - let n = report.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for ExtendedReport { - /// Unmarshal decodes the ExtendedReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(error::Error::PacketTooShort.into()); - } - - let header = Header::unmarshal(raw_packet)?; - if header.packet_type != PacketType::ExtendedReport { - return Err(error::Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - - let mut offset = HEADER_LENGTH + SSRC_LENGTH; - let mut reports = vec![]; - while raw_packet.remaining() > 0 { - if offset + XR_HEADER_LENGTH > raw_packet_len { - return Err(error::Error::PacketTooShort.into()); - } - - let block_type: BlockType = raw_packet.chunk()[0].into(); - let report: Box = match block_type { - BlockType::LossRLE => Box::new(LossRLEReportBlock::unmarshal(raw_packet)?), - BlockType::DuplicateRLE => { - Box::new(DuplicateRLEReportBlock::unmarshal(raw_packet)?) - } - BlockType::PacketReceiptTimes => { - Box::new(PacketReceiptTimesReportBlock::unmarshal(raw_packet)?) - } - BlockType::ReceiverReferenceTime => { - Box::new(ReceiverReferenceTimeReportBlock::unmarshal(raw_packet)?) - } - BlockType::DLRR => Box::new(DLRRReportBlock::unmarshal(raw_packet)?), - BlockType::StatisticsSummary => { - Box::new(StatisticsSummaryReportBlock::unmarshal(raw_packet)?) - } - BlockType::VoIPMetrics => Box::new(VoIPMetricsReportBlock::unmarshal(raw_packet)?), - _ => Box::new(UnknownReportBlock::unmarshal(raw_packet)?), - }; - - offset += report.marshal_size(); - reports.push(report); - } - - Ok(ExtendedReport { - sender_ssrc, - reports, - }) - } -} diff --git a/rtcp/src/extended_report/prt.rs b/rtcp/src/extended_report/prt.rs deleted file mode 100644 index c9da820cb..000000000 --- a/rtcp/src/extended_report/prt.rs +++ /dev/null @@ -1,150 +0,0 @@ -use super::*; - -const PRT_REPORT_BLOCK_MIN_LENGTH: u16 = 8; - -/// PacketReceiptTimesReportBlock represents a Packet Receipt Times -/// report block, as described in RFC 3611 section 4.3. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT=3 | rsvd. | t | block length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ssrc of source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | begin_seq | end_seq | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Receipt time of packet begin_seq | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Receipt time of packet (begin_seq + 1) mod 65536 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// : ... : -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Receipt time of packet (end_seq - 1) mod 65536 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct PacketReceiptTimesReportBlock { - //not included in marshal/unmarshal - pub t: u8, - - //marshal/unmarshal - pub ssrc: u32, - pub begin_seq: u16, - pub end_seq: u16, - pub receipt_time: Vec, -} - -impl fmt::Display for PacketReceiptTimesReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl PacketReceiptTimesReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: BlockType::PacketReceiptTimes, - type_specific: self.t & 0x0F, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for PacketReceiptTimesReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.ssrc] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + PRT_REPORT_BLOCK_MIN_LENGTH as usize + self.receipt_time.len() * 4 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for PacketReceiptTimesReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for PacketReceiptTimesReportBlock { - /// marshal_to encodes the PacketReceiptTimesReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - buf.put_u16(self.begin_seq); - buf.put_u16(self.end_seq); - for rt in &self.receipt_time { - buf.put_u32(*rt); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for PacketReceiptTimesReportBlock { - /// Unmarshal decodes the PacketReceiptTimesReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length < PRT_REPORT_BLOCK_MIN_LENGTH - || (block_length - PRT_REPORT_BLOCK_MIN_LENGTH) % 4 != 0 - || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let t = xr_header.type_specific & 0x0F; - - let ssrc = raw_packet.get_u32(); - let begin_seq = raw_packet.get_u16(); - let end_seq = raw_packet.get_u16(); - - let remaining = block_length - PRT_REPORT_BLOCK_MIN_LENGTH; - let mut receipt_time = vec![]; - for _ in 0..remaining / 4 { - receipt_time.push(raw_packet.get_u32()); - } - - Ok(PacketReceiptTimesReportBlock { - t, - - ssrc, - begin_seq, - end_seq, - receipt_time, - }) - } -} diff --git a/rtcp/src/extended_report/rle.rs b/rtcp/src/extended_report/rle.rs deleted file mode 100644 index fe6770b76..000000000 --- a/rtcp/src/extended_report/rle.rs +++ /dev/null @@ -1,246 +0,0 @@ -use super::*; - -const RLE_REPORT_BLOCK_MIN_LENGTH: u16 = 8; - -/// ChunkType enumerates the three kinds of chunks described in RFC 3611 section 4.1. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ChunkType { - RunLength = 0, - BitVector = 1, - TerminatingNull = 2, -} - -/// Chunk as defined in RFC 3611, section 4.1. These represent information -/// about packet losses and packet duplication. They have three representations: -/// -/// Run Length Chunk: -/// -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |C|R| run length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Bit Vector Chunk: -/// -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |C| bit vector | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Terminating Null Chunk: -/// -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0| -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct Chunk(pub u16); - -impl fmt::Display for Chunk { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.chunk_type() { - ChunkType::RunLength => { - let run_type = self.run_type().unwrap_or(0); - write!(f, "[RunLength type={}, length={}]", run_type, self.value()) - } - ChunkType::BitVector => write!(f, "[BitVector {:#b}", self.value()), - ChunkType::TerminatingNull => write!(f, "[TerminatingNull]"), - } - } -} -impl Chunk { - /// chunk_type returns the ChunkType that this Chunk represents - pub fn chunk_type(&self) -> ChunkType { - if self.0 == 0 { - ChunkType::TerminatingNull - } else if (self.0 >> 15) == 0 { - ChunkType::RunLength - } else { - ChunkType::BitVector - } - } - - /// run_type returns the run_type that this Chunk represents. It is - /// only valid if ChunkType is RunLengthChunkType. - pub fn run_type(&self) -> error::Result { - if self.chunk_type() != ChunkType::RunLength { - Err(error::Error::WrongChunkType) - } else { - Ok((self.0 >> 14) as u8 & 0x01) - } - } - - /// value returns the value represented in this Chunk - pub fn value(&self) -> u16 { - match self.chunk_type() { - ChunkType::RunLength => self.0 & 0x3FFF, - ChunkType::BitVector => self.0 & 0x7FFF, - ChunkType::TerminatingNull => 0, - } - } -} - -/// RleReportBlock defines the common structure used by both -/// Loss RLE report blocks (RFC 3611 §4.1) and Duplicate RLE -/// report blocks (RFC 3611 §4.2). -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT = 1 or 2 | rsvd. | t | block length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ssrc of source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | begin_seq | end_seq | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | chunk 1 | chunk 2 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// : ... : -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | chunk n-1 | chunk n | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct RLEReportBlock { - //not included in marshal/unmarshal - pub is_loss_rle: bool, - pub t: u8, - - //marshal/unmarshal - pub ssrc: u32, - pub begin_seq: u16, - pub end_seq: u16, - pub chunks: Vec, -} - -/// LossRLEReportBlock is used to report information about packet -/// losses, as described in RFC 3611, section 4.1 -/// make sure to set is_loss_rle = true -pub type LossRLEReportBlock = RLEReportBlock; - -/// DuplicateRLEReportBlock is used to report information about packet -/// duplication, as described in RFC 3611, section 4.1 -/// make sure to set is_loss_rle = false -pub type DuplicateRLEReportBlock = RLEReportBlock; - -impl RLEReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: if self.is_loss_rle { - BlockType::LossRLE - } else { - BlockType::DuplicateRLE - }, - type_specific: self.t & 0x0F, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl fmt::Display for RLEReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl Packet for RLEReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.ssrc] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + RLE_REPORT_BLOCK_MIN_LENGTH as usize + self.chunks.len() * 2 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for RLEReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for RLEReportBlock { - /// marshal_to encodes the RLEReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - buf.put_u16(self.begin_seq); - buf.put_u16(self.end_seq); - for chunk in &self.chunks { - buf.put_u16(chunk.0); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for RLEReportBlock { - /// Unmarshal decodes the RLEReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length < RLE_REPORT_BLOCK_MIN_LENGTH - || (block_length - RLE_REPORT_BLOCK_MIN_LENGTH) % 2 != 0 - || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let is_loss_rle = xr_header.block_type == BlockType::LossRLE; - let t = xr_header.type_specific & 0x0F; - - let ssrc = raw_packet.get_u32(); - let begin_seq = raw_packet.get_u16(); - let end_seq = raw_packet.get_u16(); - - let remaining = block_length - RLE_REPORT_BLOCK_MIN_LENGTH; - let mut chunks = vec![]; - for _ in 0..remaining / 2 { - chunks.push(Chunk(raw_packet.get_u16())); - } - - Ok(RLEReportBlock { - is_loss_rle, - t, - ssrc, - begin_seq, - end_seq, - chunks, - }) - } -} diff --git a/rtcp/src/extended_report/rrt.rs b/rtcp/src/extended_report/rrt.rs deleted file mode 100644 index fa0b34d65..000000000 --- a/rtcp/src/extended_report/rrt.rs +++ /dev/null @@ -1,111 +0,0 @@ -use super::*; - -const RRT_REPORT_BLOCK_LENGTH: u16 = 8; - -/// ReceiverReferenceTimeReportBlock encodes a Receiver Reference Time -/// report block as described in RFC 3611 section 4.4. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT=4 | reserved | block length = 2 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | NTP timestamp, most significant word | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | NTP timestamp, least significant word | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct ReceiverReferenceTimeReportBlock { - pub ntp_timestamp: u64, -} - -impl fmt::Display for ReceiverReferenceTimeReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl ReceiverReferenceTimeReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: BlockType::ReceiverReferenceTime, - type_specific: 0, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for ReceiverReferenceTimeReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + RRT_REPORT_BLOCK_LENGTH as usize - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for ReceiverReferenceTimeReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for ReceiverReferenceTimeReportBlock { - /// marshal_to encodes the ReceiverReferenceTimeReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u64(self.ntp_timestamp); - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for ReceiverReferenceTimeReportBlock { - /// Unmarshal decodes the ReceiverReferenceTimeReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length != RRT_REPORT_BLOCK_LENGTH || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let ntp_timestamp = raw_packet.get_u64(); - - Ok(ReceiverReferenceTimeReportBlock { ntp_timestamp }) - } -} diff --git a/rtcp/src/extended_report/ssr.rs b/rtcp/src/extended_report/ssr.rs deleted file mode 100644 index c4c29e0b5..000000000 --- a/rtcp/src/extended_report/ssr.rs +++ /dev/null @@ -1,235 +0,0 @@ -use super::*; - -const SSR_REPORT_BLOCK_LENGTH: u16 = 4 + 2 * 2 + 4 * 6 + 4; - -/// StatisticsSummaryReportBlock encodes a Statistics Summary Report -/// Block as described in RFC 3611, section 4.6. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT=6 |L|D|J|ToH|rsvd.| block length = 9 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ssrc of source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | begin_seq | end_seq | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | lost_packets | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | dup_packets | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | min_jitter | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | max_jitter | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | mean_jitter | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | dev_jitter | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | min_ttl_or_hl | max_ttl_or_hl |mean_ttl_or_hl | dev_ttl_or_hl | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct StatisticsSummaryReportBlock { - //not included in marshal/unmarshal - pub loss_reports: bool, - pub duplicate_reports: bool, - pub jitter_reports: bool, - pub ttl_or_hop_limit: TTLorHopLimitType, - - //marshal/unmarshal - pub ssrc: u32, - pub begin_seq: u16, - pub end_seq: u16, - pub lost_packets: u32, - pub dup_packets: u32, - pub min_jitter: u32, - pub max_jitter: u32, - pub mean_jitter: u32, - pub dev_jitter: u32, - pub min_ttl_or_hl: u8, - pub max_ttl_or_hl: u8, - pub mean_ttl_or_hl: u8, - pub dev_ttl_or_hl: u8, -} - -impl fmt::Display for StatisticsSummaryReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -/// TTLorHopLimitType encodes values for the ToH field in -/// a StatisticsSummaryReportBlock -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum TTLorHopLimitType { - #[default] - Missing = 0, - IPv4 = 1, - IPv6 = 2, -} - -impl From for TTLorHopLimitType { - fn from(v: u8) -> Self { - match v { - 1 => TTLorHopLimitType::IPv4, - 2 => TTLorHopLimitType::IPv6, - _ => TTLorHopLimitType::Missing, - } - } -} - -impl fmt::Display for TTLorHopLimitType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - TTLorHopLimitType::Missing => "[ToH Missing]", - TTLorHopLimitType::IPv4 => "[ToH = IPv4]", - TTLorHopLimitType::IPv6 => "[ToH = IPv6]", - }; - write!(f, "{s}") - } -} - -impl StatisticsSummaryReportBlock { - pub fn xr_header(&self) -> XRHeader { - let mut type_specific = 0x00; - if self.loss_reports { - type_specific |= 0x80; - } - if self.duplicate_reports { - type_specific |= 0x40; - } - if self.jitter_reports { - type_specific |= 0x20; - } - type_specific |= (self.ttl_or_hop_limit as u8 & 0x03) << 3; - - XRHeader { - block_type: BlockType::StatisticsSummary, - type_specific, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for StatisticsSummaryReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.ssrc] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + SSR_REPORT_BLOCK_LENGTH as usize - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for StatisticsSummaryReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for StatisticsSummaryReportBlock { - /// marshal_to encodes the StatisticsSummaryReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - buf.put_u16(self.begin_seq); - buf.put_u16(self.end_seq); - buf.put_u32(self.lost_packets); - buf.put_u32(self.dup_packets); - buf.put_u32(self.min_jitter); - buf.put_u32(self.max_jitter); - buf.put_u32(self.mean_jitter); - buf.put_u32(self.dev_jitter); - buf.put_u8(self.min_ttl_or_hl); - buf.put_u8(self.max_ttl_or_hl); - buf.put_u8(self.mean_ttl_or_hl); - buf.put_u8(self.dev_ttl_or_hl); - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for StatisticsSummaryReportBlock { - /// Unmarshal decodes the StatisticsSummaryReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length != SSR_REPORT_BLOCK_LENGTH || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let loss_reports = xr_header.type_specific & 0x80 != 0; - let duplicate_reports = xr_header.type_specific & 0x40 != 0; - let jitter_reports = xr_header.type_specific & 0x20 != 0; - let ttl_or_hop_limit: TTLorHopLimitType = ((xr_header.type_specific & 0x18) >> 3).into(); - - let ssrc = raw_packet.get_u32(); - let begin_seq = raw_packet.get_u16(); - let end_seq = raw_packet.get_u16(); - let lost_packets = raw_packet.get_u32(); - let dup_packets = raw_packet.get_u32(); - let min_jitter = raw_packet.get_u32(); - let max_jitter = raw_packet.get_u32(); - let mean_jitter = raw_packet.get_u32(); - let dev_jitter = raw_packet.get_u32(); - let min_ttl_or_hl = raw_packet.get_u8(); - let max_ttl_or_hl = raw_packet.get_u8(); - let mean_ttl_or_hl = raw_packet.get_u8(); - let dev_ttl_or_hl = raw_packet.get_u8(); - - Ok(StatisticsSummaryReportBlock { - loss_reports, - duplicate_reports, - jitter_reports, - ttl_or_hop_limit, - - ssrc, - begin_seq, - end_seq, - lost_packets, - dup_packets, - min_jitter, - max_jitter, - mean_jitter, - dev_jitter, - min_ttl_or_hl, - max_ttl_or_hl, - mean_ttl_or_hl, - dev_ttl_or_hl, - }) - } -} diff --git a/rtcp/src/extended_report/unknown.rs b/rtcp/src/extended_report/unknown.rs deleted file mode 100644 index 707d77231..000000000 --- a/rtcp/src/extended_report/unknown.rs +++ /dev/null @@ -1,98 +0,0 @@ -use super::*; - -/// UnknownReportBlock is used to store bytes for any report block -/// that has an unknown Report Block Type. -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct UnknownReportBlock { - pub bytes: Bytes, -} - -impl fmt::Display for UnknownReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl UnknownReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: BlockType::Unknown, - type_specific: 0, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for UnknownReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + self.bytes.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for UnknownReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for UnknownReportBlock { - /// marshal_to encodes the UnknownReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put(self.bytes.clone()); - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for UnknownReportBlock { - /// Unmarshal decodes the UnknownReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if raw_packet.remaining() < block_length as usize { - return Err(error::Error::PacketTooShort.into()); - } - - let bytes = raw_packet.copy_to_bytes(block_length as usize); - - Ok(UnknownReportBlock { bytes }) - } -} diff --git a/rtcp/src/extended_report/vm.rs b/rtcp/src/extended_report/vm.rs deleted file mode 100644 index 91cb79099..000000000 --- a/rtcp/src/extended_report/vm.rs +++ /dev/null @@ -1,209 +0,0 @@ -use super::*; - -const VM_REPORT_BLOCK_LENGTH: u16 = 4 + 4 + 2 * 4 + 10 + 2 * 3; - -/// VoIPMetricsReportBlock encodes a VoIP Metrics Report Block as described -/// in RFC 3611, section 4.7. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | BT=7 | reserved | block length = 8 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ssrc of source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | loss rate | discard rate | burst density | gap density | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | burst duration | gap duration | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | round trip delay | end system delay | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | signal level | noise level | RERL | Gmin | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | R factor | ext. R factor | MOS-LQ | MOS-CQ | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | RX config | reserved | JB nominal | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | JB maximum | JB abs max | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct VoIPMetricsReportBlock { - pub ssrc: u32, - pub loss_rate: u8, - pub discard_rate: u8, - pub burst_density: u8, - pub gap_density: u8, - pub burst_duration: u16, - pub gap_duration: u16, - pub round_trip_delay: u16, - pub end_system_delay: u16, - pub signal_level: u8, - pub noise_level: u8, - pub rerl: u8, - pub gmin: u8, - pub rfactor: u8, - pub ext_rfactor: u8, - pub mos_lq: u8, - pub mos_cq: u8, - pub rx_config: u8, - pub reserved: u8, - pub jb_nominal: u16, - pub jb_maximum: u16, - pub jb_abs_max: u16, -} - -impl fmt::Display for VoIPMetricsReportBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl VoIPMetricsReportBlock { - pub fn xr_header(&self) -> XRHeader { - XRHeader { - block_type: BlockType::VoIPMetrics, - type_specific: 0, - block_length: (self.raw_size() / 4 - 1) as u16, - } - } -} - -impl Packet for VoIPMetricsReportBlock { - fn header(&self) -> Header { - Header::default() - } - - /// destination_ssrc returns an array of ssrc values that this report block refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.ssrc] - } - - fn raw_size(&self) -> usize { - XR_HEADER_LENGTH + VM_REPORT_BLOCK_LENGTH as usize - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for VoIPMetricsReportBlock { - fn marshal_size(&self) -> usize { - self.raw_size() - } -} - -impl Marshal for VoIPMetricsReportBlock { - /// marshal_to encodes the VoIPMetricsReportBlock in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(error::Error::BufferTooShort.into()); - } - - let h = self.xr_header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - buf.put_u8(self.loss_rate); - buf.put_u8(self.discard_rate); - buf.put_u8(self.burst_density); - buf.put_u8(self.gap_density); - buf.put_u16(self.burst_duration); - buf.put_u16(self.gap_duration); - buf.put_u16(self.round_trip_delay); - buf.put_u16(self.end_system_delay); - buf.put_u8(self.signal_level); - buf.put_u8(self.noise_level); - buf.put_u8(self.rerl); - buf.put_u8(self.gmin); - buf.put_u8(self.rfactor); - buf.put_u8(self.ext_rfactor); - buf.put_u8(self.mos_lq); - buf.put_u8(self.mos_cq); - buf.put_u8(self.rx_config); - buf.put_u8(self.reserved); - buf.put_u16(self.jb_nominal); - buf.put_u16(self.jb_maximum); - buf.put_u16(self.jb_abs_max); - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for VoIPMetricsReportBlock { - /// Unmarshal decodes the VoIPMetricsReportBlock from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < XR_HEADER_LENGTH { - return Err(error::Error::PacketTooShort.into()); - } - - let xr_header = XRHeader::unmarshal(raw_packet)?; - let block_length = xr_header.block_length * 4; - if block_length != VM_REPORT_BLOCK_LENGTH || raw_packet.remaining() < block_length as usize - { - return Err(error::Error::PacketTooShort.into()); - } - - let ssrc = raw_packet.get_u32(); - let loss_rate = raw_packet.get_u8(); - let discard_rate = raw_packet.get_u8(); - let burst_density = raw_packet.get_u8(); - let gap_density = raw_packet.get_u8(); - let burst_duration = raw_packet.get_u16(); - let gap_duration = raw_packet.get_u16(); - let round_trip_delay = raw_packet.get_u16(); - let end_system_delay = raw_packet.get_u16(); - let signal_level = raw_packet.get_u8(); - let noise_level = raw_packet.get_u8(); - let rerl = raw_packet.get_u8(); - let gmin = raw_packet.get_u8(); - let rfactor = raw_packet.get_u8(); - let ext_rfactor = raw_packet.get_u8(); - let mos_lq = raw_packet.get_u8(); - let mos_cq = raw_packet.get_u8(); - let rx_config = raw_packet.get_u8(); - let reserved = raw_packet.get_u8(); - let jb_nominal = raw_packet.get_u16(); - let jb_maximum = raw_packet.get_u16(); - let jb_abs_max = raw_packet.get_u16(); - - Ok(VoIPMetricsReportBlock { - ssrc, - loss_rate, - discard_rate, - burst_density, - gap_density, - burst_duration, - gap_duration, - round_trip_delay, - end_system_delay, - signal_level, - noise_level, - rerl, - gmin, - rfactor, - ext_rfactor, - mos_lq, - mos_cq, - rx_config, - reserved, - jb_nominal, - jb_maximum, - jb_abs_max, - }) - } -} diff --git a/rtcp/src/goodbye/goodbye_test.rs b/rtcp/src/goodbye/goodbye_test.rs deleted file mode 100644 index 22e3e42bf..000000000 --- a/rtcp/src/goodbye/goodbye_test.rs +++ /dev/null @@ -1,225 +0,0 @@ -use super::*; - -#[test] -fn test_goodbye_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x81, 0xcb, 0x00, 0x0c, // v=2, p=0, count=1, BYE, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x03, 0x46, 0x4f, 0x4f, // len=3, text=FOO - ]), - Goodbye { - sources: vec![0x902f9e2e], - reason: Bytes::from_static(b"FOO"), - }, - None, - ), - ( - "invalid octet count", - Bytes::from_static(&[ - 0x81, 0xcb, 0x00, 0x0c, // v=2, p=0, count=1, BYE, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x04, 0x46, 0x4f, 0x4f, // len=4, text=FOO - ]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - Some(Error::PacketTooShort), - ), - ( - "wrong type", - Bytes::from_static(&[ - 0x81, 0xca, 0x00, 0x0c, // v=2, p=0, count=1, SDES, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x03, 0x46, 0x4f, 0x4f, // len=3, text=FOO - ]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - Some(Error::WrongType), - ), - ( - "short reason", - Bytes::from_static(&[ - 0x81, 0xcb, 0x00, 0x0c, // v=2, p=0, count=1, BYE, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x01, 0x46, 0x00, 0x00, // len=3, text=F + padding - ]), - Goodbye { - sources: vec![0x902f9e2e], - reason: Bytes::from_static(b"F"), - }, - None, - ), - ( - "not byte aligned", - Bytes::from_static(&[ - 0x81, 0xcb, 0x00, 0x0a, // v=2, p=0, count=1, BYE, len=10 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x01, 0x46, // len=1, text=F - ]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - Some(Error::PacketTooShort), - ), - ( - "bad count in header", - Bytes::from_static(&[ - 0x82, 0xcb, 0x00, 0x0c, // v=2, p=0, count=2, BYE, len=8 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - ]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - Some(Error::PacketTooShort), - ), - ( - "empty packet", - Bytes::from_static(&[ - // v=2, p=0, count=0, BYE, len=4 - 0x80, 0xcb, 0x00, 0x04, - ]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - None, - ), - ( - "nil", - Bytes::from_static(&[]), - Goodbye { - sources: vec![], - reason: Bytes::from_static(b""), - }, - Some(Error::PacketTooShort), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = Goodbye::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} bye: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_goodbye_round_trip() { - let too_many_sources = vec![0u32; 1 << 5]; - - let mut too_long_text = String::new(); - for _ in 0..1 << 8 { - too_long_text.push('x'); - } - - let tests = vec![ - ( - "empty", - Goodbye { - sources: vec![], - ..Default::default() - }, - None, - ), - ( - "valid", - Goodbye { - sources: vec![0x01020304, 0x05060708], - reason: Bytes::from_static(b"because"), - }, - None, - ), - ( - "empty reason", - Goodbye { - sources: vec![0x01020304], - reason: Bytes::from_static(b""), - }, - None, - ), - ( - "reason no source", - Goodbye { - sources: vec![], - reason: Bytes::from_static(b"foo"), - }, - None, - ), - ( - "short reason", - Goodbye { - sources: vec![], - reason: Bytes::from_static(b"f"), - }, - None, - ), - ( - "count overflow", - Goodbye { - sources: too_many_sources, - reason: Bytes::from_static(b""), - }, - Some(Error::TooManySources), - ), - ( - "reason too long", - Goodbye { - sources: vec![], - reason: Bytes::copy_from_slice(too_long_text.as_bytes()), - }, - Some(Error::ReasonTooLong), - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = - Goodbye::unmarshal(&mut data).unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/goodbye/mod.rs b/rtcp/src/goodbye/mod.rs deleted file mode 100644 index 9fa44fd77..000000000 --- a/rtcp/src/goodbye/mod.rs +++ /dev/null @@ -1,197 +0,0 @@ -#[cfg(test)] -mod goodbye_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -/// The Goodbye packet indicates that one or more sources are no longer active. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct Goodbye { - /// The SSRC/CSRC identifiers that are no longer active - pub sources: Vec, - /// Optional text indicating the reason for leaving, e.g., "camera malfunction" or "RTP loop detected" - pub reason: Bytes, -} - -impl fmt::Display for Goodbye { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = "Goodbye:\n\tSources:\n".to_string(); - for s in &self.sources { - out += format!("\t{}\n", *s).as_str(); - } - out += format!("\tReason: {:?}\n", self.reason).as_str(); - - write!(f, "{out}") - } -} - -impl Packet for Goodbye { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: self.sources.len() as u8, - packet_type: PacketType::Goodbye, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - self.sources.to_vec() - } - - fn raw_size(&self) -> usize { - let srcs_length = self.sources.len() * SSRC_LENGTH; - let reason_length = self.reason.len() + 1; - - HEADER_LENGTH + srcs_length + reason_length - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for Goodbye { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for Goodbye { - /// marshal_to encodes the packet in binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.sources.len() > COUNT_MAX { - return Err(Error::TooManySources.into()); - } - - if self.reason.len() > SDES_MAX_OCTET_COUNT { - return Err(Error::ReasonTooLong.into()); - } - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P| SC | PT=BYE=203 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC/CSRC | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * (opt) | length | reason for leaving ... - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - for source in &self.sources { - buf.put_u32(*source); - } - - buf.put_u8(self.reason.len() as u8); - if !self.reason.is_empty() { - buf.put(self.reason.clone()); - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for Goodbye { - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P| SC | PT=BYE=203 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC/CSRC | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * (opt) | length | reason for leaving ... - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let raw_packet_len = raw_packet.remaining(); - - let header = Header::unmarshal(raw_packet)?; - if header.packet_type != PacketType::Goodbye { - return Err(Error::WrongType.into()); - } - - if get_padding_size(raw_packet_len) != 0 { - return Err(Error::PacketTooShort.into()); - } - - let reason_offset = HEADER_LENGTH + header.count as usize * SSRC_LENGTH; - - if reason_offset > raw_packet_len { - return Err(Error::PacketTooShort.into()); - } - - let mut sources = Vec::with_capacity(header.count as usize); - for _ in 0..header.count { - sources.push(raw_packet.get_u32()); - } - - let reason = if reason_offset < raw_packet_len { - let reason_len = raw_packet.get_u8() as usize; - let reason_end = reason_offset + 1 + reason_len; - - if reason_end > raw_packet_len { - return Err(Error::PacketTooShort.into()); - } - - raw_packet.copy_to_bytes(reason_len) - } else { - Bytes::new() - }; - - if - /*header.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(Goodbye { sources, reason }) - } -} diff --git a/rtcp/src/header.rs b/rtcp/src/header.rs deleted file mode 100644 index 508f87278..000000000 --- a/rtcp/src/header.rs +++ /dev/null @@ -1,315 +0,0 @@ -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -/// PacketType specifies the type of an RTCP packet -/// RTCP packet types registered with IANA. See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-4 -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[repr(u8)] -pub enum PacketType { - #[default] - Unsupported = 0, - SenderReport = 200, // RFC 3550, 6.4.1 - ReceiverReport = 201, // RFC 3550, 6.4.2 - SourceDescription = 202, // RFC 3550, 6.5 - Goodbye = 203, // RFC 3550, 6.6 - ApplicationDefined = 204, // RFC 3550, 6.7 (unimplemented) - TransportSpecificFeedback = 205, // RFC 4585, 6051 - PayloadSpecificFeedback = 206, // RFC 4585, 6.3 - ExtendedReport = 207, // RFC 3611 -} - -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_SLI: u8 = 2; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_PLI: u8 = 1; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_FIR: u8 = 4; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_TLN: u8 = 1; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_RRR: u8 = 5; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here -pub const FORMAT_REMB: u8 = 15; -/// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here. -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 -pub const FORMAT_TCC: u8 = 15; - -impl std::fmt::Display for PacketType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - PacketType::Unsupported => "Unsupported", - PacketType::SenderReport => "SR", - PacketType::ReceiverReport => "RR", - PacketType::SourceDescription => "SDES", - PacketType::Goodbye => "BYE", - PacketType::ApplicationDefined => "APP", - PacketType::TransportSpecificFeedback => "TSFB", - PacketType::PayloadSpecificFeedback => "PSFB", - PacketType::ExtendedReport => "XR", - }; - write!(f, "{s}") - } -} - -impl From for PacketType { - fn from(b: u8) -> Self { - match b { - 200 => PacketType::SenderReport, // RFC 3550, 6.4.1 - 201 => PacketType::ReceiverReport, // RFC 3550, 6.4.2 - 202 => PacketType::SourceDescription, // RFC 3550, 6.5 - 203 => PacketType::Goodbye, // RFC 3550, 6.6 - 204 => PacketType::ApplicationDefined, // RFC 3550, 6.7 (unimplemented) - 205 => PacketType::TransportSpecificFeedback, // RFC 4585, 6051 - 206 => PacketType::PayloadSpecificFeedback, // RFC 4585, 6.3 - 207 => PacketType::ExtendedReport, // RFC 3611 - _ => PacketType::Unsupported, - } - } -} - -pub const RTP_VERSION: u8 = 2; -pub const VERSION_SHIFT: u8 = 6; -pub const VERSION_MASK: u8 = 0x3; -pub const PADDING_SHIFT: u8 = 5; -pub const PADDING_MASK: u8 = 0x1; -pub const COUNT_SHIFT: u8 = 0; -pub const COUNT_MASK: u8 = 0x1f; - -pub const HEADER_LENGTH: usize = 4; -pub const COUNT_MAX: usize = (1 << 5) - 1; -pub const SSRC_LENGTH: usize = 4; -pub const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; - -/// A Header is the common header shared by all RTCP packets -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct Header { - /// If the padding bit is set, this individual RTCP packet contains - /// some additional padding octets at the end which are not part of - /// the control information but are included in the length field. - pub padding: bool, - /// The number of reception reports, sources contained or FMT in this packet (depending on the Type) - pub count: u8, - /// The RTCP packet type for this packet - pub packet_type: PacketType, - /// The length of this RTCP packet in 32-bit words minus one, - /// including the header and any padding. - pub length: u16, -} - -/// Marshal encodes the Header in binary -impl MarshalSize for Header { - fn marshal_size(&self) -> usize { - HEADER_LENGTH - } -} - -impl Marshal for Header { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.count > 31 { - return Err(Error::InvalidHeader.into()); - } - if buf.remaining_mut() < HEADER_LENGTH { - return Err(Error::BufferTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P| RC | PT=SR=200 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let b0 = (RTP_VERSION << VERSION_SHIFT) - | ((self.padding as u8) << PADDING_SHIFT) - | (self.count << COUNT_SHIFT); - - buf.put_u8(b0); - buf.put_u8(self.packet_type as u8); - buf.put_u16(self.length); - - Ok(HEADER_LENGTH) - } -} - -impl Unmarshal for Header { - /// Unmarshal decodes the Header from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < HEADER_LENGTH { - return Err(Error::PacketTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P| RC | PT | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let b0 = raw_packet.get_u8(); - let version = (b0 >> VERSION_SHIFT) & VERSION_MASK; - if version != RTP_VERSION { - return Err(Error::BadVersion.into()); - } - - let padding = ((b0 >> PADDING_SHIFT) & PADDING_MASK) > 0; - let count = (b0 >> COUNT_SHIFT) & COUNT_MASK; - let packet_type = PacketType::from(raw_packet.get_u8()); - let length = raw_packet.get_u16(); - - Ok(Header { - padding, - count, - packet_type, - length, - }) - } -} - -#[cfg(test)] -mod test { - use bytes::Bytes; - - use super::*; - - #[test] - fn test_header_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - // v=2, p=0, count=1, RR, len=7 - 0x81u8, 0xc9, 0x00, 0x07, - ]), - Header { - padding: false, - count: 1, - packet_type: PacketType::ReceiverReport, - length: 7, - }, - None, - ), - ( - "also valid", - Bytes::from_static(&[ - // v=2, p=1, count=1, BYE, len=7 - 0xa1, 0xcc, 0x00, 0x07, - ]), - Header { - padding: true, - count: 1, - packet_type: PacketType::ApplicationDefined, - length: 7, - }, - None, - ), - ( - "bad version", - Bytes::from_static(&[ - // v=0, p=0, count=0, RR, len=4 - 0x00, 0xc9, 0x00, 0x04, - ]), - Header { - padding: false, - count: 0, - packet_type: PacketType::Unsupported, - length: 0, - }, - Some(Error::BadVersion), - ), - ]; - - for (name, data, want, want_error) in tests { - let buf = &mut data.clone(); - let got = Header::unmarshal(buf); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(want_error) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - want_error, got_err, - "Unmarshal {name}: err = {got_err:?}, want {want_error:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name}: got {actual:?}, want {want:?}" - ); - } - } - } - - #[test] - fn test_header_roundtrip() { - let tests = vec![ - ( - "valid", - Header { - padding: true, - count: 31, - packet_type: PacketType::SenderReport, - length: 4, - }, - None, - ), - ( - "also valid", - Header { - padding: false, - count: 28, - packet_type: PacketType::ReceiverReport, - length: 65535, - }, - None, - ), - ( - "invalid count", - Header { - padding: false, - count: 40, - packet_type: PacketType::Unsupported, - length: 0, - }, - Some(Error::InvalidHeader), - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let data = got.ok().unwrap(); - let buf = &mut data.clone(); - let actual = Header::unmarshal(buf).unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } - } -} diff --git a/rtcp/src/lib.rs b/rtcp/src/lib.rs deleted file mode 100644 index 4d99b09d5..000000000 --- a/rtcp/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -//! Package rtcp implements encoding and decoding of RTCP packets according to RFCs 3550 and 5506. -//! -//! RTCP is a sister protocol of the Real-time Transport Protocol (RTP). Its basic functionality -//! and packet structure is defined in RFC 3550. RTCP provides out-of-band statistics and control -//! information for an RTP session. It partners with RTP in the delivery and packaging of multimedia data, -//! but does not transport any media data itself. -//! -//! The primary function of RTCP is to provide feedback on the quality of service (QoS) -//! in media distribution by periodically sending statistics information such as transmitted octet -//! and packet counts, packet loss, packet delay variation, and round-trip delay time to participants -//! in a streaming multimedia session. An application may use this information to control quality of -//! service parameters, perhaps by limiting flow, or using a different codec. -//! -//! Decoding RTCP packets: -//!```nobuild -//! let pkt = rtcp::unmarshal(&rtcp_data).unwrap(); -//! -//! if let Some(e) = pkt -//! .as_any() -//! .downcast_ref::() -//! { -//! -//! } -//! else if let Some(e) = packet -//! .as_any() -//! .downcast_ref::(){} -//! .... -//!``` -//! -//! Encoding RTCP packets: -//!```nobuild -//! let pkt = PictureLossIndication{ -//! sender_ssrc: sender_ssrc, -//! media_ssrc: media_ssrc -//! }; -//! -//! let pli_data = pkt.marshal().unwrap(); -//! // ... -//!``` - -pub mod compound_packet; -mod error; -pub mod extended_report; -pub mod goodbye; -pub mod header; -pub mod packet; -pub mod payload_feedbacks; -pub mod raw_packet; -pub mod receiver_report; -pub mod reception_report; -pub mod sender_report; -pub mod source_description; -pub mod transport_feedbacks; -mod util; - -pub use error::Error; diff --git a/rtcp/src/packet.rs b/rtcp/src/packet.rs deleted file mode 100644 index 0a2aff805..000000000 --- a/rtcp/src/packet.rs +++ /dev/null @@ -1,276 +0,0 @@ -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use util::marshal::{Marshal, Unmarshal}; - -use crate::error::{Error, Result}; -use crate::extended_report::ExtendedReport; -use crate::goodbye::*; -use crate::header::*; -use crate::payload_feedbacks::full_intra_request::*; -use crate::payload_feedbacks::picture_loss_indication::*; -use crate::payload_feedbacks::receiver_estimated_maximum_bitrate::*; -use crate::payload_feedbacks::slice_loss_indication::*; -use crate::raw_packet::*; -use crate::receiver_report::*; -use crate::sender_report::*; -use crate::source_description::*; -use crate::transport_feedbacks::rapid_resynchronization_request::*; -use crate::transport_feedbacks::transport_layer_cc::*; -use crate::transport_feedbacks::transport_layer_nack::*; - -/// Packet represents an RTCP packet, a protocol used for out-of-band statistics and -/// control information for an RTP session -pub trait Packet: Marshal + Unmarshal + fmt::Display + fmt::Debug { - fn header(&self) -> Header; - fn destination_ssrc(&self) -> Vec; - fn raw_size(&self) -> usize; - fn as_any(&self) -> &(dyn Any + Send + Sync); - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool; - fn cloned(&self) -> Box; -} - -impl PartialEq for dyn Packet + Send + Sync { - fn eq(&self, other: &Self) -> bool { - self.equal(other) - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.cloned() - } -} - -/// marshal takes an array of Packets and serializes them to a single buffer -pub fn marshal(packets: &[Box]) -> Result { - let mut out = BytesMut::new(); - for p in packets { - let data = p.marshal()?; - out.put(data); - } - Ok(out.freeze()) -} - -/// Unmarshal takes an entire udp datagram (which may consist of multiple RTCP packets) and -/// returns the unmarshaled packets it contains. -/// -/// If this is a reduced-size RTCP packet a feedback packet (Goodbye, SliceLossIndication, etc) -/// will be returned. Otherwise, the underlying type of the returned packet will be -/// CompoundPacket. -pub fn unmarshal(raw_data: &mut B) -> Result>> -where - B: Buf, -{ - let mut packets = vec![]; - - while raw_data.has_remaining() { - let p = unmarshaller(raw_data)?; - packets.push(p); - } - - match packets.len() { - // Empty Packet - 0 => Err(Error::InvalidHeader), - - // Multiple Packet - _ => Ok(packets), - } -} - -/// unmarshaller is a factory which pulls the first RTCP packet from a bytestream, -/// and returns it's parsed representation, and the amount of data that was processed. -pub(crate) fn unmarshaller(raw_data: &mut B) -> Result> -where - B: Buf, -{ - let h = Header::unmarshal(raw_data)?; - - let length = (h.length as usize) * 4; - if length > raw_data.remaining() { - return Err(Error::PacketTooShort); - } - - let mut in_packet = h.marshal()?.chain(raw_data.take(length)); - - let p: Box = match h.packet_type { - PacketType::SenderReport => Box::new(SenderReport::unmarshal(&mut in_packet)?), - PacketType::ReceiverReport => Box::new(ReceiverReport::unmarshal(&mut in_packet)?), - PacketType::SourceDescription => Box::new(SourceDescription::unmarshal(&mut in_packet)?), - PacketType::Goodbye => Box::new(Goodbye::unmarshal(&mut in_packet)?), - - PacketType::TransportSpecificFeedback => match h.count { - FORMAT_TLN => Box::new(TransportLayerNack::unmarshal(&mut in_packet)?), - FORMAT_RRR => Box::new(RapidResynchronizationRequest::unmarshal(&mut in_packet)?), - FORMAT_TCC => Box::new(TransportLayerCc::unmarshal(&mut in_packet)?), - _ => Box::new(RawPacket::unmarshal(&mut in_packet)?), - }, - PacketType::PayloadSpecificFeedback => match h.count { - FORMAT_PLI => Box::new(PictureLossIndication::unmarshal(&mut in_packet)?), - FORMAT_SLI => Box::new(SliceLossIndication::unmarshal(&mut in_packet)?), - FORMAT_REMB => Box::new(ReceiverEstimatedMaximumBitrate::unmarshal(&mut in_packet)?), - FORMAT_FIR => Box::new(FullIntraRequest::unmarshal(&mut in_packet)?), - _ => Box::new(RawPacket::unmarshal(&mut in_packet)?), - }, - PacketType::ExtendedReport => Box::new(ExtendedReport::unmarshal(&mut in_packet)?), - _ => Box::new(RawPacket::unmarshal(&mut in_packet)?), - }; - - Ok(p) -} - -#[cfg(test)] -mod test { - use bytes::Bytes; - - use super::*; - use crate::reception_report::*; - - #[test] - fn test_packet_unmarshal() { - let mut data = Bytes::from_static(&[ - // Receiver Report (offset=0) - 0x81, 0xc9, 0x0, 0x7, // v=2, p=0, count=1, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - // Source Description (offset=32) - 0x81, 0xca, 0x0, 0xc, // v=2, p=0, count=1, SDES, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x1, 0x26, // CNAME, len=38 - 0x7b, 0x39, 0x63, 0x30, 0x30, 0x65, 0x62, 0x39, 0x32, 0x2d, 0x31, 0x61, 0x66, 0x62, - 0x2d, 0x39, 0x64, 0x34, 0x39, 0x2d, 0x61, 0x34, 0x37, 0x64, 0x2d, 0x39, 0x31, 0x66, - 0x36, 0x34, 0x65, 0x65, 0x65, 0x36, 0x39, 0x66, 0x35, - 0x7d, // text="{9c00eb92-1afb-9d49-a47d-91f64eee69f5}" - 0x0, 0x0, 0x0, 0x0, // END + padding - // Goodbye (offset=84) - 0x81, 0xcb, 0x0, 0x1, // v=2, p=0, count=1, BYE, len=1 - 0x90, 0x2f, 0x9e, 0x2e, // source=0x902f9e2e - 0x81, 0xce, 0x0, 0x2, // Picture Loss Indication (offset=92) - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - 0x85, 0xcd, 0x0, 0x2, // RapidResynchronizationRequest (offset=104) - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - ]); - - let packet = unmarshal(&mut data).expect("Error unmarshalling packets"); - - let a = ReceiverReport { - ssrc: 0x902f9e2e, - reports: vec![ReceptionReport { - ssrc: 0xbc5e9a40, - fraction_lost: 0, - total_lost: 0, - last_sequence_number: 0x46e1, - jitter: 273, - last_sender_report: 0x9f36432, - delay: 150137, - }], - ..Default::default() - }; - - let b = SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 0x902f9e2e, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"{9c00eb92-1afb-9d49-a47d-91f64eee69f5}"), - }], - }], - }; - - let c = Goodbye { - sources: vec![0x902f9e2e], - ..Default::default() - }; - - let d = PictureLossIndication { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - }; - - let e = RapidResynchronizationRequest { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - }; - - let expected: Vec> = vec![ - Box::new(a), - Box::new(b), - Box::new(c), - Box::new(d), - Box::new(e), - ]; - - assert!(packet == expected, "Invalid packets"); - } - - #[test] - fn test_packet_unmarshal_empty() -> Result<()> { - let result = unmarshal(&mut Bytes::new()); - if let Err(got) = result { - let want = Error::InvalidHeader; - assert_eq!(got, want, "Unmarshal(nil) err = {got}, want {want}"); - } else { - panic!("want error"); - } - - Ok(()) - } - - #[test] - fn test_packet_invalid_header_length() -> Result<()> { - let mut data = Bytes::from_static(&[ - // Goodbye (offset=84) - // v=2, p=0, count=1, BYE, len=100 - 0x81, 0xcb, 0x0, 0x64, - ]); - - let result = unmarshal(&mut data); - if let Err(got) = result { - let want = Error::PacketTooShort; - assert_eq!( - got, want, - "Unmarshal(invalid_header_length) err = {got}, want {want}" - ); - } else { - panic!("want error"); - } - - Ok(()) - } - #[test] - fn test_packet_unmarshal_firefox() -> Result<()> { - // issue report from https://github.com/webrtc-rs/srtp/issues/7 - let tests = vec![ - Bytes::from_static(&[ - 143, 205, 0, 6, 65, 227, 184, 49, 118, 243, 78, 96, 42, 63, 0, 5, 12, 162, 166, 0, - 32, 5, 200, 4, 0, 4, 0, 0, - ]), - Bytes::from_static(&[ - 143, 205, 0, 9, 65, 227, 184, 49, 118, 243, 78, 96, 42, 68, 0, 17, 12, 162, 167, 1, - 32, 17, 88, 0, 4, 0, 4, 8, 108, 0, 4, 0, 4, 12, 0, 4, 0, 4, 4, 0, - ]), - Bytes::from_static(&[ - 143, 205, 0, 8, 65, 227, 184, 49, 118, 243, 78, 96, 42, 91, 0, 12, 12, 162, 168, 3, - 32, 12, 220, 4, 0, 4, 0, 8, 128, 4, 0, 4, 0, 8, 0, 0, - ]), - Bytes::from_static(&[ - 143, 205, 0, 7, 65, 227, 184, 49, 118, 243, 78, 96, 42, 103, 0, 8, 12, 162, 169, 4, - 32, 8, 232, 4, 0, 4, 0, 4, 4, 0, 0, 0, - ]), - ]; - - for mut test in tests { - unmarshal(&mut test)?; - } - - Ok(()) - } -} diff --git a/rtcp/src/payload_feedbacks/full_intra_request/full_intra_request_test.rs b/rtcp/src/payload_feedbacks/full_intra_request/full_intra_request_test.rs deleted file mode 100644 index 2d0dce6e2..000000000 --- a/rtcp/src/payload_feedbacks/full_intra_request/full_intra_request_test.rs +++ /dev/null @@ -1,215 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_full_intra_request_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x84, 0xce, 0x00, 0x03, // v=2, p=0, FMT=4, PSFB, len=3 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - 0x12, 0x34, 0x56, 0x78, // ssrc=0x12345678 - 0x42, 0x00, 0x00, 0x00, // Seqno=0x42 - ]), - FullIntraRequest { - sender_ssrc: 0x0, - media_ssrc: 0x4bc4fcb4, - fir: vec![FirEntry { - ssrc: 0x12345678, - sequence_number: 0x42, - }], - }, - None, - ), - ( - "also valid", - Bytes::from_static(&[ - 0x84, 0xce, 0x00, 0x05, // v=2, p=0, FMT=4, PSFB, len=3 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - 0x12, 0x34, 0x56, 0x78, // ssrc=0x12345678 - 0x42, 0x00, 0x00, 0x00, // Seqno=0x42 - 0x98, 0x76, 0x54, 0x32, // ssrc=0x98765432 - 0x57, 0x00, 0x00, 0x00, // Seqno=0x57 - ]), - FullIntraRequest { - sender_ssrc: 0x0, - media_ssrc: 0x4bc4fcb4, - fir: vec![ - FirEntry { - ssrc: 0x12345678, - sequence_number: 0x42, - }, - FirEntry { - ssrc: 0x98765432, - sequence_number: 0x57, - }, - ], - }, - None, - ), - ( - "packet too short", - Bytes::from_static(&[0x00, 0x00, 0x00, 0x00]), - FullIntraRequest::default(), - Some(Error::PacketTooShort), - ), - ( - "invalid header", - Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]), - FullIntraRequest::default(), - Some(Error::BadVersion), - ), - ( - "wrong type", - Bytes::from_static(&[ - 0x84, 0xc9, 0x00, 0x03, // v=2, p=0, FMT=4, RR, len=3 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - 0x12, 0x34, 0x56, 0x78, // ssrc=0x12345678 - 0x42, 0x00, 0x00, 0x00, // Seqno=0x42 - ]), - FullIntraRequest::default(), - Some(Error::WrongType), - ), - ( - "wrong fmt", - Bytes::from_static(&[ - 0x82, 0xce, 0x00, 0x03, // v=2, p=0, FMT=2, PSFB, len=3 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - 0x12, 0x34, 0x56, 0x78, // ssrc=0x12345678 - 0x42, 0x00, 0x00, 0x00, // Seqno=0x42 - ]), - FullIntraRequest::default(), - Some(Error::WrongType), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = FullIntraRequest::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} rr: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_full_intra_request_round_trip() { - let tests: Vec<(&str, FullIntraRequest, Option)> = vec![ - ( - "valid", - FullIntraRequest { - sender_ssrc: 1, - media_ssrc: 2, - fir: vec![FirEntry { - ssrc: 3, - sequence_number: 42, - }], - }, - None, - ), - ( - "also valid", - FullIntraRequest { - sender_ssrc: 5000, - media_ssrc: 6000, - fir: vec![FirEntry { - ssrc: 3, - sequence_number: 57, - }], - }, - None, - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = FullIntraRequest::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} - -#[test] -fn test_full_intra_request_unmarshal_header() { - let tests = vec![( - "valid header", - Bytes::from_static(&[ - 0x84, 0xce, 0x00, 0x02, // v=2, p=0, FMT=1, PSFB, len=1 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 - ]), - Header { - count: FORMAT_FIR, - packet_type: PacketType::PayloadSpecificFeedback, - length: 2, - ..Default::default() - }, - )]; - - for (name, mut data, want) in tests { - let result = FullIntraRequest::unmarshal(&mut data); - - assert!( - result.is_ok(), - "Unmarshal header {name} rr: want {result:?}", - ); - - match result { - Err(_) => continue, - - Ok(fir) => { - let h = fir.header(); - - assert_eq!( - h, want, - "Unmarshal header {name} rr: got {h:?}, want {want:?}" - ) - } - } - } -} diff --git a/rtcp/src/payload_feedbacks/full_intra_request/mod.rs b/rtcp/src/payload_feedbacks/full_intra_request/mod.rs deleted file mode 100644 index fa8440889..000000000 --- a/rtcp/src/payload_feedbacks/full_intra_request/mod.rs +++ /dev/null @@ -1,172 +0,0 @@ -#[cfg(test)] -mod full_intra_request_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -/// A FIREntry is a (ssrc, seqno) pair, as carried by FullIntraRequest. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct FirEntry { - pub ssrc: u32, - pub sequence_number: u8, -} - -/// The FullIntraRequest packet is used to reliably request an Intra frame -/// in a video stream. See RFC 5104 Section 3.5.1. This is not for loss -/// recovery, which should use PictureLossIndication (PLI) instead. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct FullIntraRequest { - pub sender_ssrc: u32, - pub media_ssrc: u32, - pub fir: Vec, -} - -const FIR_OFFSET: usize = 8; - -impl fmt::Display for FullIntraRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = format!("FullIntraRequest {} {}", self.sender_ssrc, self.media_ssrc); - for e in &self.fir { - out += format!(" ({} {})", e.ssrc, e.sequence_number).as_str(); - } - write!(f, "{out}") - } -} - -impl Packet for FullIntraRequest { - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_FIR, - packet_type: PacketType::PayloadSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - let mut ssrcs: Vec = Vec::with_capacity(self.fir.len()); - for entry in &self.fir { - ssrcs.push(entry.ssrc); - } - ssrcs - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + FIR_OFFSET + self.fir.len() * 8 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for FullIntraRequest { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for FullIntraRequest { - /// Marshal encodes the FullIntraRequest - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - - for fir in self.fir.iter() { - buf.put_u32(fir.ssrc); - buf.put_u8(fir.sequence_number); - buf.put_u8(0); - buf.put_u16(0); - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for FullIntraRequest { - /// Unmarshal decodes the FullIntraRequest - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - if raw_packet_len < (HEADER_LENGTH + (4 * h.length) as usize) { - return Err(Error::PacketTooShort.into()); - } - - if h.packet_type != PacketType::PayloadSpecificFeedback || h.count != FORMAT_FIR { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - - let mut i = HEADER_LENGTH + FIR_OFFSET; - let mut fir = vec![]; - while i < HEADER_LENGTH + (h.length * 4) as usize { - fir.push(FirEntry { - ssrc: raw_packet.get_u32(), - sequence_number: raw_packet.get_u8(), - }); - raw_packet.get_u8(); - raw_packet.get_u16(); - - i += 8; - } - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(FullIntraRequest { - sender_ssrc, - media_ssrc, - fir, - }) - } -} diff --git a/rtcp/src/payload_feedbacks/mod.rs b/rtcp/src/payload_feedbacks/mod.rs deleted file mode 100644 index e7d02f177..000000000 --- a/rtcp/src/payload_feedbacks/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod full_intra_request; -pub mod picture_loss_indication; -pub mod receiver_estimated_maximum_bitrate; -pub mod slice_loss_indication; diff --git a/rtcp/src/payload_feedbacks/picture_loss_indication/mod.rs b/rtcp/src/payload_feedbacks/picture_loss_indication/mod.rs deleted file mode 100644 index 1092e0afc..000000000 --- a/rtcp/src/payload_feedbacks/picture_loss_indication/mod.rs +++ /dev/null @@ -1,141 +0,0 @@ -#[cfg(test)] -mod picture_loss_indication_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -const PLI_LENGTH: usize = 2; - -/// The PictureLossIndication packet informs the encoder about the loss of an undefined amount of coded video data belonging to one or more pictures -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct PictureLossIndication { - /// SSRC of sender - pub sender_ssrc: u32, - /// SSRC where the loss was experienced - pub media_ssrc: u32, -} - -impl fmt::Display for PictureLossIndication { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "PictureLossIndication {:x} {:x}", - self.sender_ssrc, self.media_ssrc - ) - } -} - -impl Packet for PictureLossIndication { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_PLI, - packet_type: PacketType::PayloadSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.media_ssrc] - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + SSRC_LENGTH * 2 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for PictureLossIndication { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for PictureLossIndication { - /// Marshal encodes the PictureLossIndication in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - /* - * PLI does not require parameters. Therefore, the length field MUST be - * 2, and there MUST NOT be any Feedback Control Information. - * - * The semantics of this FB message is independent of the payload type. - */ - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for PictureLossIndication { - /// Unmarshal decodes the PictureLossIndication from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + (SSRC_LENGTH * 2)) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - if h.packet_type != PacketType::PayloadSpecificFeedback || h.count != FORMAT_PLI { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(PictureLossIndication { - sender_ssrc, - media_ssrc, - }) - } -} diff --git a/rtcp/src/payload_feedbacks/picture_loss_indication/picture_loss_indication_test.rs b/rtcp/src/payload_feedbacks/picture_loss_indication/picture_loss_indication_test.rs deleted file mode 100644 index c40a8984b..000000000 --- a/rtcp/src/payload_feedbacks/picture_loss_indication/picture_loss_indication_test.rs +++ /dev/null @@ -1,162 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_picture_loss_indication_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x81, 0xce, 0x00, 0x02, // v=2, p=0, FMT=1, PSFB, len=1 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - ]), - PictureLossIndication { - sender_ssrc: 0x0, - media_ssrc: 0x4bc4fcb4, - }, - None, - ), - ( - "packet too short", - Bytes::from_static(&[0x81, 0xce, 0x00, 0x00]), - PictureLossIndication::default(), - Some(Error::PacketTooShort), - ), - ( - "invalid header", - Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]), - PictureLossIndication::default(), - Some(Error::BadVersion), - ), - ( - "wrong type", - Bytes::from_static(&[ - 0x81, 0xc9, 0x00, 0x02, // v=2, p=0, FMT=1, RR, len=1 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - ]), - PictureLossIndication::default(), - Some(Error::WrongType), - ), - ( - "wrong fmt", - Bytes::from_static(&[ - 0x82, 0xc9, 0x00, 0x02, // v=2, p=0, FMT=2, RR, len=1 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - ]), - PictureLossIndication::default(), - Some(Error::WrongType), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = PictureLossIndication::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} rr: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_picture_loss_indication_roundtrip() { - let tests: Vec<(&str, PictureLossIndication, Option)> = vec![ - ( - "valid", - PictureLossIndication { - sender_ssrc: 1, - media_ssrc: 2, - }, - None, - ), - ( - "also valid", - PictureLossIndication { - sender_ssrc: 5000, - media_ssrc: 6000, - }, - None, - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = PictureLossIndication::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} - -#[test] -fn test_picture_loss_indication_unmarshal_header() -> Result<()> { - let tests = vec![( - "valid header", - Bytes::from_static(&[ - 0x81u8, 0xce, 0x00, 0x02, // v=2, p=0, FMT=1, PSFB, len=1 - 0x00, 0x00, 0x00, 0x00, // ssrc=0x0 - 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x4bc4fcb4 - ]), - Header { - count: FORMAT_PLI, - packet_type: PacketType::PayloadSpecificFeedback, - length: PLI_LENGTH as u16, - ..Default::default() - }, - )]; - - for (name, mut data, header) in tests { - let pli = PictureLossIndication::unmarshal(&mut data)?; - - assert_eq!( - pli.header(), - header, - "Unmarshal header {} rr: got {:?}, want {:?}", - name, - pli.header(), - header - ); - } - - Ok(()) -} diff --git a/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs b/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs deleted file mode 100644 index 17703ae29..000000000 --- a/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs +++ /dev/null @@ -1,287 +0,0 @@ -#[cfg(test)] -mod receiver_estimated_maximum_bitrate_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -/// ReceiverEstimatedMaximumBitrate contains the receiver's estimated maximum bitrate. -/// see: https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03 -#[derive(Debug, PartialEq, Default, Clone)] -pub struct ReceiverEstimatedMaximumBitrate { - /// SSRC of sender - pub sender_ssrc: u32, - - /// Estimated maximum bitrate - pub bitrate: f32, - - /// SSRC entries which this packet applies to - pub ssrcs: Vec, -} - -const REMB_OFFSET: usize = 16; - -/// Keep a table of powers to units for fast conversion. -const BIT_UNITS: [&str; 7] = ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"]; -const UNIQUE_IDENTIFIER: [u8; 4] = [b'R', b'E', b'M', b'B']; - -/// String prints the REMB packet in a human-readable format. -impl fmt::Display for ReceiverEstimatedMaximumBitrate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Do some unit conversions because b/s is far too difficult to read. - let mut bitrate = self.bitrate; - let mut powers = 0; - - // Keep dividing the bitrate until it's under 1000 - while bitrate >= 1000.0 && powers < BIT_UNITS.len() { - bitrate /= 1000.0; - powers += 1; - } - - let unit = BIT_UNITS[powers]; - - write!( - f, - "ReceiverEstimatedMaximumBitrate {:x} {:.2} {}/s", - self.sender_ssrc, bitrate, unit, - ) - } -} - -impl Packet for ReceiverEstimatedMaximumBitrate { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_REMB, - packet_type: PacketType::PayloadSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - self.ssrcs.clone() - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + REMB_OFFSET + self.ssrcs.len() * 4 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for ReceiverEstimatedMaximumBitrate { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for ReceiverEstimatedMaximumBitrate { - /// Marshal serializes the packet and returns a byte slice. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - const BITRATE_MAX: f32 = 2.417_842_4e24; //0x3FFFFp+63; - - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |V=2|P| FMT=15 | PT=206 | length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of packet sender | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of media source | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Unique identifier 'R' 'E' 'M' 'B' | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Num SSRC | BR Exp | BR Mantissa | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC feedback | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ... | - */ - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(0); // always zero - - buf.put_slice(&UNIQUE_IDENTIFIER); - - // Write the length of the ssrcs to follow at the end - buf.put_u8(self.ssrcs.len() as u8); - - let mut exp = 0; - let mut bitrate = self.bitrate; - if bitrate >= BITRATE_MAX { - bitrate = BITRATE_MAX - } - - if bitrate < 0.0 { - return Err(Error::InvalidBitrate.into()); - } - - while bitrate >= (1 << 18) as f32 { - bitrate /= 2.0; - exp += 1; - } - - if exp >= (1 << 6) { - return Err(Error::InvalidBitrate.into()); - } - - let mantissa = bitrate.floor() as u32; - - // We can't quite use the binary package because - // a) it's a uint24 and b) the exponent is only 6-bits - // Just trust me; this is big-endian encoding. - buf.put_u8((exp << 2) as u8 | (mantissa >> 16) as u8); - buf.put_u8((mantissa >> 8) as u8); - buf.put_u8(mantissa as u8); - - // Write the SSRCs at the very end. - for ssrc in &self.ssrcs { - buf.put_u32(*ssrc); - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for ReceiverEstimatedMaximumBitrate { - /// Unmarshal reads a REMB packet from the given byte slice. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - // 20 bytes is the size of the packet with no SSRCs - if raw_packet_len < 20 { - return Err(Error::PacketTooShort.into()); - } - - const MANTISSA_MAX: u32 = 0x7FFFFF; - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |V=2|P| FMT=15 | PT=206 | length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of packet sender | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of media source | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Unique identifier 'R' 'E' 'M' 'B' | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Num SSRC | BR Exp | BR Mantissa | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC feedback | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ... | - */ - let header = Header::unmarshal(raw_packet)?; - - if header.packet_type != PacketType::PayloadSpecificFeedback || header.count != FORMAT_REMB - { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - if media_ssrc != 0 { - return Err(Error::SsrcMustBeZero.into()); - } - - // REMB rules all around me - let mut unique_identifier = [0; 4]; - unique_identifier[0] = raw_packet.get_u8(); - unique_identifier[1] = raw_packet.get_u8(); - unique_identifier[2] = raw_packet.get_u8(); - unique_identifier[3] = raw_packet.get_u8(); - if unique_identifier[0] != UNIQUE_IDENTIFIER[0] - || unique_identifier[1] != UNIQUE_IDENTIFIER[1] - || unique_identifier[2] != UNIQUE_IDENTIFIER[2] - || unique_identifier[3] != UNIQUE_IDENTIFIER[3] - { - return Err(Error::MissingRembIdentifier.into()); - } - - // The next byte is the number of SSRC entries at the end. - let ssrcs_len = raw_packet.get_u8() as usize; - - // Get the 6-bit exponent value. - let b17 = raw_packet.get_u8(); - let mut exp = (b17 as u64) >> 2; - exp += 127; // bias for IEEE754 - exp += 23; // IEEE754 biases the decimal to the left, abs-send-time biases it to the right - - // The remaining 2-bits plus the next 16-bits are the mantissa. - let b18 = raw_packet.get_u8(); - let b19 = raw_packet.get_u8(); - let mut mantissa = ((b17 & 3) as u32) << 16 | (b18 as u32) << 8 | b19 as u32; - - if mantissa != 0 { - // ieee754 requires an implicit leading bit - while (mantissa & (MANTISSA_MAX + 1)) == 0 { - exp -= 1; - mantissa *= 2; - } - } - - // bitrate = mantissa * 2^exp - let bitrate = f32::from_bits(((exp as u32) << 23) | (mantissa & MANTISSA_MAX)); - - let mut ssrcs = vec![]; - for _i in 0..ssrcs_len { - ssrcs.push(raw_packet.get_u32()); - } - - if - /*header.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(ReceiverEstimatedMaximumBitrate { - sender_ssrc, - //media_ssrc, - bitrate, - ssrcs, - }) - } -} diff --git a/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/receiver_estimated_maximum_bitrate_test.rs b/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/receiver_estimated_maximum_bitrate_test.rs deleted file mode 100644 index 4ea3efb99..000000000 --- a/rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/receiver_estimated_maximum_bitrate_test.rs +++ /dev/null @@ -1,121 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_receiver_estimated_maximum_bitrate_marshal() { - let input = ReceiverEstimatedMaximumBitrate { - sender_ssrc: 1, - bitrate: 8927168.0, - ssrcs: vec![1215622422], - }; - - let expected = Bytes::from_static(&[ - 143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22, - ]); - - let output = input.marshal().unwrap(); - assert_eq!(output, expected); -} - -#[test] -fn test_receiver_estimated_maximum_bitrate_unmarshal() { - // Real data sent by Chrome while watching a 6Mb/s stream - let mut input = Bytes::from_static(&[ - 143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22, - ]); - - // mantissa = []byte{26 & 3, 32, 223} = []byte{2, 32, 223} = 139487 - // exp = 26 >> 2 = 6 - // bitrate = 139487 * 2^6 = 139487 * 64 = 8927168 = 8.9 Mb/s - let expected = ReceiverEstimatedMaximumBitrate { - sender_ssrc: 1, - bitrate: 8927168.0, - ssrcs: vec![1215622422], - }; - - let packet = ReceiverEstimatedMaximumBitrate::unmarshal(&mut input).unwrap(); - assert_eq!(packet, expected); -} - -#[test] -fn test_receiver_estimated_maximum_bitrate_truncate() { - let input = Bytes::from_static(&[ - 143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22, - ]); - - // Make sure that we're interpreting the bitrate correctly. - // For the above example, we have: - - // mantissa = 139487 - // exp = 6 - // bitrate = 8927168 - - let mut buf = input.clone(); - let mut packet = ReceiverEstimatedMaximumBitrate::unmarshal(&mut buf).unwrap(); - assert_eq!(packet.bitrate, 8927168.0); - - // Just verify marshal produces the same input. - let output = packet.marshal().unwrap(); - assert_eq!(output, input); - - // If we subtract the bitrate by 1, we'll round down a lower mantissa - packet.bitrate -= 1.0; - - // bitrate = 8927167 - // mantissa = 139486 - // exp = 6 - - let mut output = packet.marshal().unwrap(); - assert_ne!(output, input); - let expected = Bytes::from_static(&[ - 143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 222, 72, 116, 237, 22, - ]); - assert_eq!(output, expected); - - // Which if we actually unmarshal again, we'll find that it's actually decreased by 63 (which is exp) - // mantissa = 139486 - // exp = 6 - // bitrate = 8927104 - - let packet = ReceiverEstimatedMaximumBitrate::unmarshal(&mut output).unwrap(); - assert_eq!(8927104.0, packet.bitrate); -} - -#[test] -fn test_receiver_estimated_maximum_bitrate_overflow() { - // Marshal a packet with the maximum possible bitrate. - let packet = ReceiverEstimatedMaximumBitrate { - bitrate: f32::MAX, - ..Default::default() - }; - - // mantissa = 262143 = 0x3FFFF - // exp = 63 - - let expected = Bytes::from_static(&[ - 143, 206, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 82, 69, 77, 66, 0, 255, 255, 255, - ]); - - let output = packet.marshal().unwrap(); - assert_eq!(output, expected); - - // mantissa = 262143 - // exp = 63 - // bitrate = 0xFFFFC00000000000 - - let mut buf = output; - let packet = ReceiverEstimatedMaximumBitrate::unmarshal(&mut buf).unwrap(); - assert_eq!(packet.bitrate, f32::from_bits(0x67FFFFC0)); - - // Make sure we marshal to the same result again. - let output = packet.marshal().unwrap(); - assert_eq!(output, expected); - - // Finally, try unmarshalling one number higher than we used to be able to handle. - let mut input = Bytes::from_static(&[ - 143, 206, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 82, 69, 77, 66, 0, 188, 0, 0, - ]); - let packet = ReceiverEstimatedMaximumBitrate::unmarshal(&mut input).unwrap(); - assert_eq!(packet.bitrate, f32::from_bits(0x62800000)); -} diff --git a/rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs b/rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs deleted file mode 100644 index c424f5c60..000000000 --- a/rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs +++ /dev/null @@ -1,180 +0,0 @@ -#[cfg(test)] -mod slice_loss_indication_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -const SLI_LENGTH: usize = 2; -const SLI_OFFSET: usize = 8; - -/// SLIEntry represents a single entry to the SLI packet's -/// list of lost slices. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SliEntry { - /// ID of first lost slice - pub first: u16, - /// Number of lost slices - pub number: u16, - /// ID of related picture - pub picture: u8, -} - -/// The SliceLossIndication packet informs the encoder about the loss of a picture slice -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SliceLossIndication { - /// SSRC of sender - pub sender_ssrc: u32, - /// SSRC of the media source - pub media_ssrc: u32, - - pub sli_entries: Vec, -} - -impl fmt::Display for SliceLossIndication { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "SliceLossIndication {:x} {:x} {:?}", - self.sender_ssrc, self.media_ssrc, self.sli_entries, - ) - } -} - -impl Packet for SliceLossIndication { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_SLI, - packet_type: PacketType::TransportSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.media_ssrc] - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + SLI_OFFSET + self.sli_entries.len() * 4 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for SliceLossIndication { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for SliceLossIndication { - /// Marshal encodes the SliceLossIndication in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if (self.sli_entries.len() + SLI_LENGTH) as u8 > u8::MAX { - return Err(Error::TooManyReports.into()); - } - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - - for s in &self.sli_entries { - let sli = ((s.first as u32 & 0x1FFF) << 19) - | ((s.number as u32 & 0x1FFF) << 6) - | (s.picture as u32 & 0x3F); - - buf.put_u32(sli); - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for SliceLossIndication { - /// Unmarshal decodes the SliceLossIndication from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - if raw_packet_len < (HEADER_LENGTH + (4 * h.length as usize)) { - return Err(Error::PacketTooShort.into()); - } - - if h.packet_type != PacketType::TransportSpecificFeedback || h.count != FORMAT_SLI { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - - let mut i = HEADER_LENGTH + SLI_OFFSET; - let mut sli_entries = vec![]; - while i < HEADER_LENGTH + h.length as usize * 4 { - let sli = raw_packet.get_u32(); - sli_entries.push(SliEntry { - first: ((sli >> 19) & 0x1FFF) as u16, - number: ((sli >> 6) & 0x1FFF) as u16, - picture: (sli & 0x3F) as u8, - }); - - i += 4; - } - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(SliceLossIndication { - sender_ssrc, - media_ssrc, - sli_entries, - }) - } -} diff --git a/rtcp/src/payload_feedbacks/slice_loss_indication/slice_loss_indication_test.rs b/rtcp/src/payload_feedbacks/slice_loss_indication/slice_loss_indication_test.rs deleted file mode 100644 index c60b3be96..000000000 --- a/rtcp/src/payload_feedbacks/slice_loss_indication/slice_loss_indication_test.rs +++ /dev/null @@ -1,135 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_slice_loss_indication_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x82u8, 0xcd, 0x0, 0x3, // SliceLossIndication - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - 0x55, 0x50, 0x00, 0x2C, // nack 0xAAAA, 0x5555 - ]), - SliceLossIndication { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - sli_entries: vec![SliEntry { - first: 0xaaa, - number: 0, - picture: 0x2C, - }], - }, - None, - ), - ( - "short report", - Bytes::from_static(&[ - 0x82, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, - // report ends early - ]), - SliceLossIndication::default(), - Some(Error::PacketTooShort), - ), - ( - "wrong type", - Bytes::from_static(&[ - // v=2, p=0, count=1, SR, len=7 - 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 - 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 - 0x0, 0x0, 0x46, 0xe1, // jitter=273 - 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 - 0x9, 0xf3, 0x64, 0x32, // delay=150137 - 0x0, 0x2, 0x4a, 0x79, - ]), - SliceLossIndication::default(), - Some(Error::WrongType), - ), - ( - "nil", - Bytes::from_static(&[]), - SliceLossIndication::default(), - Some(Error::PacketTooShort), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = SliceLossIndication::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} rr: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_slice_loss_indication_roundtrip() { - let tests: Vec<(&str, SliceLossIndication, Option)> = vec![( - "valid", - SliceLossIndication { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - sli_entries: vec![ - SliEntry { - first: 1, - number: 0xAA, - picture: 0x1F, - }, - SliEntry { - first: 1034, - number: 0x05, - picture: 0x6, - }, - ], - }, - None, - )]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = SliceLossIndication::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/raw_packet.rs b/rtcp/src/raw_packet.rs deleted file mode 100644 index 68b8cd870..000000000 --- a/rtcp/src/raw_packet.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::Packet; -use crate::util::*; - -/// RawPacket represents an unparsed RTCP packet. It's returned by Unmarshal when -/// a packet with an unknown type is encountered. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct RawPacket(pub Bytes); - -impl fmt::Display for RawPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "RawPacket: {self:?}") - } -} - -impl Packet for RawPacket { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - match Header::unmarshal(&mut self.0.clone()) { - Ok(h) => h, - Err(_) => Header::default(), - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![] - } - - fn raw_size(&self) -> usize { - self.0.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for RawPacket { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for RawPacket { - /// Marshal encodes the packet in binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let h = Header::unmarshal(&mut self.0.clone())?; - buf.put(self.0.clone()); - if h.padding { - put_padding(buf, self.raw_size()); - } - Ok(self.marshal_size()) - } -} - -impl Unmarshal for RawPacket { - /// Unmarshal decodes the packet from binary. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < HEADER_LENGTH { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - let raw_hdr = h.marshal()?; - let raw_body = raw_packet.copy_to_bytes(raw_packet.remaining()); - let mut raw = BytesMut::new(); - raw.extend(raw_hdr); - raw.extend(raw_body); - - Ok(RawPacket(raw.freeze())) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_raw_packet_roundtrip() -> Result<(), Error> { - let tests: Vec<(&str, RawPacket, Option)> = vec![ - ( - "valid", - RawPacket(Bytes::from_static(&[ - 0x81, 0xcb, 0x00, 0x0c, // v=2, p=0, count=1, BYE, len=12 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x03, 0x46, 0x4f, 0x4f, // len=3, text=FOO - ])), - None, - ), - ( - "short header", - RawPacket(Bytes::from_static(&[0x80])), - Some(Error::PacketTooShort), - ), - ( - "invalid header", - RawPacket( - // v=0, p=0, count=0, RR, len=4 - Bytes::from_static(&[0x00, 0xc9, 0x00, 0x04]), - ), - Some(Error::BadVersion), - ), - ]; - - for (name, pkt, unmarshal_error) in tests { - let result = pkt.marshal(); - assert_eq!( - result.is_err(), - unmarshal_error.is_some(), - "Unmarshal {name}: err = {result:?}, want {unmarshal_error:?}" - ); - - if result.is_err() { - continue; - } - - let mut data = result.unwrap(); - - let result = RawPacket::unmarshal(&mut data); - - assert_eq!( - result.is_err(), - unmarshal_error.is_some(), - "Unmarshal {name}: err = {result:?}, want {unmarshal_error:?}" - ); - - if result.is_err() { - continue; - } - - let decoded = result.unwrap(); - - assert_eq!( - decoded, pkt, - "{name} raw round trip: got {decoded:?}, want {pkt:?}" - ) - } - - Ok(()) - } -} diff --git a/rtcp/src/receiver_report/mod.rs b/rtcp/src/receiver_report/mod.rs deleted file mode 100644 index 369c5d2c1..000000000 --- a/rtcp/src/receiver_report/mod.rs +++ /dev/null @@ -1,230 +0,0 @@ -#[cfg(test)] -mod receiver_report_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::reception_report::*; -use crate::util::*; - -type Result = std::result::Result; - -pub(super) const RR_SSRC_OFFSET: usize = HEADER_LENGTH; -pub(super) const RR_REPORT_OFFSET: usize = RR_SSRC_OFFSET + SSRC_LENGTH; - -/// A ReceiverReport (RR) packet provides reception quality feedback for an RTP stream -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct ReceiverReport { - /// The synchronization source identifier for the originator of this RR packet. - pub ssrc: u32, - /// Zero or more reception report blocks depending on the number of other - /// sources heard by this sender since the last report. Each reception report - /// block conveys statistics on the reception of RTP packets from a - /// single synchronization source. - pub reports: Vec, - /// Extension contains additional, payload-specific information that needs to - /// be reported regularly about the receiver. - pub profile_extensions: Bytes, -} - -impl fmt::Display for ReceiverReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = format!("ReceiverReport from {}\n", self.ssrc); - out += "\tSSRC \tLost\tLastSequence\n"; - for rep in &self.reports { - out += format!( - "\t{:x}\t{}/{}\t{}\n", - rep.ssrc, rep.fraction_lost, rep.total_lost, rep.last_sequence_number - ) - .as_str(); - } - out += format!("\tProfile Extension Data: {:?}\n", self.profile_extensions).as_str(); - - write!(f, "{out}") - } -} - -impl Packet for ReceiverReport { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: self.reports.len() as u8, - packet_type: PacketType::ReceiverReport, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - self.reports.iter().map(|x| x.ssrc).collect() - } - - fn raw_size(&self) -> usize { - let mut reps_length = 0; - for rep in &self.reports { - reps_length += rep.marshal_size(); - } - - HEADER_LENGTH + SSRC_LENGTH + reps_length + self.profile_extensions.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for ReceiverReport { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for ReceiverReport { - /// marshal_to encodes the packet in binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.reports.len() > COUNT_MAX { - return Err(Error::TooManyReports.into()); - } - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| RC | PT=RR=201 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC of packet sender | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_1 (SSRC of first source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 1 | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_2 (SSRC of second source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 2 : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | profile-specific extensions | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - - for report in &self.reports { - let n = report.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - buf.put(self.profile_extensions.clone()); - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for ReceiverReport { - /// Unmarshal decodes the ReceiverReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| RC | PT=RR=201 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC of packet sender | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_1 (SSRC of first source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 1 | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_2 (SSRC of second source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 2 : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | profile-specific extensions | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let header = Header::unmarshal(raw_packet)?; - if header.packet_type != PacketType::ReceiverReport { - return Err(Error::WrongType.into()); - } - - let ssrc = raw_packet.get_u32(); - - let mut offset = RR_REPORT_OFFSET; - let mut reports = Vec::with_capacity(header.count as usize); - for _ in 0..header.count { - if offset + RECEPTION_REPORT_LENGTH > raw_packet_len { - return Err(Error::PacketTooShort.into()); - } - let reception_report = ReceptionReport::unmarshal(raw_packet)?; - reports.push(reception_report); - offset += RECEPTION_REPORT_LENGTH; - } - let profile_extensions = raw_packet.copy_to_bytes(raw_packet.remaining()); - /* - if header.padding && raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - */ - - Ok(ReceiverReport { - ssrc, - reports, - profile_extensions, - }) - } -} diff --git a/rtcp/src/receiver_report/receiver_report_test.rs b/rtcp/src/receiver_report/receiver_report_test.rs deleted file mode 100644 index 22450a54a..000000000 --- a/rtcp/src/receiver_report/receiver_report_test.rs +++ /dev/null @@ -1,242 +0,0 @@ -use super::*; - -#[test] -fn test_receiver_report_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x81u8, 0xc9, 0x0, 0x7, // v=2, p=0, count=1, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - ReceiverReport { - ssrc: 0x902f9e2e, - reports: vec![ReceptionReport { - ssrc: 0xbc5e9a40, - fraction_lost: 0, - total_lost: 0, - last_sequence_number: 0x46e1, - jitter: 273, - last_sender_report: 0x9f36432, - delay: 150137, - }], - profile_extensions: Bytes::new(), - }, - None, - ), - ( - "valid with extension data", - Bytes::from_static(&[ - 0x81, 0xc9, 0x0, 0x9, // v=2, p=0, count=1, RR, len=9 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, - 0x41, // profile-specific extension data - ]), - ReceiverReport { - ssrc: 0x902f9e2e, - reports: vec![ReceptionReport { - ssrc: 0xbc5e9a40, - fraction_lost: 0, - total_lost: 0, - last_sequence_number: 0x46e1, - jitter: 273, - last_sender_report: 0x9f36432, - delay: 150137, - }], - profile_extensions: Bytes::from_static(&[ - 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, 0x41, - ]), - }, - None, - ), - ( - "short report", - Bytes::from_static(&[ - 0x81, 0xc9, 0x00, 0x0c, // v=2, p=0, count=1, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0x00, 0x00, 0x00, - 0x00, // fracLost=0, totalLost=0 - // report ends early - ]), - ReceiverReport::default(), - Some(Error::PacketTooShort), - ), - ( - "wrong type", - Bytes::from_static(&[ - // v=2, p=0, count=1, SR, len=7 - 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 - 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 - 0x0, 0x0, 0x46, 0xe1, // jitter=273 - 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 - 0x9, 0xf3, 0x64, 0x32, // delay=150137 - 0x0, 0x2, 0x4a, 0x79, - ]), - ReceiverReport::default(), - Some(Error::WrongType), - ), - ( - "bad count in header", - Bytes::from_static(&[ - 0x82, 0xc9, 0x0, 0x7, // v=2, p=0, count=2, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - ReceiverReport::default(), - Some(Error::PacketTooShort), - ), - ( - "nil", - Bytes::from_static(&[]), - ReceiverReport::default(), - Some(Error::PacketTooShort), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = ReceiverReport::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name}: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name}: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_receiver_report_roundtrip() { - let mut too_many_reports = vec![]; - for _i in 0..(1 << 5) { - too_many_reports.push(ReceptionReport { - ssrc: 2, - fraction_lost: 2, - total_lost: 3, - last_sequence_number: 4, - jitter: 5, - last_sender_report: 6, - delay: 7, - }); - } - - let tests = vec![ - ( - "valid", - ReceiverReport { - ssrc: 1, - reports: vec![ - ReceptionReport { - ssrc: 2, - fraction_lost: 2, - total_lost: 3, - last_sequence_number: 4, - jitter: 5, - last_sender_report: 6, - delay: 7, - }, - ReceptionReport::default(), - ], - profile_extensions: Bytes::from_static(&[]), - }, - None, - ), - ( - "also valid", - ReceiverReport { - ssrc: 2, - reports: vec![ReceptionReport { - ssrc: 999, - fraction_lost: 30, - total_lost: 12345, - last_sequence_number: 99, - jitter: 22, - last_sender_report: 92, - delay: 46, - }], - ..Default::default() - }, - None, - ), - ( - "totallost overflow", - ReceiverReport { - ssrc: 1, - reports: vec![ReceptionReport { - total_lost: 1 << 25, - ..Default::default() - }], - ..Default::default() - }, - Some(Error::InvalidTotalLost), - ), - ( - "count overflow", - ReceiverReport { - ssrc: 1, - reports: too_many_reports, - ..Default::default() - }, - Some(Error::TooManyReports), - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = - ReceiverReport::unmarshal(&mut data).unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/reception_report.rs b/rtcp/src/reception_report.rs deleted file mode 100644 index b9c49f2ad..000000000 --- a/rtcp/src/reception_report.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -pub(crate) const RECEPTION_REPORT_LENGTH: usize = 24; -pub(crate) const FRACTION_LOST_OFFSET: usize = 4; -pub(crate) const TOTAL_LOST_OFFSET: usize = 5; -pub(crate) const LAST_SEQ_OFFSET: usize = 8; -pub(crate) const JITTER_OFFSET: usize = 12; -pub(crate) const LAST_SR_OFFSET: usize = 16; -pub(crate) const DELAY_OFFSET: usize = 20; - -/// A ReceptionReport block conveys statistics on the reception of RTP packets -/// from a single synchronization source. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct ReceptionReport { - /// The SSRC identifier of the source to which the information in this - /// reception report block pertains. - pub ssrc: u32, - /// The fraction of RTP data packets from source SSRC lost since the - /// previous SR or RR packet was sent, expressed as a fixed point - /// number with the binary point at the left edge of the field. - pub fraction_lost: u8, - /// The total number of RTP data packets from source SSRC that have - /// been lost since the beginning of reception. - pub total_lost: u32, - /// The least significant 16 bits contain the highest sequence number received - /// in an RTP data packet from source SSRC, and the most significant 16 bits extend - /// that sequence number with the corresponding count of sequence number cycles. - pub last_sequence_number: u32, - /// An estimate of the statistical variance of the RTP data packet - /// interarrival time, measured in timestamp units and expressed as an - /// unsigned integer. - pub jitter: u32, - /// The middle 32 bits out of 64 in the NTP timestamp received as part of - /// the most recent RTCP sender report (SR) packet from source SSRC. If no - /// SR has been received yet, the field is set to zero. - pub last_sender_report: u32, - /// The delay, expressed in units of 1/65536 seconds, between receiving the - /// last SR packet from source SSRC and sending this reception report block. - /// If no SR packet has been received yet from SSRC, the field is set to zero. - pub delay: u32, -} - -impl fmt::Display for ReceptionReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl Packet for ReceptionReport { - fn header(&self) -> Header { - Header::default() - } - - fn destination_ssrc(&self) -> Vec { - vec![] - } - - fn raw_size(&self) -> usize { - RECEPTION_REPORT_LENGTH - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for ReceptionReport { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for ReceptionReport { - /// marshal_to encodes the ReceptionReport in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | SSRC | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - buf.put_u32(self.ssrc); - - buf.put_u8(self.fraction_lost); - - // pack TotalLost into 24 bits - if self.total_lost >= (1 << 25) { - return Err(Error::InvalidTotalLost.into()); - } - - buf.put_u8(((self.total_lost >> 16) & 0xFF) as u8); - buf.put_u8(((self.total_lost >> 8) & 0xFF) as u8); - buf.put_u8((self.total_lost & 0xFF) as u8); - - buf.put_u32(self.last_sequence_number); - buf.put_u32(self.jitter); - buf.put_u32(self.last_sender_report); - buf.put_u32(self.delay); - - put_padding(buf, self.raw_size()); - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for ReceptionReport { - /// unmarshal decodes the ReceptionReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < RECEPTION_REPORT_LENGTH { - return Err(Error::PacketTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | SSRC | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - let ssrc = raw_packet.get_u32(); - let fraction_lost = raw_packet.get_u8(); - - let t0 = raw_packet.get_u8(); - let t1 = raw_packet.get_u8(); - let t2 = raw_packet.get_u8(); - // TODO: The type of `total_lost` should be `i32`, per the RFC: - // The total number of RTP data packets from source SSRC_n that have - // been lost since the beginning of reception. This number is - // defined to be the number of packets expected less the number of - // packets actually received, where the number of packets received - // includes any which are late or duplicates. Thus, packets that - // arrive late are not counted as lost, and the loss may be negative - // if there are duplicates. The number of packets expected is - // defined to be the extended last sequence number received, as - // defined next, less the initial sequence number received. This may - // be calculated as shown in Appendix A.3. - let total_lost = (t2 as u32) | (t1 as u32) << 8 | (t0 as u32) << 16; - - let last_sequence_number = raw_packet.get_u32(); - let jitter = raw_packet.get_u32(); - let last_sender_report = raw_packet.get_u32(); - let delay = raw_packet.get_u32(); - - Ok(ReceptionReport { - ssrc, - fraction_lost, - total_lost, - last_sequence_number, - jitter, - last_sender_report, - delay, - }) - } -} diff --git a/rtcp/src/sender_report/mod.rs b/rtcp/src/sender_report/mod.rs deleted file mode 100644 index 1e66d9b83..000000000 --- a/rtcp/src/sender_report/mod.rs +++ /dev/null @@ -1,299 +0,0 @@ -#[cfg(test)] -mod sender_report_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::reception_report::*; -use crate::util::*; - -type Result = std::result::Result; - -pub(crate) const SR_HEADER_LENGTH: usize = 24; -pub(crate) const SR_SSRC_OFFSET: usize = HEADER_LENGTH; -pub(crate) const SR_REPORT_OFFSET: usize = SR_SSRC_OFFSET + SR_HEADER_LENGTH; - -pub(crate) const SR_NTP_OFFSET: usize = SR_SSRC_OFFSET + SSRC_LENGTH; -pub(crate) const NTP_TIME_LENGTH: usize = 8; -pub(crate) const SR_RTP_OFFSET: usize = SR_NTP_OFFSET + NTP_TIME_LENGTH; -pub(crate) const RTP_TIME_LENGTH: usize = 4; -pub(crate) const SR_PACKET_COUNT_OFFSET: usize = SR_RTP_OFFSET + RTP_TIME_LENGTH; -pub(crate) const SR_PACKET_COUNT_LENGTH: usize = 4; -pub(crate) const SR_OCTET_COUNT_OFFSET: usize = SR_PACKET_COUNT_OFFSET + SR_PACKET_COUNT_LENGTH; -pub(crate) const SR_OCTET_COUNT_LENGTH: usize = 4; - -/// A SenderReport (SR) packet provides reception quality feedback for an RTP stream -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SenderReport { - /// The synchronization source identifier for the originator of this SR packet. - pub ssrc: u32, - /// The wallclock time when this report was sent so that it may be used in - /// combination with timestamps returned in reception reports from other - /// receivers to measure round-trip propagation to those receivers. - pub ntp_time: u64, - /// Corresponds to the same time as the NTP timestamp (above), but in - /// the same units and with the same random offset as the RTP - /// timestamps in data packets. This correspondence may be used for - /// intra- and inter-media synchronization for sources whose NTP - /// timestamps are synchronized, and may be used by media-independent - /// receivers to estimate the nominal RTP clock frequency. - pub rtp_time: u32, - /// The total number of RTP data packets transmitted by the sender - /// since starting transmission up until the time this SR packet was - /// generated. - pub packet_count: u32, - /// The total number of payload octets (i.e., not including header or - /// padding) transmitted in RTP data packets by the sender since - /// starting transmission up until the time this SR packet was - /// generated. - pub octet_count: u32, - /// Zero or more reception report blocks depending on the number of other - /// sources heard by this sender since the last report. Each reception report - /// block conveys statistics on the reception of RTP packets from a - /// single synchronization source. - pub reports: Vec, - - /// ProfileExtensions contains additional, payload-specific information that needs to - /// be reported regularly about the sender. - pub profile_extensions: Bytes, -} - -impl fmt::Display for SenderReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = format!("SenderReport from {}\n", self.ssrc); - out += format!("\tNTPTime:\t{}\n", self.ntp_time).as_str(); - out += format!("\tRTPTIme:\t{}\n", self.rtp_time).as_str(); - out += format!("\tPacketCount:\t{}\n", self.packet_count).as_str(); - out += format!("\tOctetCount:\t{}\n", self.octet_count).as_str(); - out += "\tSSRC \tLost\tLastSequence\n"; - for rep in &self.reports { - out += format!( - "\t{:x}\t{}/{}\t{}\n", - rep.ssrc, rep.fraction_lost, rep.total_lost, rep.last_sequence_number - ) - .as_str(); - } - out += format!("\tProfile Extension Data: {:?}\n", self.profile_extensions).as_str(); - - write!(f, "{out}") - } -} - -impl Packet for SenderReport { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: self.reports.len() as u8, - packet_type: PacketType::SenderReport, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - let mut out: Vec = self.reports.iter().map(|x| x.ssrc).collect(); - out.push(self.ssrc); - out - } - - fn raw_size(&self) -> usize { - let mut reps_length = 0; - for rep in &self.reports { - reps_length += rep.marshal_size(); - } - - HEADER_LENGTH + SR_HEADER_LENGTH + reps_length + self.profile_extensions.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for SenderReport { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for SenderReport { - /// Marshal encodes the packet in binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.reports.len() > COUNT_MAX { - return Err(Error::TooManyReports.into()); - } - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| RC | PT=SR=200 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC of sender | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * sender | NTP timestamp, most significant word | - * info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | NTP timestamp, least significant word | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | RTP timestamp | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | sender's packet count | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | sender's octet count | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_1 (SSRC of first source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 1 | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_2 (SSRC of second source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 2 : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | profile-specific extensions | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.ssrc); - buf.put_u64(self.ntp_time); - buf.put_u32(self.rtp_time); - buf.put_u32(self.packet_count); - buf.put_u32(self.octet_count); - - for report in &self.reports { - let n = report.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - buf.put(self.profile_extensions.clone()); - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for SenderReport { - /// Unmarshal decodes the SenderReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| RC | PT=SR=200 | length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SSRC of sender | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * sender | NTP timestamp, most significant word | - * info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | NTP timestamp, least significant word | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | RTP timestamp | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | sender's packet count | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | sender's octet count | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_1 (SSRC of first source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 1 | fraction lost | cumulative number of packets lost | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | extended highest sequence number received | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | interarrival jitter | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | last SR (LSR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | delay since last SR (DLSR) | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * report | SSRC_2 (SSRC of second source) | - * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * 2 : ... : - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | profile-specific extensions | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SR_HEADER_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let header = Header::unmarshal(raw_packet)?; - if header.packet_type != PacketType::SenderReport { - return Err(Error::WrongType.into()); - } - - let ssrc = raw_packet.get_u32(); - let ntp_time = raw_packet.get_u64(); - let rtp_time = raw_packet.get_u32(); - let packet_count = raw_packet.get_u32(); - let octet_count = raw_packet.get_u32(); - - let mut offset = SR_REPORT_OFFSET; - let mut reports = Vec::with_capacity(header.count as usize); - for _ in 0..header.count { - if offset + RECEPTION_REPORT_LENGTH > raw_packet_len { - return Err(Error::PacketTooShort.into()); - } - let reception_report = ReceptionReport::unmarshal(raw_packet)?; - reports.push(reception_report); - offset += RECEPTION_REPORT_LENGTH; - } - let profile_extensions = raw_packet.copy_to_bytes(raw_packet.remaining()); - /* - if header.padding && raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - */ - - Ok(SenderReport { - ssrc, - ntp_time, - rtp_time, - packet_count, - octet_count, - reports, - profile_extensions, - }) - } -} diff --git a/rtcp/src/sender_report/sender_report_test.rs b/rtcp/src/sender_report/sender_report_test.rs deleted file mode 100644 index 7cd7617ed..000000000 --- a/rtcp/src/sender_report/sender_report_test.rs +++ /dev/null @@ -1,252 +0,0 @@ -use super::*; - -#[test] -fn test_sender_report_unmarshal() { - let tests = vec![ - ( - "nil", - Bytes::from_static(&[]), - SenderReport::default(), - Some(Error::PacketTooShort), - ), - ( - "valid", - Bytes::from_static(&[ - 0x81u8, 0xc8, 0x0, 0x7, // v=2, p=0, count=1, SR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // ntp=0xda8bd1fcdddda05a - 0xaa, 0xf4, 0xed, 0xd5, // rtp=0xaaf4edd5 - 0x00, 0x00, 0x00, 0x01, // packetCount=1 - 0x00, 0x00, 0x00, 0x02, // octetCount=2 - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - SenderReport { - ssrc: 0x902f9e2e, - ntp_time: 0xda8bd1fcdddda05a, - rtp_time: 0xaaf4edd5, - packet_count: 1, - octet_count: 2, - reports: vec![ReceptionReport { - ssrc: 0xbc5e9a40, - fraction_lost: 0, - total_lost: 0, - last_sequence_number: 0x46e1, - jitter: 273, - last_sender_report: 0x9f36432, - delay: 150137, - }], - profile_extensions: Bytes::from_static(&[]), - }, - None, - ), - ( - "wrong type", - Bytes::from_static(&[ - 0x81, 0xc9, 0x0, 0x7, // v=2, p=0, count=1, RR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // ntp=0xda8bd1fcdddda05a - 0xaa, 0xf4, 0xed, 0xd5, // rtp=0xaaf4edd5 - 0x00, 0x00, 0x00, 0x01, // packetCount=1 - 0x00, 0x00, 0x00, 0x02, // octetCount=2 - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // jitter=273 - 0x0, 0x0, 0x1, 0x11, // lastSeq=0x46e1 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - SenderReport::default(), - Some(Error::WrongType), - ), - ( - "bad count in header", - Bytes::from_static(&[ - 0x82, 0xc8, 0x0, 0x7, // v=2, p=0, count=1, SR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // ntp=0xda8bd1fcdddda05a - 0xaa, 0xf4, 0xed, 0xd5, // rtp=0xaaf4edd5 - 0x00, 0x00, 0x00, 0x01, // packetCount=1 - 0x00, 0x00, 0x00, 0x02, // octetCount=2 - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - SenderReport::default(), - Some(Error::PacketTooShort), - ), - ( - "with extension", // issue #447 - Bytes::from_static(&[ - 0x80, 0xc8, 0x0, 0x6, // v=2, p=0, count=0, SR, len=6 - 0x2b, 0x7e, 0xc0, 0xc5, // ssrc=0x2b7ec0c5 - 0xe0, 0x20, 0xa2, 0xa9, 0x52, 0xa5, 0x3f, 0xc0, // ntp=0xe020a2a952a53fc0 - 0x2e, 0x48, 0xa5, 0x52, // rtp=0x2e48a552 - 0x0, 0x0, 0x0, 0x46, // packetCount=70 - 0x0, 0x0, 0x12, 0x1d, // octetCount=4637 - 0x81, 0xca, 0x0, 0x6, 0x2b, 0x7e, 0xc0, 0xc5, 0x1, 0x10, 0x4c, 0x63, 0x49, 0x66, - 0x7a, 0x58, 0x6f, 0x6e, 0x44, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x57, 0x36, 0x0, - 0x0, // profile-specific extension - ]), - SenderReport { - ssrc: 0x2b7ec0c5, - ntp_time: 0xe020a2a952a53fc0, - rtp_time: 0x2e48a552, - packet_count: 70, - octet_count: 4637, - reports: vec![], - profile_extensions: Bytes::from_static(&[ - 0x81, 0xca, 0x0, 0x6, 0x2b, 0x7e, 0xc0, 0xc5, 0x1, 0x10, 0x4c, 0x63, 0x49, - 0x66, 0x7a, 0x58, 0x6f, 0x6e, 0x44, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x57, 0x36, - 0x0, 0x0, - ]), - }, - None, - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = SenderReport::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name}: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name}: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_sender_report_roundtrip() { - let mut too_many_reports = vec![]; - for _i in 0..(1 << 5) { - too_many_reports.push(ReceptionReport { - ssrc: 2, - fraction_lost: 2, - total_lost: 3, - last_sequence_number: 4, - jitter: 5, - last_sender_report: 6, - delay: 7, - }); - } - - let tests = vec![ - ( - "valid", - SenderReport { - ssrc: 1, - ntp_time: 999, - rtp_time: 555, - packet_count: 32, - octet_count: 11, - reports: vec![ - ReceptionReport { - ssrc: 2, - fraction_lost: 2, - total_lost: 3, - last_sequence_number: 4, - jitter: 5, - last_sender_report: 6, - delay: 7, - }, - ReceptionReport::default(), - ], - profile_extensions: Bytes::from_static(&[]), - }, - None, - ), - ( - "also valid", - SenderReport { - ssrc: 2, - reports: vec![ReceptionReport { - ssrc: 999, - fraction_lost: 30, - total_lost: 12345, - last_sequence_number: 99, - jitter: 22, - last_sender_report: 92, - delay: 46, - }], - ..Default::default() - }, - None, - ), - ( - "extension", - SenderReport { - ssrc: 2, - reports: vec![ReceptionReport { - ssrc: 999, - fraction_lost: 30, - total_lost: 12345, - last_sequence_number: 99, - jitter: 22, - last_sender_report: 92, - delay: 46, - }], - profile_extensions: Bytes::from_static(&[1, 2, 3, 4]), - ..Default::default() - }, - None, - ), - ( - "count overflow", - SenderReport { - ssrc: 1, - reports: too_many_reports, - ..Default::default() - }, - Some(Error::TooManyReports), - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = - SenderReport::unmarshal(&mut data).unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/source_description/mod.rs b/rtcp/src/source_description/mod.rs deleted file mode 100644 index 5cf66545b..000000000 --- a/rtcp/src/source_description/mod.rs +++ /dev/null @@ -1,440 +0,0 @@ -#[cfg(test)] -mod source_description_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -const SDES_SOURCE_LEN: usize = 4; -const SDES_TYPE_LEN: usize = 1; -const SDES_TYPE_OFFSET: usize = 0; -const SDES_OCTET_COUNT_LEN: usize = 1; -const SDES_OCTET_COUNT_OFFSET: usize = 1; -const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; -const SDES_TEXT_OFFSET: usize = 2; - -/// SDESType is the item type used in the RTCP SDES control packet. -/// RTP SDES item types registered with IANA. See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-5 -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[repr(u8)] -pub enum SdesType { - #[default] - SdesEnd = 0, // end of SDES list RFC 3550, 6.5 - SdesCname = 1, // canonical name RFC 3550, 6.5.1 - SdesName = 2, // user name RFC 3550, 6.5.2 - SdesEmail = 3, // user's electronic mail address RFC 3550, 6.5.3 - SdesPhone = 4, // user's phone number RFC 3550, 6.5.4 - SdesLocation = 5, // geographic user location RFC 3550, 6.5.5 - SdesTool = 6, // name of application or tool RFC 3550, 6.5.6 - SdesNote = 7, // notice about the source RFC 3550, 6.5.7 - SdesPrivate = 8, // private extensions RFC 3550, 6.5.8 (not implemented) -} - -impl fmt::Display for SdesType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - SdesType::SdesEnd => "END", - SdesType::SdesCname => "CNAME", - SdesType::SdesName => "NAME", - SdesType::SdesEmail => "EMAIL", - SdesType::SdesPhone => "PHONE", - SdesType::SdesLocation => "LOC", - SdesType::SdesTool => "TOOL", - SdesType::SdesNote => "NOTE", - SdesType::SdesPrivate => "PRIV", - }; - write!(f, "{s}") - } -} - -impl From for SdesType { - fn from(b: u8) -> Self { - match b { - 1 => SdesType::SdesCname, - 2 => SdesType::SdesName, - 3 => SdesType::SdesEmail, - 4 => SdesType::SdesPhone, - 5 => SdesType::SdesLocation, - 6 => SdesType::SdesTool, - 7 => SdesType::SdesNote, - 8 => SdesType::SdesPrivate, - _ => SdesType::SdesEnd, - } - } -} - -/// A SourceDescriptionChunk contains items describing a single RTP source -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SourceDescriptionChunk { - /// The source (ssrc) or contributing source (csrc) identifier this packet describes - pub source: u32, - pub items: Vec, -} - -impl SourceDescriptionChunk { - fn raw_size(&self) -> usize { - let mut len = SDES_SOURCE_LEN; - for it in &self.items { - len += it.marshal_size(); - } - len += SDES_TYPE_LEN; // for terminating null octet - len - } -} - -impl MarshalSize for SourceDescriptionChunk { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for SourceDescriptionChunk { - /// Marshal encodes the SourceDescriptionChunk in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - /* - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | SSRC/CSRC_1 | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - - buf.put_u32(self.source); - - for it in &self.items { - let n = it.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - // The list of items in each chunk MUST be terminated by one or more null octets - buf.put_u8(SdesType::SdesEnd as u8); - - // additional null octets MUST be included if needed to pad until the next 32-bit boundary - put_padding(buf, self.raw_size()); - Ok(self.marshal_size()) - } -} - -impl Unmarshal for SourceDescriptionChunk { - /// Unmarshal decodes the SourceDescriptionChunk from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | SSRC/CSRC_1 | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (SDES_SOURCE_LEN + SDES_TYPE_LEN) { - return Err(Error::PacketTooShort.into()); - } - - let source = raw_packet.get_u32(); - - let mut offset = SDES_SOURCE_LEN; - let mut items = vec![]; - while offset < raw_packet_len { - let item = SourceDescriptionItem::unmarshal(raw_packet)?; - if item.sdes_type == SdesType::SdesEnd { - // offset + 1 (one byte for SdesEnd) - let padding_len = get_padding_size(offset + 1); - if raw_packet.remaining() >= padding_len { - raw_packet.advance(padding_len); - return Ok(SourceDescriptionChunk { source, items }); - } else { - return Err(Error::PacketTooShort.into()); - } - } - offset += item.marshal_size(); - items.push(item); - } - - Err(Error::PacketTooShort.into()) - } -} - -/// A SourceDescriptionItem is a part of a SourceDescription that describes a stream. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SourceDescriptionItem { - /// The type identifier for this item. eg, SDESCNAME for canonical name description. - /// - /// Type zero or SDESEnd is interpreted as the end of an item list and cannot be used. - pub sdes_type: SdesType, - /// Text is a unicode text blob associated with the item. Its meaning varies based on the item's Type. - pub text: Bytes, -} - -impl MarshalSize for SourceDescriptionItem { - fn marshal_size(&self) -> usize { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | CNAME=1 | length | user and domain name ... - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - SDES_TYPE_LEN + SDES_OCTET_COUNT_LEN + self.text.len() - } -} - -impl Marshal for SourceDescriptionItem { - /// Marshal encodes the SourceDescriptionItem in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | CNAME=1 | length | user and domain name ... - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - - if self.sdes_type == SdesType::SdesEnd { - return Err(Error::SdesMissingType.into()); - } - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - buf.put_u8(self.sdes_type as u8); - - if self.text.len() > SDES_MAX_OCTET_COUNT { - return Err(Error::SdesTextTooLong.into()); - } - buf.put_u8(self.text.len() as u8); - buf.put(self.text.clone()); - - //no padding for each SourceDescriptionItem - Ok(self.marshal_size()) - } -} - -impl Unmarshal for SourceDescriptionItem { - /// Unmarshal decodes the SourceDescriptionItem from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | CNAME=1 | length | user and domain name ... - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < SDES_TYPE_LEN { - return Err(Error::PacketTooShort.into()); - } - - let sdes_type = SdesType::from(raw_packet.get_u8()); - if sdes_type == SdesType::SdesEnd { - return Ok(SourceDescriptionItem { - sdes_type, - text: Bytes::new(), - }); - } - - if raw_packet_len < (SDES_TYPE_LEN + SDES_OCTET_COUNT_LEN) { - return Err(Error::PacketTooShort.into()); - } - - let octet_count = raw_packet.get_u8() as usize; - if SDES_TEXT_OFFSET + octet_count > raw_packet_len { - return Err(Error::PacketTooShort.into()); - } - - let text = raw_packet.copy_to_bytes(octet_count); - - Ok(SourceDescriptionItem { sdes_type, text }) - } -} - -/// A SourceDescription (SDES) packet describes the sources in an RTP stream. -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct SourceDescription { - pub chunks: Vec, -} - -impl fmt::Display for SourceDescription { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = "Source Description:\n".to_string(); - for c in &self.chunks { - out += format!("\t{:x}\n", c.source).as_str(); - for it in &c.items { - out += format!("\t\t{it:?}\n").as_str(); - } - } - write!(f, "{out}") - } -} - -impl Packet for SourceDescription { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: self.chunks.len() as u8, - packet_type: PacketType::SourceDescription, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - self.chunks.iter().map(|x| x.source).collect() - } - - fn raw_size(&self) -> usize { - let mut chunks_length = 0; - for c in &self.chunks { - chunks_length += c.marshal_size(); - } - - HEADER_LENGTH + chunks_length - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for SourceDescription { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for SourceDescription { - /// Marshal encodes the SourceDescription in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.chunks.len() > COUNT_MAX { - return Err(Error::TooManyChunks.into()); - } - - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| SC | PT=SDES=202 | length | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * chunk | SSRC/CSRC_1 | - * 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * chunk | SSRC/CSRC_2 | - * 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - for c in &self.chunks { - let n = c.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for SourceDescription { - /// Unmarshal decodes the SourceDescription from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * header |V=2|P| SC | PT=SDES=202 | length | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * chunk | SSRC/CSRC_1 | - * 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * chunk | SSRC/CSRC_2 | - * 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | SDES items | - * | ... | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - */ - let raw_packet_len = raw_packet.remaining(); - - let h = Header::unmarshal(raw_packet)?; - if h.packet_type != PacketType::SourceDescription { - return Err(Error::WrongType.into()); - } - - let mut offset = HEADER_LENGTH; - let mut chunks = vec![]; - while offset < raw_packet_len { - let chunk = SourceDescriptionChunk::unmarshal(raw_packet)?; - offset += chunk.marshal_size(); - chunks.push(chunk); - } - - if chunks.len() != h.count as usize { - return Err(Error::InvalidHeader.into()); - } - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(SourceDescription { chunks }) - } -} diff --git a/rtcp/src/source_description/source_description_test.rs b/rtcp/src/source_description/source_description_test.rs deleted file mode 100644 index cdc0d68fb..000000000 --- a/rtcp/src/source_description/source_description_test.rs +++ /dev/null @@ -1,359 +0,0 @@ -use super::*; - -#[test] -fn test_source_description_unmarshal() { - let tests = vec![ - ( - "nil", - Bytes::from_static(&[]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "no chunks", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=8 - 0x80, 0xca, 0x00, 0x04, - ]), - SourceDescription::default(), - None, - ), - ( - "missing type", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=8 - 0x81, 0xca, 0x00, 0x08, // ssrc=0x00000000 - 0x00, 0x00, 0x00, 0x00, - ]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "bad cname length", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=10 - 0x81, 0xca, 0x00, 0x0a, // ssrc=0x00000000 - 0x00, 0x00, 0x00, 0x00, // CNAME, len = 1 - 0x01, 0x01, - ]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "short cname", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=9 - 0x81, 0xca, 0x00, 0x09, // ssrc=0x00000000 - 0x00, 0x00, 0x00, 0x00, // CNAME, Missing length - 0x01, - ]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "no end", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=11 - 0x81, 0xca, 0x00, 0x0b, // ssrc=0x00000000 - 0x00, 0x00, 0x00, 0x00, // CNAME, len=1, content=A - 0x01, 0x02, 0x41, - // Missing END - ]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "bad octet count", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=10 - 0x81, 0xca, 0x00, 0x0a, // ssrc=0x00000000 - 0x00, 0x00, 0x00, 0x00, // CNAME, len=1 - 0x01, 0x01, - ]), - SourceDescription::default(), - Some(Error::PacketTooShort), - ), - ( - "zero item chunk", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=12 - 0x81, 0xca, 0x00, 0x0c, // ssrc=0x01020304 - 0x01, 0x02, 0x03, 0x04, // END + padding - 0x00, 0x00, 0x00, 0x00, - ]), - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 0x01020304, - items: vec![], - }], - }, - None, - ), - ( - "wrong type", - Bytes::from_static(&[ - // v=2, p=0, count=1, SR, len=12 - 0x81, 0xc8, 0x00, 0x0c, // ssrc=0x01020304 - 0x01, 0x02, 0x03, 0x04, // END + padding - 0x00, 0x00, 0x00, 0x00, - ]), - SourceDescription::default(), - Some(Error::WrongType), - ), - ( - "bad count in header", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=12 - 0x81, 0xca, 0x00, 0x0c, - ]), - SourceDescription::default(), - Some(Error::InvalidHeader), - ), - ( - "empty string", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=12 - 0x81, 0xca, 0x00, 0x0c, // ssrc=0x01020304 - 0x01, 0x02, 0x03, 0x04, // CNAME, len=0 - 0x01, 0x00, // END + padding - 0x00, 0x00, - ]), - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 0x01020304, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b""), - }], - }], - }, - None, - ), - ( - "two items", - Bytes::from_static(&[ - // v=2, p=0, count=1, SDES, len=16 - 0x81, 0xca, 0x00, 0x10, // ssrc=0x10000000 - 0x10, 0x00, 0x00, 0x00, // CNAME, len=1, content=A - 0x01, 0x01, 0x41, // PHONE, len=1, content=B - 0x04, 0x01, 0x42, // END + padding - 0x00, 0x00, - ]), - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 0x10000000, - items: vec![ - SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"A"), - }, - SourceDescriptionItem { - sdes_type: SdesType::SdesPhone, - text: Bytes::from_static(b"B"), - }, - ], - }], - }, - None, - ), - ( - "two chunks", - Bytes::from_static(&[ - // v=2, p=0, count=2, SDES, len=24 - 0x82, 0xca, 0x00, 0x18, // ssrc=0x01020304 - 0x01, 0x02, 0x03, 0x04, - // Chunk 1 - // CNAME, len=1, content=A - 0x01, 0x01, 0x41, // END - 0x00, // Chunk 2 - // SSRC 0x05060708 - 0x05, 0x06, 0x07, 0x08, // CNAME, len=3, content=BCD - 0x01, 0x03, 0x42, 0x43, 0x44, // END - 0x00, 0x00, 0x00, - ]), - SourceDescription { - chunks: vec![ - SourceDescriptionChunk { - source: 0x01020304, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"A"), - }], - }, - SourceDescriptionChunk { - source: 0x05060708, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"BCD"), - }], - }, - ], - }, - None, - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = SourceDescription::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name}: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name}: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_source_description_roundtrip() { - let mut too_long_text = String::new(); - for _ in 0..(1 << 8) { - too_long_text += "x"; - } - - let mut too_many_chunks = vec![]; - for _ in 0..(1 << 5) { - too_many_chunks.push(SourceDescriptionChunk::default()); - } - - let tests = vec![ - ( - "valid", - SourceDescription { - chunks: vec![ - SourceDescriptionChunk { - source: 1, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b"test@example.com"), - }], - }, - SourceDescriptionChunk { - source: 2, - items: vec![ - SourceDescriptionItem { - sdes_type: SdesType::SdesNote, - text: Bytes::from_static(b"some note"), - }, - SourceDescriptionItem { - sdes_type: SdesType::SdesNote, - text: Bytes::from_static(b"another note"), - }, - ], - }, - ], - }, - None, - ), - ( - "item without type", - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesEnd, - text: Bytes::from_static(b"test@example.com"), - }], - }], - }, - Some(Error::SdesMissingType), - ), - ( - "zero items", - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1, - items: vec![], - }], - }, - None, - ), - ( - "email item", - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesEmail, - text: Bytes::from_static(b"test@example.com"), - }], - }], - }, - None, - ), - ( - "empty text", - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::from_static(b""), - }], - }], - }, - None, - ), - ( - "text too long", - SourceDescription { - chunks: vec![SourceDescriptionChunk { - source: 1, - items: vec![SourceDescriptionItem { - sdes_type: SdesType::SdesCname, - text: Bytes::copy_from_slice(too_long_text.as_bytes()), - }], - }], - }, - Some(Error::SdesTextTooLong), - ), - ( - "count overflow", - SourceDescription { - chunks: too_many_chunks, - }, - Some(Error::TooManyChunks), - ), - ]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = SourceDescription::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/transport_feedbacks/mod.rs b/rtcp/src/transport_feedbacks/mod.rs deleted file mode 100644 index f59db6075..000000000 --- a/rtcp/src/transport_feedbacks/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod rapid_resynchronization_request; -pub mod transport_layer_cc; -pub mod transport_layer_nack; diff --git a/rtcp/src/transport_feedbacks/rapid_resynchronization_request/mod.rs b/rtcp/src/transport_feedbacks/rapid_resynchronization_request/mod.rs deleted file mode 100644 index 7ea106755..000000000 --- a/rtcp/src/transport_feedbacks/rapid_resynchronization_request/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -#[cfg(test)] -mod rapid_resynchronization_request_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -const RRR_LENGTH: usize = 2; -const RRR_HEADER_LENGTH: usize = SSRC_LENGTH * 2; -const RRR_MEDIA_OFFSET: usize = 4; - -/// The RapidResynchronizationRequest packet informs the encoder about the loss of an undefined amount of coded video data belonging to one or more pictures -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct RapidResynchronizationRequest { - /// SSRC of sender - pub sender_ssrc: u32, - /// SSRC of the media source - pub media_ssrc: u32, -} - -impl fmt::Display for RapidResynchronizationRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "RapidResynchronizationRequest {:x} {:x}", - self.sender_ssrc, self.media_ssrc - ) - } -} - -impl Packet for RapidResynchronizationRequest { - /// Header returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_RRR, - packet_type: PacketType::TransportSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// Destination SSRC returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.media_ssrc] - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + RRR_HEADER_LENGTH - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for RapidResynchronizationRequest { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for RapidResynchronizationRequest { - /// Marshal encodes the RapidResynchronizationRequest in binary - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - /* - * RRR does not require parameters. Therefore, the length field MUST be - * 2, and there MUST NOT be any Feedback Control Information. - * - * The semantics of this FB message is independent of the payload type. - */ - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for RapidResynchronizationRequest { - /// Unmarshal decodes the RapidResynchronizationRequest from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + (SSRC_LENGTH * 2)) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - if h.packet_type != PacketType::TransportSpecificFeedback || h.count != FORMAT_RRR { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(RapidResynchronizationRequest { - sender_ssrc, - media_ssrc, - }) - } -} diff --git a/rtcp/src/transport_feedbacks/rapid_resynchronization_request/rapid_resynchronization_request_test.rs b/rtcp/src/transport_feedbacks/rapid_resynchronization_request/rapid_resynchronization_request_test.rs deleted file mode 100644 index 6eb86e6a4..000000000 --- a/rtcp/src/transport_feedbacks/rapid_resynchronization_request/rapid_resynchronization_request_test.rs +++ /dev/null @@ -1,116 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_rapid_resynchronization_request_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - 0x85, 0xcd, 0x0, 0x2, // RapidResynchronizationRequest - 0x90, 0x2f, 0x9e, 0x2e, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - ]), - RapidResynchronizationRequest { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - }, - None, - ), - ( - "short report", - Bytes::from_static(&[ - 0x85, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, - // report ends early - ]), - RapidResynchronizationRequest::default(), - Some(Error::PacketTooShort), - ), - ( - "wrong type", - Bytes::from_static(&[ - 0x81, 0xc8, 0x0, 0x7, // v=2, p=0, count=1, SR, len=7 - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e - 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 - 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 - 0x0, 0x0, 0x1, 0x11, // jitter=273 - 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 - 0x0, 0x2, 0x4a, 0x79, // delay=150137 - ]), - RapidResynchronizationRequest::default(), - Some(Error::WrongType), - ), - ( - "nil", - Bytes::from_static(&[]), - RapidResynchronizationRequest::default(), - Some(Error::PacketTooShort), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = RapidResynchronizationRequest::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} rr: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_rapid_resynchronization_request_roundtrip() { - let tests: Vec<(&str, RapidResynchronizationRequest, Option)> = vec![( - "valid", - RapidResynchronizationRequest { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - }, - None, - )]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = RapidResynchronizationRequest::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} diff --git a/rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs b/rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs deleted file mode 100644 index 5256cd83d..000000000 --- a/rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs +++ /dev/null @@ -1,722 +0,0 @@ -#[cfg(test)] -mod transport_layer_cc_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -type Result = std::result::Result; - -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |V=2|P| FMT=15 | PT=205 | length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | SSRC of packet sender | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | SSRC of media source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | base sequence number | packet status count | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | reference time | fb pkt. count | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | packet chunk | packet chunk | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// . . -/// . . -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | packet chunk | recv delta | recv delta | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// . . -/// . . -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | recv delta | recv delta | zero padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -// for packet status chunk -/// type of packet status chunk -#[derive(Default, PartialEq, Eq, Debug, Clone)] -#[repr(u16)] -pub enum StatusChunkTypeTcc { - #[default] - RunLengthChunk = 0, - StatusVectorChunk = 1, -} - -/// type of packet status symbol and recv delta -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -#[repr(u16)] -pub enum SymbolTypeTcc { - /// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.1 - #[default] - PacketNotReceived = 0, - /// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.1 - PacketReceivedSmallDelta = 1, - /// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.1 - PacketReceivedLargeDelta = 2, - /// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - /// see Example 2: "packet received, w/o recv delta" - PacketReceivedWithoutDelta = 3, -} - -/// for status vector chunk -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -#[repr(u16)] -pub enum SymbolSizeTypeTcc { - /// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.4 - #[default] - OneBit = 0, - TwoBit = 1, -} - -impl From for SymbolSizeTypeTcc { - fn from(val: u16) -> Self { - match val { - 0 => SymbolSizeTypeTcc::OneBit, - _ => SymbolSizeTypeTcc::TwoBit, - } - } -} - -impl From for StatusChunkTypeTcc { - fn from(val: u16) -> Self { - match val { - 0 => StatusChunkTypeTcc::RunLengthChunk, - _ => StatusChunkTypeTcc::StatusVectorChunk, - } - } -} - -impl From for SymbolTypeTcc { - fn from(val: u16) -> Self { - match val { - 0 => SymbolTypeTcc::PacketNotReceived, - 1 => SymbolTypeTcc::PacketReceivedSmallDelta, - 2 => SymbolTypeTcc::PacketReceivedLargeDelta, - _ => SymbolTypeTcc::PacketReceivedWithoutDelta, - } - } -} - -/// PacketStatusChunk has two kinds: -/// RunLengthChunk and StatusVectorChunk -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PacketStatusChunk { - RunLengthChunk(RunLengthChunk), - StatusVectorChunk(StatusVectorChunk), -} - -impl MarshalSize for PacketStatusChunk { - fn marshal_size(&self) -> usize { - match self { - PacketStatusChunk::RunLengthChunk(c) => c.marshal_size(), - PacketStatusChunk::StatusVectorChunk(c) => c.marshal_size(), - } - } -} - -impl Marshal for PacketStatusChunk { - /// Marshal .. - fn marshal_to(&self, buf: &mut [u8]) -> Result { - match self { - PacketStatusChunk::RunLengthChunk(c) => c.marshal_to(buf), - PacketStatusChunk::StatusVectorChunk(c) => c.marshal_to(buf), - } - } -} - -/// RunLengthChunk T=TypeTCCRunLengthChunk -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |T| S | Run Length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct RunLengthChunk { - /// T = TypeTCCRunLengthChunk - pub type_tcc: StatusChunkTypeTcc, - /// S: type of packet status - /// kind: TypeTCCPacketNotReceived or... - pub packet_status_symbol: SymbolTypeTcc, - /// run_length: count of S - pub run_length: u16, -} - -impl MarshalSize for RunLengthChunk { - fn marshal_size(&self) -> usize { - PACKET_STATUS_CHUNK_LENGTH - } -} - -impl Marshal for RunLengthChunk { - /// Marshal .. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - // append 1 bit '0' - let mut dst = set_nbits_of_uint16(0, 1, 0, 0)?; - - // append 2 bit packet_status_symbol - dst = set_nbits_of_uint16(dst, 2, 1, self.packet_status_symbol as u16)?; - - // append 13 bit run_length - dst = set_nbits_of_uint16(dst, 13, 3, self.run_length)?; - - buf.put_u16(dst); - - Ok(PACKET_STATUS_CHUNK_LENGTH) - } -} - -impl Unmarshal for RunLengthChunk { - /// Unmarshal .. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < PACKET_STATUS_CHUNK_LENGTH { - return Err(Error::PacketStatusChunkLength.into()); - } - - // record type - let type_tcc = StatusChunkTypeTcc::RunLengthChunk; - - let b0 = raw_packet.get_u8(); - let b1 = raw_packet.get_u8(); - - // get PacketStatusSymbol - let packet_status_symbol = get_nbits_from_byte(b0, 1, 2).into(); - - // get RunLength - let run_length = ((get_nbits_from_byte(b0, 3, 5) as usize) << 8) as u16 + (b1 as u16); - - Ok(RunLengthChunk { - type_tcc, - packet_status_symbol, - run_length, - }) - } -} - -/// StatusVectorChunk T=typeStatusVectorChunk -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |T|S| symbol list | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct StatusVectorChunk { - /// T = TypeTCCRunLengthChunk - pub type_tcc: StatusChunkTypeTcc, - - /// TypeTCCSymbolSizeOneBit or TypeTCCSymbolSizeTwoBit - pub symbol_size: SymbolSizeTypeTcc, - - /// when symbol_size = TypeTCCSymbolSizeOneBit, symbol_list is 14*1bit: - /// TypeTCCSymbolListPacketReceived or TypeTCCSymbolListPacketNotReceived - /// when symbol_size = TypeTCCSymbolSizeTwoBit, symbol_list is 7*2bit: - /// TypeTCCPacketNotReceived TypeTCCPacketReceivedSmallDelta TypeTCCPacketReceivedLargeDelta or typePacketReserved - pub symbol_list: Vec, -} - -impl MarshalSize for StatusVectorChunk { - fn marshal_size(&self) -> usize { - PACKET_STATUS_CHUNK_LENGTH - } -} - -impl Marshal for StatusVectorChunk { - /// Marshal .. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - // set first bit '1' - let mut dst = set_nbits_of_uint16(0, 1, 0, 1)?; - - // set second bit symbol_size - dst = set_nbits_of_uint16(dst, 1, 1, self.symbol_size as u16)?; - - let num_of_bits = NUM_OF_BITS_OF_SYMBOL_SIZE[self.symbol_size as usize]; - // append 14 bit symbol_list - for (i, s) in self.symbol_list.iter().enumerate() { - let index = num_of_bits * (i as u16) + 2; - dst = set_nbits_of_uint16(dst, num_of_bits, index, *s as u16)?; - } - - buf.put_u16(dst); - - Ok(PACKET_STATUS_CHUNK_LENGTH) - } -} - -impl Unmarshal for StatusVectorChunk { - /// Unmarshal .. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < PACKET_STATUS_CHUNK_LENGTH { - return Err(Error::PacketBeforeCname.into()); - } - - let type_tcc = StatusChunkTypeTcc::StatusVectorChunk; - - let b0 = raw_packet.get_u8(); - let b1 = raw_packet.get_u8(); - - let symbol_size = get_nbits_from_byte(b0, 1, 1).into(); - - let mut symbol_list: Vec = vec![]; - match symbol_size { - SymbolSizeTypeTcc::OneBit => { - for i in 0..6u16 { - symbol_list.push(get_nbits_from_byte(b0, 2 + i, 1).into()); - } - - for i in 0..8u16 { - symbol_list.push(get_nbits_from_byte(b1, i, 1).into()) - } - } - - SymbolSizeTypeTcc::TwoBit => { - for i in 0..3u16 { - symbol_list.push(get_nbits_from_byte(b0, 2 + i * 2, 2).into()); - } - - for i in 0..4u16 { - symbol_list.push(get_nbits_from_byte(b1, i * 2, 2).into()); - } - } - } - - Ok(StatusVectorChunk { - type_tcc, - symbol_size, - symbol_list, - }) - } -} - -/// RecvDelta are represented as multiples of 250us -/// small delta is 1 byte: [0,63.75]ms = [0, 63750]us = [0, 255]*250us -/// big delta is 2 bytes: [-8192.0, 8191.75]ms = [-8192000, 8191750]us = [-32768, 32767]*250us -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.5 -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct RecvDelta { - pub type_tcc_packet: SymbolTypeTcc, - /// us - pub delta: i64, -} - -impl MarshalSize for RecvDelta { - fn marshal_size(&self) -> usize { - let delta = self.delta / TYPE_TCC_DELTA_SCALE_FACTOR; - - // small delta - if self.type_tcc_packet == SymbolTypeTcc::PacketReceivedSmallDelta - && delta >= 0 - && delta <= u8::MAX as i64 - { - return 1; - } - - // big delta - if self.type_tcc_packet == SymbolTypeTcc::PacketReceivedLargeDelta - && delta >= i16::MIN as i64 - && delta <= i16::MAX as i64 - { - return 2; - } - - 0 - } -} - -impl Marshal for RecvDelta { - /// Marshal .. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - let delta = self.delta / TYPE_TCC_DELTA_SCALE_FACTOR; - - // small delta - if self.type_tcc_packet == SymbolTypeTcc::PacketReceivedSmallDelta - && delta >= 0 - && delta <= u8::MAX as i64 - && buf.remaining_mut() >= 1 - { - buf.put_u8(delta as u8); - return Ok(1); - } - - // big delta - if self.type_tcc_packet == SymbolTypeTcc::PacketReceivedLargeDelta - && delta >= i16::MIN as i64 - && delta <= i16::MAX as i64 - && buf.remaining_mut() >= 2 - { - buf.put_i16(delta as i16); - return Ok(2); - } - - // overflow - Err(Error::DeltaExceedLimit.into()) - } -} - -impl Unmarshal for RecvDelta { - /// Unmarshal .. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let chunk_len = raw_packet.remaining(); - - // must be 1 or 2 bytes - if chunk_len != 1 && chunk_len != 2 { - return Err(Error::DeltaExceedLimit.into()); - } - - let (type_tcc_packet, delta) = if chunk_len == 1 { - ( - SymbolTypeTcc::PacketReceivedSmallDelta, - TYPE_TCC_DELTA_SCALE_FACTOR * raw_packet.get_u8() as i64, - ) - } else { - ( - SymbolTypeTcc::PacketReceivedLargeDelta, - TYPE_TCC_DELTA_SCALE_FACTOR * raw_packet.get_i16() as i64, - ) - }; - - Ok(RecvDelta { - type_tcc_packet, - delta, - }) - } -} - -/// The offset after header -const BASE_SEQUENCE_NUMBER_OFFSET: usize = 8; -/// The offset after header -const PACKET_STATUS_COUNT_OFFSET: usize = 10; -/// The offset after header -const REFERENCE_TIME_OFFSET: usize = 12; -/// The offset after header -const FB_PKT_COUNT_OFFSET: usize = 15; -/// The offset after header -const PACKET_CHUNK_OFFSET: usize = 16; -/// len of packet status chunk -const TYPE_TCC_STATUS_VECTOR_CHUNK: usize = 1; - -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.5 -pub const TYPE_TCC_DELTA_SCALE_FACTOR: i64 = 250; - -// Notice: RFC is wrong: "packet received" (0) and "packet not received" (1) -// if S == TYPE_TCCSYMBOL_SIZE_ONE_BIT, symbol list will be: TypeTCCPacketNotReceived TypeTCCPacketReceivedSmallDelta -// if S == TYPE_TCCSYMBOL_SIZE_TWO_BIT, symbol list will be same as above: - -static NUM_OF_BITS_OF_SYMBOL_SIZE: [u16; 2] = [1, 2]; - -/// len of packet status chunk -const PACKET_STATUS_CHUNK_LENGTH: usize = 2; - -/// TransportLayerCC for sender-BWE -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct TransportLayerCc { - /// SSRC of sender - pub sender_ssrc: u32, - /// SSRC of the media source - pub media_ssrc: u32, - /// Transport wide sequence of rtp extension - pub base_sequence_number: u16, - /// packet_status_count - pub packet_status_count: u16, - /// reference_time - pub reference_time: u32, - /// fb_pkt_count - pub fb_pkt_count: u8, - /// packet_chunks - pub packet_chunks: Vec, - /// recv_deltas - pub recv_deltas: Vec, -} - -impl fmt::Display for TransportLayerCc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = String::new(); - out += format!("TransportLayerCC:\n\tSender Ssrc {}\n", self.sender_ssrc).as_str(); - out += format!("\tMedia Ssrc {}\n", self.media_ssrc).as_str(); - out += format!("\tBase Sequence Number {}\n", self.base_sequence_number).as_str(); - out += format!("\tStatus Count {}\n", self.packet_status_count).as_str(); - out += format!("\tReference Time {}\n", self.reference_time).as_str(); - out += format!("\tFeedback Packet Count {}\n", self.fb_pkt_count).as_str(); - out += "\tpacket_chunks "; - out += "\n\trecv_deltas "; - for delta in &self.recv_deltas { - out += format!("{delta:?} ").as_str(); - } - out += "\n"; - - write!(f, "{out}") - } -} - -impl Packet for TransportLayerCc { - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_TCC, - packet_type: PacketType::TransportSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.media_ssrc] - } - - fn raw_size(&self) -> usize { - let mut n = HEADER_LENGTH + PACKET_CHUNK_OFFSET + self.packet_chunks.len() * 2; - for d in &self.recv_deltas { - // small delta - if d.type_tcc_packet == SymbolTypeTcc::PacketReceivedSmallDelta { - n += 1; - } else { - n += 2 - } - } - n - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for TransportLayerCc { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for TransportLayerCc { - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - buf.put_u16(self.base_sequence_number); - buf.put_u16(self.packet_status_count); - - let reference_time_and_fb_pkt_count = append_nbits_to_uint32(0, 24, self.reference_time); - let reference_time_and_fb_pkt_count = - append_nbits_to_uint32(reference_time_and_fb_pkt_count, 8, self.fb_pkt_count as u32); - - buf.put_u32(reference_time_and_fb_pkt_count); - - for chunk in &self.packet_chunks { - let n = chunk.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - for delta in &self.recv_deltas { - let n = delta.marshal_to(buf)?; - buf = &mut buf[n..]; - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for TransportLayerCc { - /// Unmarshal .. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - // https://tools.ietf.org/html/rfc4585#page-33 - // header's length + payload's length - let total_length = 4 * (h.length + 1) as usize; - - if total_length < HEADER_LENGTH + PACKET_CHUNK_OFFSET { - return Err(Error::PacketTooShort.into()); - } - - if raw_packet_len < total_length { - return Err(Error::PacketTooShort.into()); - } - - if h.packet_type != PacketType::TransportSpecificFeedback || h.count != FORMAT_TCC { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - let base_sequence_number = raw_packet.get_u16(); - let packet_status_count = raw_packet.get_u16(); - - let mut buf = vec![0u8; 3]; - buf[0] = raw_packet.get_u8(); - buf[1] = raw_packet.get_u8(); - buf[2] = raw_packet.get_u8(); - let reference_time = get_24bits_from_bytes(&buf); - let fb_pkt_count = raw_packet.get_u8(); - let mut packet_chunks = vec![]; - let mut recv_deltas = vec![]; - - let mut packet_status_pos = HEADER_LENGTH + PACKET_CHUNK_OFFSET; - let mut processed_packet_num = 0u16; - while processed_packet_num < packet_status_count { - if packet_status_pos + PACKET_STATUS_CHUNK_LENGTH >= total_length { - return Err(Error::PacketTooShort.into()); - } - - let mut chunk_reader = raw_packet.copy_to_bytes(PACKET_STATUS_CHUNK_LENGTH); - let b0 = chunk_reader[0]; - - let typ = get_nbits_from_byte(b0, 0, 1); - let initial_packet_status: PacketStatusChunk; - match typ.into() { - StatusChunkTypeTcc::RunLengthChunk => { - let packet_status = RunLengthChunk::unmarshal(&mut chunk_reader)?; - - let packet_number_to_process = - (packet_status_count - processed_packet_num).min(packet_status.run_length); - - if packet_status.packet_status_symbol == SymbolTypeTcc::PacketReceivedSmallDelta - || packet_status.packet_status_symbol - == SymbolTypeTcc::PacketReceivedLargeDelta - { - let mut j = 0u16; - - while j < packet_number_to_process { - recv_deltas.push(RecvDelta { - type_tcc_packet: packet_status.packet_status_symbol, - ..Default::default() - }); - - j += 1; - } - } - - initial_packet_status = PacketStatusChunk::RunLengthChunk(packet_status); - processed_packet_num += packet_number_to_process; - } - - StatusChunkTypeTcc::StatusVectorChunk => { - let packet_status = StatusVectorChunk::unmarshal(&mut chunk_reader)?; - - match packet_status.symbol_size { - SymbolSizeTypeTcc::OneBit => { - for sym in &packet_status.symbol_list { - if *sym == SymbolTypeTcc::PacketReceivedSmallDelta { - recv_deltas.push(RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - ..Default::default() - }) - } - } - } - - SymbolSizeTypeTcc::TwoBit => { - for sym in &packet_status.symbol_list { - if *sym == SymbolTypeTcc::PacketReceivedSmallDelta - || *sym == SymbolTypeTcc::PacketReceivedLargeDelta - { - recv_deltas.push(RecvDelta { - type_tcc_packet: *sym, - ..Default::default() - }) - } - } - } - } - - processed_packet_num += packet_status.symbol_list.len() as u16; - initial_packet_status = PacketStatusChunk::StatusVectorChunk(packet_status); - } - } - - packet_status_pos += PACKET_STATUS_CHUNK_LENGTH; - packet_chunks.push(initial_packet_status); - } - - let mut recv_deltas_pos = packet_status_pos; - - for delta in &mut recv_deltas { - if recv_deltas_pos >= total_length { - return Err(Error::PacketTooShort.into()); - } - - if delta.type_tcc_packet == SymbolTypeTcc::PacketReceivedSmallDelta { - let mut delta_reader = raw_packet.take(1); - *delta = RecvDelta::unmarshal(&mut delta_reader)?; - recv_deltas_pos += 1; - } - - if delta.type_tcc_packet == SymbolTypeTcc::PacketReceivedLargeDelta { - let mut delta_reader = raw_packet.take(2); - *delta = RecvDelta::unmarshal(&mut delta_reader)?; - recv_deltas_pos += 2; - } - } - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(TransportLayerCc { - sender_ssrc, - media_ssrc, - base_sequence_number, - packet_status_count, - reference_time, - fb_pkt_count, - packet_chunks, - recv_deltas, - }) - } -} diff --git a/rtcp/src/transport_feedbacks/transport_layer_cc/transport_layer_cc_test.rs b/rtcp/src/transport_feedbacks/transport_layer_cc/transport_layer_cc_test.rs deleted file mode 100644 index e1cc83fb9..000000000 --- a/rtcp/src/transport_feedbacks/transport_layer_cc/transport_layer_cc_test.rs +++ /dev/null @@ -1,927 +0,0 @@ -use bytes::Bytes; - -use super::*; - -#[test] -fn test_transport_layer_cc_run_length_chunk_unmarshal() -> Result<()> { - let tests = vec![ - ( - // 3.1.3 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example1", - Bytes::from_static(&[0, 0xDD]), - RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketNotReceived, - run_length: 221, - }, - ), - ( - // 3.1.3 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example2", - Bytes::from_static(&[0x60, 0x18]), - RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedWithoutDelta, - run_length: 24, - }, - ), - ]; - - for (name, mut data, want) in tests { - let got = RunLengthChunk::unmarshal(&mut data)?; - assert_eq!(got, want, "Unmarshal {name} : err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_run_length_chunk_marshal() -> Result<()> { - let tests = vec![ - ( - // 3.1.3 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example1", - RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketNotReceived, - run_length: 221, - }, - Bytes::from_static(&[0, 0xDD]), - ), - ( - // 3.1.3 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example2", - RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedWithoutDelta, - run_length: 24, - }, - Bytes::from_static(&[0x60, 0x18]), - ), - ]; - - for (name, chunk, want) in tests { - let got = chunk.marshal()?; - assert_eq!(got, want, "Marshal {name}: err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_status_vector_chunk_unmarshal() -> Result<()> { - let tests = vec![ - ( - // 3.1.4 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example1", - Bytes::from_static(&[0x9F, 0x1C]), - StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }, - ), - ( - // 3.1.4 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example2", - Bytes::from_static(&[0xCD, 0x50]), - StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }, - ), - ]; - - for (name, mut data, want) in tests { - let got = StatusVectorChunk::unmarshal(&mut data)?; - assert_eq!(got, want, "Unmarshal {name} : err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_status_vector_chunk_marshal() -> Result<()> { - let tests = vec![ - ( - //3.1.4 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example1", - StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }, - Bytes::from_static(&[0x9F, 0x1C]), - ), - ( - //3.1.4 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 - "example2", - StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }, - Bytes::from_static(&[0xCD, 0x50]), - ), - ]; - - for (name, chunk, want) in tests { - let got = chunk.marshal()?; - assert_eq!(got, want, "Marshal {name}: err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_recv_delta_unmarshal() -> Result<()> { - let tests = vec![ - ( - "small delta 63.75ms", - Bytes::from_static(&[0xFF]), - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - // 255 * 250 - delta: 63750, - }, - ), - ( - "big delta 8191.75ms", - Bytes::from_static(&[0x7F, 0xFF]), - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - // 32767 * 250 - delta: 8191750, - }, - ), - ( - "big delta -8192ms", - Bytes::from_static(&[0x80, 0x00]), - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - // -32768 * 250 - delta: -8192000, - }, - ), - ]; - - for (name, mut data, want) in tests { - let got = RecvDelta::unmarshal(&mut data)?; - assert_eq!(got, want, "Unmarshal {name} : err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_recv_delta_marshal() -> Result<()> { - let tests = vec![ - ( - "small delta 63.75ms", - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - // 255 * 250 - delta: 63750, - }, - Bytes::from_static(&[0xFF]), - ), - ( - "big delta 8191.75ms", - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - // 32767 * 250 - delta: 8191750, - }, - Bytes::from_static(&[0x7F, 0xFF]), - ), - ( - "big delta -8192ms", - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - // -32768 * 250 - delta: -8192000, - }, - Bytes::from_static(&[0x80, 0x00]), - ), - ]; - - for (name, chunk, want) in tests { - let got = chunk.marshal()?; - assert_eq!(got, want, "Marshal {name}: err",); - } - - Ok(()) -} - -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |V=2|P| FMT=15 | PT=205 | length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | SSRC of packet sender | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | SSRC of media source | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | base sequence number | packet status count | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | reference time | fb pkt. count | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | packet chunk | recv delta | recv delta | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// 0b10101111,0b11001101,0b00000000,0b00000101, -/// 0b11111010,0b00010111,0b11111010,0b00010111, -/// 0b01000011,0b00000011,0b00101111,0b10100000, -/// 0b00000000,0b10011001,0b00000000,0b00000001, -/// 0b00111101,0b11101000,0b00000010,0b00010111, -/// 0b00100000,0b00000001,0b10010100,0b00000001, -#[test] -fn test_transport_layer_cc_unmarshal() -> Result<()> { - let tests = vec![ - ( - "example1", - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, - 0x0, 0x1, 0x3d, 0xe8, 0x2, 0x17, 0x20, 0x1, 0x94, 0x1, - ]), - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 1124282272, - base_sequence_number: 153, - packet_status_count: 1, - reference_time: 4057090, - fb_pkt_count: 23, - // 0b00100000, 0b00000001 - packet_chunks: vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 1, - })], - // 0b10010100 - recv_deltas: vec![RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 37000, - }], - }, - ), - ( - "example2", - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, - 0x0, 0xe, 0x45, 0xb1, 0x5a, 0x40, 0xd8, 0x0, 0xf0, 0xff, 0xd0, 0x0, 0x0, 0x3, - ]), - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 372, - packet_status_count: 14, - reference_time: 4567386, - fb_pkt_count: 64, - packet_chunks: vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - ], - }), - ], - // 0b10010100 - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0, - }, - ], - }, - ), - ( - "example3", - Bytes::from_static(&[ - 0x8f, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, - 0x0, 0x6, 0x45, 0xb1, 0x5a, 0x40, 0x40, 0x2, 0x20, 0x04, 0x1f, 0xfe, 0x1f, 0x9a, - 0xd0, 0x0, 0xd0, 0x0, - ]), - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 372, - packet_status_count: 6, - reference_time: 4567386, - fb_pkt_count: 64, - packet_chunks: vec![ - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedLargeDelta, - run_length: 2, - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 4, - }), - ], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 2047500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 2022500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - ], - }, - ), - ( - "example4", - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x4, - 0x0, 0x7, 0x10, 0x63, 0x6e, 0x1, 0x20, 0x7, 0x4c, 0x24, 0x24, 0x10, 0xc, 0xc, 0x10, - 0x0, 0x0, 0x3, - ]), - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 4, - packet_status_count: 7, - reference_time: 1074030, - fb_pkt_count: 1, - packet_chunks: vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 7, - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 19000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 9000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 9000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - ], - }, - ), - ( - "example5", - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x1, - 0x0, 0xe, 0x10, 0x63, 0x6d, 0x0, 0xba, 0x0, 0x10, 0xc, 0xc, 0x10, 0x0, 0x3, - ]), - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 1, - packet_status_count: 14, - reference_time: 1074029, - fb_pkt_count: 0, - packet_chunks: vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - ], - }, - ), - ( - "example6", - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x7, 0x9b, 0x74, 0xf6, 0x1f, 0x93, 0x71, 0xdc, 0xbc, 0x85, 0x3c, - 0x0, 0x9, 0x63, 0xf9, 0x16, 0xb3, 0xd5, 0x52, 0x0, 0x30, 0x9b, 0xaa, 0x6a, 0xaa, - 0x7b, 0x1, 0x9, 0x1, - ]), - TransportLayerCc { - sender_ssrc: 2608133663, - media_ssrc: 2473712828, - base_sequence_number: 34108, - packet_status_count: 9, - reference_time: 6551830, - fb_pkt_count: 179, - packet_chunks: vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedLargeDelta, - ], - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketNotReceived, - run_length: 48, - }), - ], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 38750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 42500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 26500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 42500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 30750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 66250, - }, - ], - }, - ), - ( - "example3", - Bytes::from_static(&[ - 0x8f, 0xcd, 0x0, 0x4, 0x9a, 0xcb, 0x4, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, - ]), - TransportLayerCc { - sender_ssrc: 2596996162, - media_ssrc: 0, - base_sequence_number: 0, - packet_status_count: 0, - reference_time: 0, - fb_pkt_count: 0, - packet_chunks: vec![], - recv_deltas: vec![], - }, - ), - ]; - - for (name, mut data, want) in tests { - let got = TransportLayerCc::unmarshal(&mut data)?; - assert!(got == want, "Unmarshal {name} : err",); - } - - Ok(()) -} - -#[test] -fn test_transport_layer_cc_marshal() -> Result<()> { - let tests = vec![ - ( - "example1", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 1124282272, - base_sequence_number: 153, - packet_status_count: 1, - reference_time: 4057090, - fb_pkt_count: 23, - // 0b00100000, 0b00000001 - packet_chunks: vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 1, - })], - // 0b10010100 - recv_deltas: vec![RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 37000, - }], - }, - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, - 0x0, 0x1, 0x3d, 0xe8, 0x2, 0x17, 0x20, 0x1, 0x94, 0x1, - ]), - ), - ( - "example2", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 372, - packet_status_count: 2, - reference_time: 4567386, - fb_pkt_count: 64, - packet_chunks: vec![ - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedLargeDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - }), - PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::TwoBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - SymbolTypeTcc::PacketReceivedWithoutDelta, - ], - }), - ], - // 0b10010100 - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 0, - }, - ], - }, - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, - 0x0, 0x2, 0x45, 0xb1, 0x5a, 0x40, 0xd8, 0x0, 0xf0, 0xff, 0xd0, 0x0, 0x0, 0x1, - ]), - ), - ( - "example3", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 372, - packet_status_count: 6, - reference_time: 4567386, - fb_pkt_count: 64, - packet_chunks: vec![ - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedLargeDelta, - run_length: 2, - }), - PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 4, - }), - ], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 2047500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedLargeDelta, - delta: 2022500, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 52000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 0, - }, - ], - }, - Bytes::from_static(&[ - 0x8f, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, - 0x0, 0x6, 0x45, 0xb1, 0x5a, 0x40, 0x40, 0x2, 0x20, 0x04, 0x1f, 0xfe, 0x1f, 0x9a, - 0xd0, 0x0, 0xd0, 0x0, - ]), - ), - ( - "example4", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 4, - packet_status_count: 7, - reference_time: 1074030, - fb_pkt_count: 1, - packet_chunks: vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk { - type_tcc: StatusChunkTypeTcc::RunLengthChunk, - packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta, - run_length: 7, - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 19000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 9000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 9000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - ], - }, - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x4, - 0x0, 0x7, 0x10, 0x63, 0x6e, 0x1, 0x20, 0x7, 0x4c, 0x24, 0x24, 0x10, 0xc, 0xc, 0x10, - 0x0, 0x0, 0x3, - ]), - ), - ( - "example5", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 423483579, - base_sequence_number: 1, - packet_status_count: 14, - reference_time: 1074029, - fb_pkt_count: 0, - packet_chunks: vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 3000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 4000, - }, - ], - }, - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x1, - 0x0, 0xe, 0x10, 0x63, 0x6d, 0x0, 0xba, 0x0, 0x10, 0xc, 0xc, 0x10, 0x0, 0x2, - ]), - ), - ( - "example6", - TransportLayerCc { - sender_ssrc: 4195875351, - media_ssrc: 1124282272, - base_sequence_number: 39956, - packet_status_count: 12, - reference_time: 7701536, - fb_pkt_count: 0, - packet_chunks: vec![PacketStatusChunk::StatusVectorChunk(StatusVectorChunk { - type_tcc: StatusChunkTypeTcc::StatusVectorChunk, - symbol_size: SymbolSizeTypeTcc::OneBit, - symbol_list: vec![ - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketReceivedSmallDelta, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - SymbolTypeTcc::PacketNotReceived, - ], - })], - recv_deltas: vec![ - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 48250, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 15750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 14750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 15750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 20750, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 36000, - }, - RecvDelta { - type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta, - delta: 14750, - }, - ], - }, - Bytes::from_static(&[ - 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x9c, 0x14, - 0x0, 0xc, 0x75, 0x84, 0x20, 0x0, 0xbe, 0xc0, 0xc1, 0x3f, 0x3b, 0x3f, 0x53, 0x90, - 0x3b, 0x0, 0x0, 0x3, - ]), - ), - ]; - - for (name, chunk, want) in tests { - let got = chunk.marshal()?; - assert_eq!(got, want, "Marshal {name}: err"); - } - - Ok(()) -} diff --git a/rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs b/rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs deleted file mode 100644 index 05330017d..000000000 --- a/rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs +++ /dev/null @@ -1,275 +0,0 @@ -#[cfg(test)] -mod transport_layer_nack_test; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; -use crate::packet::*; -use crate::util::*; - -/// PacketBitmap shouldn't be used like a normal integral, -/// so it's type is masked here. Access it with PacketList(). -type PacketBitmap = u16; - -/// NackPair is a wire-representation of a collection of -/// Lost RTP packets -#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] -pub struct NackPair { - /// ID of lost packets - pub packet_id: u16, - /// Bitmask of following lost packets - pub lost_packets: PacketBitmap, -} - -pub struct NackIterator { - packet_id: u16, - bitfield: PacketBitmap, - has_yielded_packet_id: bool, -} - -impl Iterator for NackIterator { - type Item = u16; - - fn next(&mut self) -> Option { - if !self.has_yielded_packet_id { - self.has_yielded_packet_id = true; - - Some(self.packet_id) - } else { - let mut i = 0; - - while self.bitfield != 0 { - if (self.bitfield & (1 << i)) != 0 { - self.bitfield &= !(1 << i); - - return Some(self.packet_id.wrapping_add(i + 1)); - } - - i += 1; - } - - None - } - } -} - -impl NackPair { - pub fn new(seq: u16) -> Self { - Self { - packet_id: seq, - lost_packets: Default::default(), - } - } - - /// PacketList returns a list of Nack'd packets that's referenced by a NackPair - pub fn packet_list(&self) -> Vec { - self.into_iter().collect() - } - - pub fn range(&self, f: F) - where - F: Fn(u16) -> bool, - { - for packet_id in self.into_iter() { - if !f(packet_id) { - return; - } - } - } -} - -/// Create an iterator over all the packet sequence numbers expressed by this NACK pair. -impl IntoIterator for NackPair { - type Item = u16; - - type IntoIter = NackIterator; - - fn into_iter(self) -> Self::IntoIter { - NackIterator { - packet_id: self.packet_id, - bitfield: self.lost_packets, - has_yielded_packet_id: false, - } - } -} - -const TLN_LENGTH: usize = 2; -const NACK_OFFSET: usize = 8; - -// The TransportLayerNack packet informs the encoder about the loss of a transport packet -// IETF RFC 4585, Section 6.2.1 -// https://tools.ietf.org/html/rfc4585#section-6.2.1 -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct TransportLayerNack { - /// SSRC of sender - pub sender_ssrc: u32, - /// SSRC of the media source - pub media_ssrc: u32, - - pub nacks: Vec, -} - -impl fmt::Display for TransportLayerNack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = format!("TransportLayerNack from {:x}\n", self.sender_ssrc); - out += format!("\tMedia Ssrc {:x}\n", self.media_ssrc).as_str(); - out += "\tID\tLostPackets\n"; - for nack in &self.nacks { - out += format!("\t{}\t{:b}\n", nack.packet_id, nack.lost_packets).as_str(); - } - write!(f, "{out}") - } -} - -impl Packet for TransportLayerNack { - /// returns the Header associated with this packet. - fn header(&self) -> Header { - Header { - padding: get_padding_size(self.raw_size()) != 0, - count: FORMAT_TLN, - packet_type: PacketType::TransportSpecificFeedback, - length: ((self.marshal_size() / 4) - 1) as u16, - } - } - - /// destination_ssrc returns an array of SSRC values that this packet refers to. - fn destination_ssrc(&self) -> Vec { - vec![self.media_ssrc] - } - - fn raw_size(&self) -> usize { - HEADER_LENGTH + NACK_OFFSET + self.nacks.len() * 4 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn equal(&self, other: &(dyn Packet + Send + Sync)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn cloned(&self) -> Box { - Box::new(self.clone()) - } -} - -impl MarshalSize for TransportLayerNack { - fn marshal_size(&self) -> usize { - let l = self.raw_size(); - // align to 32-bit boundary - l + get_padding_size(l) - } -} - -impl Marshal for TransportLayerNack { - /// Marshal encodes the packet in binary. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if self.nacks.len() + TLN_LENGTH > u8::MAX as usize { - return Err(Error::TooManyReports.into()); - } - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::BufferTooShort.into()); - } - - let h = self.header(); - let n = h.marshal_to(buf)?; - buf = &mut buf[n..]; - - buf.put_u32(self.sender_ssrc); - buf.put_u32(self.media_ssrc); - - for i in 0..self.nacks.len() { - buf.put_u16(self.nacks[i].packet_id); - buf.put_u16(self.nacks[i].lost_packets); - } - - if h.padding { - put_padding(buf, self.raw_size()); - } - - Ok(self.marshal_size()) - } -} - -impl Unmarshal for TransportLayerNack { - /// Unmarshal decodes the ReceptionReport from binary - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < (HEADER_LENGTH + SSRC_LENGTH) { - return Err(Error::PacketTooShort.into()); - } - - let h = Header::unmarshal(raw_packet)?; - - if raw_packet_len < (HEADER_LENGTH + (4 * h.length) as usize) { - return Err(Error::PacketTooShort.into()); - } - - if h.packet_type != PacketType::TransportSpecificFeedback || h.count != FORMAT_TLN { - return Err(Error::WrongType.into()); - } - - let sender_ssrc = raw_packet.get_u32(); - let media_ssrc = raw_packet.get_u32(); - - let mut nacks = vec![]; - for _i in 0..(h.length as i32 - NACK_OFFSET as i32 / 4) { - nacks.push(NackPair { - packet_id: raw_packet.get_u16(), - lost_packets: raw_packet.get_u16(), - }); - } - - if - /*h.padding &&*/ - raw_packet.has_remaining() { - raw_packet.advance(raw_packet.remaining()); - } - - Ok(TransportLayerNack { - sender_ssrc, - media_ssrc, - nacks, - }) - } -} - -pub fn nack_pairs_from_sequence_numbers(seq_nos: &[u16]) -> Vec { - if seq_nos.is_empty() { - return vec![]; - } - - let mut nack_pair = NackPair::new(seq_nos[0]); - let mut pairs = vec![]; - - for &seq in seq_nos.iter().skip(1) { - if seq == nack_pair.packet_id { - continue; - } - if seq <= nack_pair.packet_id || seq > nack_pair.packet_id.saturating_add(16) { - pairs.push(nack_pair); - nack_pair = NackPair::new(seq); - continue; - } - - // Subtraction here is safe because the above checks that seqnum > nack_pair.packet_id. - nack_pair.lost_packets |= 1 << (seq - nack_pair.packet_id - 1); - } - - pairs.push(nack_pair); - - pairs -} diff --git a/rtcp/src/transport_feedbacks/transport_layer_nack/transport_layer_nack_test.rs b/rtcp/src/transport_feedbacks/transport_layer_nack/transport_layer_nack_test.rs deleted file mode 100644 index 40d833b86..000000000 --- a/rtcp/src/transport_feedbacks/transport_layer_nack/transport_layer_nack_test.rs +++ /dev/null @@ -1,361 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use bytes::Bytes; - -use super::*; - -#[test] -fn test_transport_layer_nack_unmarshal() { - let tests = vec![ - ( - "valid", - Bytes::from_static(&[ - // TransportLayerNack - 0x81, 0xcd, 0x0, 0x3, // sender=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // nack 0xAAAA, 0x5555 - 0xaa, 0xaa, 0x55, 0x55, - ]), - TransportLayerNack { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - nacks: vec![NackPair { - packet_id: 0xaaaa, - lost_packets: 0x5555, - }], - }, - None, - ), - ( - "short report", - Bytes::from_static(&[ - 0x81, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, - // report ends early - ]), - TransportLayerNack::default(), - Some(Error::PacketTooShort), - ), - ( - "wrong type", - Bytes::from_static(&[ - // v=2, p=0, count=1, SR, len=7 - 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e - 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 - 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 - 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 - 0x0, 0x0, 0x46, 0xe1, // jitter=273 - 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 - 0x9, 0xf3, 0x64, 0x32, // delay=150137 - 0x0, 0x2, 0x4a, 0x79, - ]), - TransportLayerNack::default(), - Some(Error::WrongType), - ), - ( - "nil", - Bytes::from_static(&[]), - TransportLayerNack::default(), - Some(Error::PacketTooShort), - ), - ]; - - for (name, mut data, want, want_error) in tests { - let got = TransportLayerNack::unmarshal(&mut data); - - assert_eq!( - got.is_err(), - want_error.is_some(), - "Unmarshal {name} rr: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let actual = got.unwrap(); - assert_eq!( - actual, want, - "Unmarshal {name} rr: got {actual:?}, want {want:?}" - ); - } - } -} - -#[test] -fn test_transport_layer_nack_roundtrip() { - let tests: Vec<(&str, TransportLayerNack, Option)> = vec![( - "valid", - TransportLayerNack { - sender_ssrc: 0x902f9e2e, - media_ssrc: 0x902f9e2e, - nacks: vec![ - NackPair { - packet_id: 1, - lost_packets: 0xAA, - }, - NackPair { - packet_id: 1034, - lost_packets: 0x05, - }, - ], - }, - None, - )]; - - for (name, want, want_error) in tests { - let got = want.marshal(); - - assert_eq!( - got.is_ok(), - want_error.is_none(), - "Marshal {name}: err = {got:?}, want {want_error:?}" - ); - - if let Some(err) = want_error { - let got_err = got.err().unwrap(); - assert_eq!( - err, got_err, - "Unmarshal {name} rr: err = {got_err:?}, want {err:?}", - ); - } else { - let mut data = got.ok().unwrap(); - let actual = TransportLayerNack::unmarshal(&mut data) - .unwrap_or_else(|_| panic!("Unmarshal {name}")); - - assert_eq!( - actual, want, - "{name} round trip: got {actual:?}, want {want:?}" - ) - } - } -} - -#[test] -fn test_nack_pair() { - let test_nack = |s: Vec, n: NackPair| { - let l = n.packet_list(); - - assert_eq!(s, l, "{n:?}: expected {s:?}, got {l:?}"); - }; - - test_nack( - vec![42], - NackPair { - packet_id: 42, - lost_packets: 0, - }, - ); - - test_nack( - vec![42, 43], - NackPair { - packet_id: 42, - lost_packets: 1, - }, - ); - - test_nack( - vec![42, 44], - NackPair { - packet_id: 42, - lost_packets: 2, - }, - ); - - test_nack( - vec![42, 43, 44], - NackPair { - packet_id: 42, - lost_packets: 3, - }, - ); - - test_nack( - vec![42, 42 + 16], - NackPair { - packet_id: 42, - lost_packets: 0x8000, - }, - ); - - // Wrap around - test_nack( - vec![65534, 65535, 0, 1], - NackPair { - packet_id: 65534, - lost_packets: 0b0000_0111, - }, - ); - - // Gap - test_nack( - vec![123, 125, 127, 129], - NackPair { - packet_id: 123, - lost_packets: 0b0010_1010, - }, - ); -} - -#[test] -fn test_nack_pair_range() { - let n = NackPair { - packet_id: 42, - lost_packets: 2, - }; - - let out = Arc::new(Mutex::new(vec![])); - let out1 = Arc::clone(&out); - n.range(move |s: u16| -> bool { - let out2 = Arc::clone(&out1); - let mut o = out2.lock().unwrap(); - o.push(s); - true - }); - - { - let o = out.lock().unwrap(); - assert_eq!(*o, &[42, 44]); - } - - let out = Arc::new(Mutex::new(vec![])); - let out1 = Arc::clone(&out); - n.range(move |s: u16| -> bool { - let out2 = Arc::clone(&out1); - let mut o = out2.lock().unwrap(); - o.push(s); - false - }); - - { - let o = out.lock().unwrap(); - assert_eq!(*o, &[42]); - } -} - -#[test] -fn test_transport_layer_nack_pair_generation() { - let test = vec![ - ("No Sequence Numbers", vec![], vec![]), - ( - "Single Sequence Number", - vec![100u16], - vec![NackPair { - packet_id: 100, - lost_packets: 0x0, - }], - ), - // Make sure it doesn't crash. - ( - "Single Sequence Number (duplicates)", - vec![100u16, 100], - vec![NackPair { - packet_id: 100, - lost_packets: 0x0, - }], - ), - ( - "Multiple in range, Single NACKPair", - vec![100, 101, 105, 115], - vec![NackPair { - packet_id: 100, - lost_packets: 0x4011, - }], - ), - ( - "Multiple Ranges, Multiple NACKPair", - vec![100, 117, 500, 501, 502], - vec![ - NackPair { - packet_id: 100, - lost_packets: 0, - }, - NackPair { - packet_id: 117, - lost_packets: 0, - }, - NackPair { - packet_id: 500, - lost_packets: 0x3, - }, - ], - ), - ( - "Multiple Ranges, Multiple NACKPair", - vec![100, 117, 500, 501, 502], - vec![ - NackPair { - packet_id: 100, - lost_packets: 0, - }, - NackPair { - packet_id: 117, - lost_packets: 0, - }, - NackPair { - packet_id: 500, - lost_packets: 0x3, - }, - ], - ), - ( - "Multiple Ranges, Multiple NACKPair (with rollover)", - vec![100, 117, 65534, 65535, 0, 1, 99], - vec![ - NackPair { - packet_id: 100, - lost_packets: 0, - }, - NackPair { - packet_id: 117, - lost_packets: 0, - }, - NackPair { - packet_id: 65534, - lost_packets: 1, - }, - NackPair { - packet_id: 0, - lost_packets: 1, - }, - NackPair { - packet_id: 99, - lost_packets: 0, - }, - ], - ), - ]; - - for (name, seq_numbers, expected) in test { - let actual = nack_pairs_from_sequence_numbers(&seq_numbers); - - assert_eq!( - actual, expected, - "{name} NackPair generation mismatch: got {actual:#?}, want {expected:#?}" - ) - } -} - -/// This test case reproduced a bug in the implementation -#[test] -fn test_lost_packets_is_reset_when_crossing_16_bit_boundary() { - let seq: Vec<_> = (0u16..=17u16).collect(); - assert_eq!( - nack_pairs_from_sequence_numbers(&seq), - vec![ - NackPair { - packet_id: 0, - lost_packets: 0b1111_1111_1111_1111, - }, - NackPair { - packet_id: 17, - // Was 0xffff before fixing the bug - lost_packets: 0b0000_0000_0000_0000, - } - ], - ) -} diff --git a/rtcp/src/util.rs b/rtcp/src/util.rs deleted file mode 100644 index 44c1096de..000000000 --- a/rtcp/src/util.rs +++ /dev/null @@ -1,118 +0,0 @@ -use bytes::BufMut; - -use crate::error::{Error, Result}; - -// returns the padding required to make the length a multiple of 4 -pub(crate) fn get_padding_size(len: usize) -> usize { - if len % 4 == 0 { - 0 - } else { - 4 - (len % 4) - } -} - -pub(crate) fn put_padding(mut buf: &mut [u8], len: usize) { - let padding_size = get_padding_size(len); - for i in 0..padding_size { - if i == padding_size - 1 { - buf.put_u8(padding_size as u8); - } else { - buf.put_u8(0); - } - } -} - -// set_nbits_of_uint16 will truncate the value to size, left-shift to start_index position and set -pub(crate) fn set_nbits_of_uint16( - src: u16, - size: u16, - start_index: u16, - mut val: u16, -) -> Result { - if start_index + size > 16 { - return Err(Error::InvalidSizeOrStartIndex); - } - - // truncate val to size bits - val &= (1 << size) - 1; - - Ok(src | (val << (16 - size - start_index))) -} - -// appendBit32 will left-shift and append n bits of val -pub(crate) fn append_nbits_to_uint32(src: u32, n: u32, val: u32) -> u32 { - (src << n) | (val & (0xFFFFFFFF >> (32 - n))) -} - -// getNBit get n bits from 1 byte, begin with a position -pub(crate) fn get_nbits_from_byte(b: u8, begin: u16, n: u16) -> u16 { - let end_shift = 8 - (begin + n); - let mask = (0xFF >> begin) & (0xFF << end_shift) as u8; - (b & mask) as u16 >> end_shift -} - -// get24BitFromBytes get 24bits from `[3]byte` slice -pub(crate) fn get_24bits_from_bytes(b: &[u8]) -> u32 { - ((b[0] as u32) << 16) + ((b[1] as u32) << 8) + (b[2] as u32) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_get_padding() -> Result<()> { - let tests = vec![(0, 0), (1, 3), (2, 2), (3, 1), (4, 0), (100, 0), (500, 0)]; - - for (n, p) in tests { - assert_eq!( - get_padding_size(n), - p, - "Test case returned wrong value for input {n}" - ); - } - - Ok(()) - } - - #[test] - fn test_set_nbits_of_uint16() -> Result<()> { - let tests = vec![ - ("setOneBit", 0, 1, 8, 1, 128, None), - ("setStatusVectorBit", 0, 1, 0, 1, 32768, None), - ("setStatusVectorSecondBit", 32768, 1, 1, 1, 49152, None), - ( - "setStatusVectorInnerBitsAndCutValue", - 49152, - 2, - 6, - 11111, - 49920, - None, - ), - ("setRunLengthSecondTwoBit", 32768, 2, 1, 1, 40960, None), - ( - "setOneBitOutOfBounds", - 32768, - 2, - 15, - 1, - 0, - Some("invalid size or startIndex"), - ), - ]; - - for (name, source, size, index, value, result, err) in tests { - let res = set_nbits_of_uint16(source, size, index, value); - if err.is_some() { - assert!(res.is_err(), "setNBitsOfUint16 {name} : should be error"); - } else if let Ok(got) = res { - assert_eq!(got, result, "setNBitsOfUint16 {name}"); - } else { - panic!("setNBitsOfUint16 {name} :unexpected error result"); - } - } - - Ok(()) - } -} diff --git a/rtp/.gitignore b/rtp/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/rtp/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/rtp/CHANGELOG.md b/rtp/CHANGELOG.md deleted file mode 100644 index 52d87874c..000000000 --- a/rtp/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# rtp changelog - -## Unreleased - -## v0.6.8 - -* Increased minimum support rust version to `1.60.0`. -* Adds a new generic header extensions type `rtp::extension::HeaderExtension` which allows abstracting over all known extensions as well as custom extensions. [#336](https://github.com/webrtc-rs/webrtc/pull/336) by [@k0nserv](https://github.com/k0nserv). -* Added video orientation(`urn:3gpp:video-orientation`) extension support. [#331](https://github.com/webrtc-rs/webrtc/pull/331) by [@algesten](https://github.com/algesten). -* Allow RTP extensions to be serialized and deserialized via serder. [#332](https://github.com/webrtc-rs/webrtc/pull/332) by [@algesten](https://github.com/algesten). -* Increased required `webrtc-util` version to `0.7.0`. - -## v0.6.7 - -* Bumped util dependency to `0.6.0`. - -## Prior to 0.6.7 - -Before 0.6.7 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/rtp/releases). - diff --git a/rtp/Cargo.toml b/rtp/Cargo.toml deleted file mode 100644 index 6c54464aa..000000000 --- a/rtp/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "rtp" -version = "0.11.0" -authors = ["Rain Liu ", "Michael Uti "] -edition = "2021" -description = "A pure Rust implementation of RTP" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/rtp" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/rtp" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["marshal"] } - -bytes = "1" -rand = "0.8" -thiserror = "1" -serde = { version = "1", features = ["derive"] } -portable-atomic = "1.6" - -memchr = "2.1.1" - -[dev-dependencies] -chrono = "0.4.28" -criterion = "0.5" - -[[bench]] -name = "packet_bench" -harness = false diff --git a/rtp/LICENSE-APACHE b/rtp/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/rtp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/rtp/LICENSE-MIT b/rtp/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/rtp/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/rtp/README.md b/rtp/README.md deleted file mode 100644 index c11c2f972..000000000 --- a/rtp/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of RTP. Rewrite Pion RTP in Rust -

diff --git a/rtp/benches/packet_bench.rs b/rtp/benches/packet_bench.rs deleted file mode 100644 index 128728e73..000000000 --- a/rtp/benches/packet_bench.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Silence warning on `..Default::default()` with no effect: -#![allow(clippy::needless_update)] - -use bytes::{Bytes, BytesMut}; -use criterion::{criterion_group, criterion_main, Criterion}; -use rtp::header::*; -use rtp::packet::*; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -fn benchmark_packet(c: &mut Criterion) { - let pkt = Packet { - header: Header { - extension: true, - csrc: vec![1, 2], - extension_profile: EXTENSION_PROFILE_TWO_BYTE, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[3, 4]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[5, 6]), - }, - ], - ..Default::default() - }, - payload: Bytes::from_static(&[0xFFu8; 15]), //vec![0x07, 0x08, 0x09, 0x0a], //MTU=1500 - ..Default::default() - }; - let raw = pkt.marshal().unwrap(); - let buf = &mut raw.clone(); - let p = Packet::unmarshal(buf).unwrap(); - if pkt != p { - panic!("marshal or unmarshal not correct: \npkt: {pkt:?} \nvs \np: {p:?}"); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - let mut buf = BytesMut::with_capacity(pkt.marshal_size()); - buf.resize(pkt.marshal_size(), 0); - c.bench_function("Benchmark MarshalTo", |b| { - b.iter(|| { - let _ = pkt.marshal_to(&mut buf).unwrap(); - }) - }); - - c.bench_function("Benchmark Marshal", |b| { - b.iter(|| { - let _ = pkt.marshal().unwrap(); - }) - }); - - c.bench_function("Benchmark Unmarshal ", |b| { - b.iter(|| { - let buf = &mut raw.clone(); - let _ = Packet::unmarshal(buf).unwrap(); - }) - }); -} - -criterion_group!(benches, benchmark_packet); -criterion_main!(benches); diff --git a/rtp/codecov.yml b/rtp/codecov.yml deleted file mode 100644 index 93bdf2942..000000000 --- a/rtp/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: cd48e18f-3916-4a20-ba56-81354d68a5d2 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/rtp/doc/webrtc.rs.png b/rtp/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/rtp/src/codecs/av1/av1_test.rs b/rtp/src/codecs/av1/av1_test.rs deleted file mode 100644 index 8734cae31..000000000 --- a/rtp/src/codecs/av1/av1_test.rs +++ /dev/null @@ -1,454 +0,0 @@ -use crate::codecs::av1::obu::{ - OBU_HAS_EXTENSION_BIT, OBU_TYPE_FRAME, OBU_TYPE_FRAME_HEADER, OBU_TYPE_METADATA, - OBU_TYPE_SEQUENCE_HEADER, OBU_TYPE_TEMPORAL_DELIMITER, OBU_TYPE_TILE_GROUP, OBU_TYPE_TILE_LIST, -}; -use crate::error::Result; - -use super::*; - -const OBU_EXTENSION_S1T1: u8 = 0b0010_1000; -const NEW_CODED_VIDEO_SEQUENCE_BIT: u8 = 0b0000_1000; - -struct Av1Obu { - header: u8, - extension: u8, - payload: Vec, -} - -impl Av1Obu { - pub fn new(obu_type: u8) -> Self { - Self { - header: obu_type << 3 | OBU_HAS_SIZE_BIT, - extension: 0, - payload: vec![], - } - } - - pub fn with_extension(mut self, extension: u8) -> Self { - self.extension = extension; - self.header |= OBU_HAS_EXTENSION_BIT; - self - } - - pub fn without_size(mut self) -> Self { - self.header &= !OBU_HAS_SIZE_BIT; - self - } - - pub fn with_payload(mut self, payload: Vec) -> Self { - self.payload = payload; - self - } -} - -fn build_av1_frame(obus: &Vec) -> Bytes { - let mut raw = vec![]; - for obu in obus { - raw.push(obu.header); - if obu.header & OBU_HAS_EXTENSION_BIT != 0 { - raw.push(obu.extension); - } - if obu.header & OBU_HAS_SIZE_BIT != 0 { - // write size in leb128 format. - let mut payload_size = obu.payload.len(); - while payload_size >= 0b1000_0000 { - raw.push(0b1000_0000 | (payload_size & 0b0111_1111) as u8); - payload_size >>= 7; - } - raw.push(payload_size as u8); - } - raw.extend_from_slice(&obu.payload); - } - Bytes::from(raw) -} - -#[test] -fn test_packetize_one_obu_without_size_and_extension() -> Result<()> { - let frame = build_av1_frame(&vec![Av1Obu::new(OBU_TYPE_FRAME) - .without_size() - .with_payload(vec![1, 2, 3, 4, 5, 6, 7])]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0001_0000, // aggregation header - OBU_TYPE_FRAME << 3, // header - 1, - 2, - 3, - 4, - 5, - 6, - 7 - ]] - ); - Ok(()) -} - -#[test] -fn test_packetize_one_obu_without_size_with_extension() -> Result<()> { - let frame = build_av1_frame(&vec![Av1Obu::new(OBU_TYPE_FRAME) - .without_size() - .with_extension(OBU_EXTENSION_S1T1) - .with_payload(vec![2, 3, 4, 5, 6, 7])]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0001_0000, // aggregation header - OBU_TYPE_FRAME << 3 | OBU_HAS_EXTENSION_BIT, // header - OBU_EXTENSION_S1T1, // extension header - 2, - 3, - 4, - 5, - 6, - 7 - ]] - ); - Ok(()) -} - -#[test] -fn removes_obu_size_field_without_extension() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![11, 12, 13, 14, 15, 16, 17]) - ]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0001_0000, // aggregation header - OBU_TYPE_FRAME << 3, // header - 11, - 12, - 13, - 14, - 15, - 16, - 17 - ]] - ); - Ok(()) -} - -#[test] -fn removes_obu_size_field_with_extension() -> Result<()> { - let frame = build_av1_frame(&vec![Av1Obu::new(OBU_TYPE_FRAME) - .with_extension(OBU_EXTENSION_S1T1) - .with_payload(vec![1, 2, 3, 4, 5, 6, 7])]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0001_0000, // aggregation header - OBU_TYPE_FRAME << 3 | OBU_HAS_EXTENSION_BIT, // header - OBU_EXTENSION_S1T1, // extension header - 1, - 2, - 3, - 4, - 5, - 6, - 7 - ]] - ); - Ok(()) -} - -#[test] -fn test_omits_size_for_last_obu_when_three_obus_fits_into_the_packet() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_SEQUENCE_HEADER).with_payload(vec![1, 2, 3, 4, 5, 6]), - Av1Obu::new(OBU_TYPE_METADATA).with_payload(vec![11, 12, 13, 14]), - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![21, 22, 23, 24, 25, 26]), - ]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0011_1000, // aggregation header - 7, // size of the first OBU - OBU_TYPE_SEQUENCE_HEADER << 3, // header of the first OBU - 1, - 2, - 3, - 4, - 5, - 6, - 5, // size of the second OBU - OBU_TYPE_METADATA << 3, // header of the second OBU - 11, - 12, - 13, - 14, - OBU_TYPE_FRAME << 3, // header of the third OBU - 21, - 22, - 23, - 24, - 25, - 26, - ]] - ); - Ok(()) -} - -#[test] -fn test_use_size_for_all_obus_when_four_obus_fits_into_the_packet() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_SEQUENCE_HEADER).with_payload(vec![1, 2, 3, 4, 5, 6]), - Av1Obu::new(OBU_TYPE_METADATA).with_payload(vec![11, 12, 13, 14]), - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![21, 22, 23]), - Av1Obu::new(OBU_TYPE_TILE_GROUP).with_payload(vec![31, 32, 33, 34, 35, 36]), - ]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0000_1000, // aggregation header - 7, // size of the first OBU - OBU_TYPE_SEQUENCE_HEADER << 3, // header of the first OBU - 1, - 2, - 3, - 4, - 5, - 6, - 5, // size of the second OBU - OBU_TYPE_METADATA << 3, // header of the second OBU - 11, - 12, - 13, - 14, - 4, // size of the third OBU - OBU_TYPE_FRAME << 3, // header of the third OBU - 21, - 22, - 23, - 7, // size of the fourth OBU - OBU_TYPE_TILE_GROUP << 3, // header of the fourth OBU - 31, - 32, - 33, - 34, - 35, - 36 - ]] - ); - Ok(()) -} - -#[test] -fn test_discards_temporal_delimiter_and_tile_list_obu() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_TEMPORAL_DELIMITER), - Av1Obu::new(OBU_TYPE_METADATA), - Av1Obu::new(OBU_TYPE_TILE_LIST).with_payload(vec![1, 2, 3, 4, 5, 6]), - Av1Obu::new(OBU_TYPE_FRAME_HEADER).with_payload(vec![21, 22, 23]), - Av1Obu::new(OBU_TYPE_TILE_GROUP).with_payload(vec![31, 32, 33, 34, 35, 36]), - ]); - let mut payloader = Av1Payloader {}; - assert_eq!( - payloader.payload(1200, &frame)?, - vec![vec![ - 0b0011_0000, // aggregation header - 1, // size of the first OBU - OBU_TYPE_METADATA << 3, // header of the first OBU - 4, // size of the second OBU - OBU_TYPE_FRAME_HEADER << 3, // header of the second OBU - 21, - 22, - 23, - OBU_TYPE_TILE_GROUP << 3, // header of the fourth OBU - 31, - 32, - 33, - 34, - 35, - 36 - ]] - ); - Ok(()) -} - -#[test] -fn test_split_two_obus_into_two_packet_force_split_obu_header() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_FRAME_HEADER) - .with_extension(OBU_EXTENSION_S1T1) - .with_payload(vec![21]), - Av1Obu::new(OBU_TYPE_TILE_GROUP) - .with_extension(OBU_EXTENSION_S1T1) - .with_payload(vec![11, 12, 13, 14]), - ]); - let mut payloader = Av1Payloader {}; - - // Craft expected payloads so that there is only one way to split original - // frame into two packets. - assert_eq!( - payloader.payload(6, &frame)?, - vec![ - vec![ - 0b0110_0000, // aggregation header - 3, // size of the first OBU - OBU_TYPE_FRAME_HEADER << 3 | OBU_HAS_EXTENSION_BIT, // header of the first OBU - OBU_EXTENSION_S1T1, // extension header - 21, - OBU_TYPE_TILE_GROUP << 3 | OBU_HAS_EXTENSION_BIT, // header of the second OBU - ], - vec![ - 0b1001_0000, // aggregation header - OBU_EXTENSION_S1T1, - 11, - 12, - 13, - 14 - ] - ] - ); - Ok(()) -} - -#[test] -fn test_sets_n_bit_at_the_first_packet_of_a_key_frame_with_sequence_header() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_SEQUENCE_HEADER).with_payload(vec![1, 2, 3, 4, 5, 6, 7]) - ]); - let mut payloader = Av1Payloader {}; - let result = payloader.payload(6, &frame)?; - assert_eq!(result.len(), 2); - assert_eq!( - result[0][0] & NEW_CODED_VIDEO_SEQUENCE_BIT, - NEW_CODED_VIDEO_SEQUENCE_BIT - ); - assert_eq!(result[1][0] & NEW_CODED_VIDEO_SEQUENCE_BIT, 0); - Ok(()) -} - -#[test] -fn test_doesnt_set_n_bit_at_the_packets_of_a_key_frame_without_sequence_header() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![1, 2, 3, 4, 5, 6, 7]) - ]); - let mut payloader = Av1Payloader {}; - let result = payloader.payload(6, &frame)?; - assert_eq!(result.len(), 2); - assert_eq!(result[0][0] & NEW_CODED_VIDEO_SEQUENCE_BIT, 0); - assert_eq!(result[1][0] & NEW_CODED_VIDEO_SEQUENCE_BIT, 0); - Ok(()) -} - -#[test] -fn test_doesnt_set_n_bit_at_the_packets_of_a_delta_frame() -> Result<()> { - // TODO: implement delta frame detection. - Ok(()) -} - -#[test] -fn test_split_single_obu_into_two_packets() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![11, 12, 13, 14, 15, 16, 17, 18, 19]) - ]); - let mut payloader = Av1Payloader {}; - // let result = payloader.payload(8, &frame)?; - // println!("{:?}", result[0].to_vec()); - // println!("{:?}", result[1].to_vec()); - assert_eq!( - payloader.payload(8, &frame)?, - vec![ - vec![ - 0b0101_0000, // aggregation header - OBU_TYPE_FRAME << 3, // header - 11, - 12, - 13, - 14, - 15, - 16 - ], - vec![ - 0b1001_0000, // aggregation header - 17, - 18, - 19 - ], - ] - ); - - Ok(()) -} - -#[test] -fn test_split_single_obu_into_many_packets() -> Result<()> { - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![27; 1200]) - ]); - let mut payloader = Av1Payloader {}; - let result = payloader.payload(100, &frame)?; - assert_eq!(result.len(), 13); - assert_eq!(result[0], { - let mut ret = vec![ - 0b0101_0000, // aggregation header - OBU_TYPE_FRAME << 3, // header - ]; - ret.extend(vec![27; 98]); - ret - }); - for packet in result.iter().take(12).skip(1) { - assert_eq!(packet.to_vec(), { - let mut ret = vec![ - 0b1101_0000, // aggregation header - ]; - ret.extend(vec![27; 99]); - ret - }); - } - assert_eq!(result[12], { - let mut ret = vec![ - 0b1001_0000, // aggregation header - ]; - ret.extend(vec![27; 13]); - ret - }); - - Ok(()) -} - -#[test] -fn test_split_two_obus_into_two_packets() -> Result<()> { - // 2nd OBU is too large to fit into one packet, so its head would be in the - // same packet as the 1st OBU. - let frame = build_av1_frame(&vec![ - Av1Obu::new(OBU_TYPE_SEQUENCE_HEADER).with_payload(vec![11, 12]), - Av1Obu::new(OBU_TYPE_FRAME).with_payload(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]), - ]); - let mut payloader = Av1Payloader {}; - let result = payloader.payload(8, &frame)?; - assert_eq!( - result, - vec![ - vec![ - 0b0110_1000, // aggregation header - 3, // size of the first OBU - OBU_TYPE_SEQUENCE_HEADER << 3, // header - 11, - 12, - OBU_TYPE_FRAME << 3, // header of the second OBU - 1, - 2 - ], - vec![ - 0b1001_0000, // aggregation header - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ] - ] - ); - Ok(()) -} diff --git a/rtp/src/codecs/av1/leb128.rs b/rtp/src/codecs/av1/leb128.rs deleted file mode 100644 index 677524f5d..000000000 --- a/rtp/src/codecs/av1/leb128.rs +++ /dev/null @@ -1,64 +0,0 @@ -use bytes::{BufMut, Bytes, BytesMut}; - -pub fn encode_leb128(mut val: u32) -> u32 { - let mut b = 0; - loop { - b |= val & 0b_0111_1111; - val >>= 7; - if val != 0 { - b |= 0b_1000_0000; - b <<= 8; - } else { - return b; - } - } -} - -pub fn decode_leb128(mut val: u32) -> u32 { - let mut b = 0; - loop { - b |= val & 0b_0111_1111; - val >>= 8; - if val == 0 { - return b; - } - b <<= 7; - } -} - -pub fn read_leb128(bytes: &Bytes) -> (u32, usize) { - let mut encoded = 0; - for i in 0..bytes.len() { - encoded |= bytes[i] as u32; - if bytes[i] & 0b_1000_0000 == 0 { - return (decode_leb128(encoded), i + 1); - } - encoded <<= 8; - } - (0, 0) -} - -pub fn leb128_size(value: u32) -> usize { - let mut size = 0; - let mut value = value; - while value >= 0b_1000_0000 { - size += 1; - value >>= 7; - } - size + 1 -} - -pub trait BytesMutExt { - fn put_leb128(&mut self, n: u32); -} - -impl BytesMutExt for BytesMut { - fn put_leb128(&mut self, n: u32) { - let mut encoded = encode_leb128(n); - while encoded >= 0b_1000_0000 { - self.put_u8(0b_1000_0000 | (encoded & 0b_0111_1111) as u8); - encoded >>= 7; - } - self.put_u8(encoded as u8); - } -} diff --git a/rtp/src/codecs/av1/mod.rs b/rtp/src/codecs/av1/mod.rs deleted file mode 100644 index 42358c3b0..000000000 --- a/rtp/src/codecs/av1/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -use bytes::{BufMut, Bytes, BytesMut}; - -use crate::codecs::av1::leb128::BytesMutExt; -use crate::codecs::av1::obu::{obu_has_extension, parse_obus, OBU_HAS_SIZE_BIT}; -use crate::codecs::av1::packetizer::{ - get_aggregation_header, packetize, AGGREGATION_HEADER_SIZE, MAX_NUM_OBUS_TO_OMIT_SIZE, -}; -use crate::packetizer::Payloader; - -#[cfg(test)] -mod av1_test; -mod leb128; -mod obu; -mod packetizer; - -#[derive(Default, Clone, Debug)] -pub struct Av1Payloader {} - -impl Payloader for Av1Payloader { - /// Based on - /// Reference: - fn payload(&mut self, mtu: usize, payload: &Bytes) -> crate::error::Result> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // |Z|Y|1 0|N|-|-|-| OBU element 1 size (leb128) | | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | - // | | - // : : - // : OBU element 1 data : - // : : - // | | - // | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | | | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | - // | | - // : : - // : OBU element 2 data : - // : : - // | | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - // Parse the payload into series of OBUs. - let obus = parse_obus(payload)?; - - // Packetize the OBUs, possibly aggregating multiple OBUs into a single packet, - // or splitting a single OBU across multiple packets. - let packets_metadata = packetize(&obus, mtu); - - let mut payloads = vec![]; - - // Split the payload into RTP packets according to the packetization scheme. - for packet_index in 0..packets_metadata.len() { - let packet = &packets_metadata[packet_index]; - let mut obu_offset = packet.first_obu_offset; - let aggregation_header = get_aggregation_header(&obus, &packets_metadata, packet_index); - - let mut out = BytesMut::with_capacity(AGGREGATION_HEADER_SIZE + packet.packet_size); - out.put_u8(aggregation_header); - - // Store all OBU elements except the last one. - for i in 0..packet.num_obu_elements - 1 { - let obu = &obus[packet.first_obu_index + i]; - let fragment_size = obu.size - obu_offset; - out.put_leb128(fragment_size as u32); - if obu_offset == 0 { - out.put_u8(obu.header & !OBU_HAS_SIZE_BIT); - } - if obu_offset <= 1 && obu_has_extension(obu.header) { - out.put_u8(obu.extension_header); - } - let payload_offset = if obu_offset > obu.header_size() { - obu_offset - obu.header_size() - } else { - 0 - }; - let payload_size = obu.payload.len() - payload_offset; - out.put_slice( - obu.payload - .slice(payload_offset..payload_offset + payload_size) - .as_ref(), - ); - // All obus are stored from the beginning, except, may be, the first one. - obu_offset = 0; - } - - // Store the last OBU element. - let last_obu = &obus[packet.first_obu_index + packet.num_obu_elements - 1]; - let mut fragment_size = packet.last_obu_size; - if packet.num_obu_elements > MAX_NUM_OBUS_TO_OMIT_SIZE { - out.put_leb128(fragment_size as u32); - } - if obu_offset == 0 && fragment_size > 0 { - out.put_u8(last_obu.header & !OBU_HAS_SIZE_BIT); - fragment_size -= 1; - } - if obu_offset <= 1 && obu_has_extension(last_obu.header) && fragment_size > 0 { - out.put_u8(last_obu.extension_header); - fragment_size -= 1; - } - let payload_offset = if obu_offset > last_obu.header_size() { - obu_offset - last_obu.header_size() - } else { - 0 - }; - out.put_slice( - last_obu - .payload - .slice(payload_offset..payload_offset + fragment_size) - .as_ref(), - ); - - payloads.push(out.freeze()); - } - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/rtp/src/codecs/av1/obu.rs b/rtp/src/codecs/av1/obu.rs deleted file mode 100644 index f28936e60..000000000 --- a/rtp/src/codecs/av1/obu.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Based on https://chromium.googlesource.com/external/webrtc/+/4e513346ec56c829b3a6010664998469fc237b35/modules/rtp_rtcp/source/rtp_packetizer_av1.cc -//! Reference: https://aomediacodec.github.io/av1-spec/#obu-syntax - -use bytes::Bytes; - -use crate::codecs::av1::leb128::read_leb128; -use crate::error::Result; -use crate::Error::{ErrPayloadTooSmallForObuExtensionHeader, ErrPayloadTooSmallForObuPayloadSize}; - -pub const OBU_HAS_EXTENSION_BIT: u8 = 0b0000_0100; -pub const OBU_HAS_SIZE_BIT: u8 = 0b0000_0010; -pub const OBU_TYPE_MASK: u8 = 0b0111_1000; - -pub const OBU_TYPE_SEQUENCE_HEADER: u8 = 1; -pub const OBU_TYPE_TEMPORAL_DELIMITER: u8 = 2; -pub const OBU_TYPE_FRAME_HEADER: u8 = 3; -pub const OBU_TYPE_TILE_GROUP: u8 = 4; -pub const OBU_TYPE_METADATA: u8 = 5; -pub const OBU_TYPE_FRAME: u8 = 6; -pub const OBU_TYPE_TILE_LIST: u8 = 8; -pub const OBU_TYPE_PADDING: u8 = 15; - -#[derive(Debug, Clone)] -pub struct Obu { - pub header: u8, - pub extension_header: u8, - pub payload: Bytes, - /// size of the header and payload combined. - pub size: usize, -} - -impl Obu { - pub fn header_size(&self) -> usize { - if obu_has_extension(self.header) { - 2 - } else { - 1 - } - } -} - -/// Parses the payload into series of OBUs. -/// Reference: https://aomediacodec.github.io/av1-spec/#obu-syntax -pub fn parse_obus(payload: &Bytes) -> Result> { - let mut obus = vec![]; - let mut payload_data_remaining = payload.len() as isize; - let mut payload_data_index: usize = 0; - - while payload_data_remaining > 0 { - // Read OBU header. - let header = payload[payload_data_index]; - let has_extension = obu_has_extension(header); - let has_size = obu_has_size(header); - let obu_type = obu_type(header); - - // Read OBU extension header. - let extension_header = if has_extension { - if payload_data_remaining < 2 { - return Err(ErrPayloadTooSmallForObuExtensionHeader); - } - payload[payload_data_index + 1] - } else { - 0 - }; - let obu_header_size = if has_extension { 2 } else { 1 }; - let payload_without_header = payload.slice(payload_data_index + obu_header_size..); - - // Read OBU payload. - let obu_payload = if !has_size { - payload_without_header - } else { - if payload_without_header.is_empty() { - return Err(ErrPayloadTooSmallForObuPayloadSize); - } - let (obu_payload_size, leb128_size) = read_leb128(&payload_without_header); - payload_data_remaining -= leb128_size as isize; - payload_data_index += leb128_size; - payload_without_header.slice(leb128_size..leb128_size + obu_payload_size as usize) - }; - - let obu_size = obu_header_size + obu_payload.len(); - if !should_ignore_obu_type(obu_type) { - obus.push(Obu { - header, - extension_header, - payload: obu_payload, - size: obu_size, - }); - } - - payload_data_remaining -= obu_size as isize; - payload_data_index += obu_size; - } - - Ok(obus) -} - -pub fn obu_has_extension(header: u8) -> bool { - header & OBU_HAS_EXTENSION_BIT != 0 -} - -pub fn obu_has_size(header: u8) -> bool { - header & OBU_HAS_SIZE_BIT != 0 -} - -pub fn obu_type(header: u8) -> u8 { - (header & OBU_TYPE_MASK) >> 3 -} - -fn should_ignore_obu_type(obu_type: u8) -> bool { - obu_type == OBU_TYPE_TEMPORAL_DELIMITER - || obu_type == OBU_TYPE_TILE_LIST - || obu_type == OBU_TYPE_PADDING -} diff --git a/rtp/src/codecs/av1/packetizer.rs b/rtp/src/codecs/av1/packetizer.rs deleted file mode 100644 index 119a91f48..000000000 --- a/rtp/src/codecs/av1/packetizer.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Based on https://chromium.googlesource.com/external/webrtc/+/4e513346ec56c829b3a6010664998469fc237b35/modules/rtp_rtcp/source/rtp_packetizer_av1.cc -//! Reference: https://aomediacodec.github.io/av1-rtp-spec - -use std::cmp::min; - -use crate::codecs::av1::leb128::leb128_size; -use crate::codecs::av1::obu::{obu_type, Obu, OBU_TYPE_SEQUENCE_HEADER}; - -/// When there are 3 or less OBU (fragments) in a packet, size of the last one -/// can be omitted. -pub const MAX_NUM_OBUS_TO_OMIT_SIZE: usize = 3; -pub const AGGREGATION_HEADER_SIZE: usize = 1; - -pub struct PacketMetadata { - pub first_obu_index: usize, - pub num_obu_elements: usize, - pub first_obu_offset: usize, - pub last_obu_size: usize, - /// Total size consumed by the packet. - pub packet_size: usize, -} - -impl PacketMetadata { - fn new(first_obu_index: usize) -> Self { - Self { - first_obu_index, - num_obu_elements: 0, - first_obu_offset: 0, - last_obu_size: 0, - packet_size: 0, - } - } -} - -/// Returns the scheme for how to aggregate or split the OBUs across RTP packets. -/// Reference: https://aomediacodec.github.io/av1-rtp-spec/#45-payload-structure -/// https://aomediacodec.github.io/av1-rtp-spec/#5-packetization-rules -pub fn packetize(obus: &[Obu], mtu: usize) -> Vec { - if obus.is_empty() { - return vec![]; - } - // Ignore certain edge cases where packets should be very small. They are - // impractical but adds complexity to handle. - if mtu < 3 { - return vec![]; - } - - let mut packets = vec![]; - - // Aggregation header will be present in all packets. - let max_payload_size = mtu - AGGREGATION_HEADER_SIZE; - - // Assemble packets. Push to current packet as much as it can hold before - // considering next one. That would normally cause uneven distribution across - // packets, specifically last one would be generally smaller. - packets.push(PacketMetadata::new(0)); - let mut packet_remaining_bytes = max_payload_size; - - for obu_index in 0..obus.len() { - let is_last_obu = obu_index == obus.len() - 1; - let obu = &obus[obu_index]; - - // Putting |obu| into the last packet would make last obu element stored in - // that packet not last. All not last OBU elements must be prepend with the - // element length. AdditionalBytesForPreviousObuElement calculates how many - // bytes are needed to store that length. - let mut packet = packets.pop().unwrap(); - let mut previous_obu_extra_size = additional_bytes_for_previous_obu_element(&packet); - let min_required_size = if packet.num_obu_elements >= MAX_NUM_OBUS_TO_OMIT_SIZE { - 2 - } else { - 1 - }; - if packet_remaining_bytes < previous_obu_extra_size + min_required_size { - // Start a new packet. - packets.push(packet); - packet = PacketMetadata::new(obu_index); - packet_remaining_bytes = max_payload_size; - previous_obu_extra_size = 0; - } - packet.packet_size += previous_obu_extra_size; - packet_remaining_bytes -= previous_obu_extra_size; - packet.num_obu_elements += 1; - let must_write_obu_element_size = packet.num_obu_elements > MAX_NUM_OBUS_TO_OMIT_SIZE; - - // Can fit all of the obu into the packet? - let mut required_bytes = obu.size; - if must_write_obu_element_size { - required_bytes += leb128_size(obu.size as u32); - } - if required_bytes < packet_remaining_bytes { - // Insert the obu into the packet unfragmented. - packet.last_obu_size = obu.size; - packet.packet_size += required_bytes; - packet_remaining_bytes -= required_bytes; - packets.push(packet); - continue; - } - - // Fragment the obu. - let max_first_fragment_size = if must_write_obu_element_size { - max_fragment_size(packet_remaining_bytes) - } else { - packet_remaining_bytes - }; - // Because available_bytes might be different than - // packet_remaining_bytes it might happen that max_first_fragment_size >= - // obu.size. Also, since checks above verified |obu| should not be put - // completely into the |packet|, leave at least 1 byte for later packet. - let first_fragment_size = min(obu.size - 1, max_first_fragment_size); - if first_fragment_size == 0 { - // Rather than writing 0-size element at the tail of the packet, - // 'uninsert' the |obu| from the |packet|. - packet.num_obu_elements -= 1; - packet.packet_size -= previous_obu_extra_size; - } else { - packet.packet_size += first_fragment_size; - if must_write_obu_element_size { - packet.packet_size += leb128_size(first_fragment_size as u32); - } - packet.last_obu_size = first_fragment_size; - } - packets.push(packet); - - // Add middle fragments that occupy all of the packet. - // These are easy because - // - one obu per packet imply no need to store the size of the obu. - // - this packets are nor the first nor the last packets of the frame, so - // packet capacity is always limits.max_payload_len. - let mut obu_offset = first_fragment_size; - while obu_offset + max_payload_size < obu.size { - let mut packet = PacketMetadata::new(obu_index); - packet.num_obu_elements = 1; - packet.first_obu_offset = obu_offset; - let middle_fragment_size = max_payload_size; - packet.last_obu_size = middle_fragment_size; - packet.packet_size = middle_fragment_size; - packets.push(packet); - obu_offset += max_payload_size; - } - - // Add the last fragment of the obu. - let mut last_fragment_size = obu.size - obu_offset; - // Check for corner case where last fragment of the last obu is too large - // to fit into last packet, but may fully fit into semi-last packet. - if is_last_obu && last_fragment_size > max_payload_size { - // Split last fragments into two. - // Try to even packet sizes rather than payload sizes across the last - // two packets. - let mut semi_last_fragment_size = last_fragment_size / 2; - // But leave at least one payload byte for the last packet to avoid - // weird scenarios where size of the fragment is zero and rtp payload has - // nothing except for an aggregation header. - if semi_last_fragment_size >= last_fragment_size { - semi_last_fragment_size = last_fragment_size - 1; - } - last_fragment_size -= semi_last_fragment_size; - let mut packet = PacketMetadata::new(obu_index); - packet.first_obu_offset = obu_offset; - packet.last_obu_size = semi_last_fragment_size; - packet.packet_size = semi_last_fragment_size; - packets.push(packet); - obu_offset += semi_last_fragment_size - } - let mut last_packet = PacketMetadata::new(obu_index); - last_packet.num_obu_elements = 1; - last_packet.first_obu_offset = obu_offset; - last_packet.last_obu_size = last_fragment_size; - last_packet.packet_size = last_fragment_size; - packets.push(last_packet); - packet_remaining_bytes = max_payload_size - last_fragment_size; - } - - packets -} - -/// Returns the aggregation header for the packet. -/// Reference: https://aomediacodec.github.io/av1-rtp-spec/#44-av1-aggregation-header -pub fn get_aggregation_header(obus: &[Obu], packets: &[PacketMetadata], packet_index: usize) -> u8 { - let packet = &packets[packet_index]; - let mut header: u8 = 0; - - // Set Z flag: first obu element is continuation of the previous OBU. - let first_obu_element_is_fragment = packet.first_obu_offset > 0; - if first_obu_element_is_fragment { - header |= 1 << 7; - } - - // Set Y flag: last obu element will be continuated in the next packet. - let last_obu_offset = if packet.num_obu_elements == 1 { - packet.first_obu_offset - } else { - 0 - }; - let last_obu_is_fragment = last_obu_offset + packet.last_obu_size - < obus[packet.first_obu_index + packet.num_obu_elements - 1].size; - if last_obu_is_fragment { - header |= 1 << 6; - } - - // Set W field: number of obu elements in the packet (when not too large). - if packet.num_obu_elements <= MAX_NUM_OBUS_TO_OMIT_SIZE { - header |= (packet.num_obu_elements as u8) << 4; - } - - // Set N flag: beginning of a new coded video sequence. - // Encoder may produce key frame without a sequence header, thus double check - // incoming frame includes the sequence header. Since Temporal delimiter is - // already filtered out, sequence header should be the first obu when present. - // - // TODO: This is technically incorrect, since sequence headers may be present in delta frames. - // However, unlike the Chromium implementation: https://chromium.googlesource.com/external/webrtc/+/4e513346ec56c829b3a6010664998469fc237b35/modules/rtp_rtcp/source/rtp_packetizer_av1.cc#345, - // we do not have direct access to the whether this is a keyframe or a delta frame. - // Thus for now we assume that every frame that starts with a sequence header is a keyframe, - // which is not always true. This is the best we can do for now until implementing - // a proper frame type detection, perhaps by parsing the FRAME_HEADER OBUs according to - // https://aomediacodec.github.io/av1-spec/#ordering-of-obus: - // A new coded video sequence is defined to start at each temporal unit which - // satisfies both of the following conditions: - // - A sequence header OBU appears before the first frame header. - // - The first frame header has frame_type equal to KEY_FRAME, show_frame equal - // to 1, show_existing_frame equal to 0, and temporal_id equal to 0. - if packet_index == 0 && obu_type(obus.first().unwrap().header) == OBU_TYPE_SEQUENCE_HEADER { - header |= 1 << 3; - } - header -} - -/// Returns the number of additional bytes needed to store the previous OBU -/// element if an additional OBU element is added to the packet. -fn additional_bytes_for_previous_obu_element(packet: &PacketMetadata) -> usize { - if packet.packet_size == 0 || packet.num_obu_elements > MAX_NUM_OBUS_TO_OMIT_SIZE { - // Packet is still empty => no last OBU element, no need to reserve space for it. - // OR - // There are so many obu elements in the packet, all of them must be - // prepended with the length field. That imply space for the length of the - // last obu element is already reserved. - 0 - } else { - leb128_size(packet.last_obu_size as u32) - } -} - -/// Given |remaining_bytes| free bytes left in a packet, returns max size of an -/// OBU fragment that can fit into the packet. -/// i.e. MaxFragmentSize + Leb128Size(MaxFragmentSize) <= remaining_bytes. -fn max_fragment_size(remaining_bytes: usize) -> usize { - if remaining_bytes <= 1 { - return 0; - } - let mut i = 1; - loop { - if remaining_bytes < (1 << (7 * i)) + i { - return remaining_bytes - i; - } - i += 1; - } -} diff --git a/rtp/src/codecs/g7xx/g7xx_test.rs b/rtp/src/codecs/g7xx/g7xx_test.rs deleted file mode 100644 index a172944d1..000000000 --- a/rtp/src/codecs/g7xx/g7xx_test.rs +++ /dev/null @@ -1,50 +0,0 @@ -use super::*; - -#[test] -fn test_g7xx_payload() -> Result<()> { - let mut pck = G711Payloader::default(); - - const TEST_LEN: usize = 10000; - const TEST_MTU: usize = 1500; - - //generate random 8-bit g722 samples - let samples: Vec = (0..TEST_LEN).map(|_| rand::random::()).collect(); - - //make a copy, for payloader input - let mut samples_in = vec![0u8; TEST_LEN]; - samples_in.clone_from_slice(&samples); - let samples_in = Bytes::copy_from_slice(&samples_in); - - //split our samples into payloads - let payloads = pck.payload(TEST_MTU, &samples_in)?; - - let outcnt = ((TEST_LEN as f64) / (TEST_MTU as f64)).ceil() as usize; - assert_eq!( - outcnt, - payloads.len(), - "Generated {} payloads instead of {}", - payloads.len(), - outcnt - ); - assert_eq!(&samples, &samples_in, "Modified input samples"); - - let samples_out = payloads.concat(); - assert_eq!(&samples_out, &samples_in, "Output samples don't match"); - - let empty = Bytes::from_static(&[]); - let payload = Bytes::from_static(&[0x90, 0x90, 0x90]); - - // Positive MTU, empty payload - let result = pck.payload(1, &empty)?; - assert!(result.is_empty(), "Generated payload should be empty"); - - // 0 MTU, small payload - let result = pck.payload(0, &payload)?; - assert_eq!(result.len(), 0, "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(10, &payload)?; - assert_eq!(result.len(), 1, "Generated payload should be the 1"); - - Ok(()) -} diff --git a/rtp/src/codecs/g7xx/mod.rs b/rtp/src/codecs/g7xx/mod.rs deleted file mode 100644 index e0c69020f..000000000 --- a/rtp/src/codecs/g7xx/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[cfg(test)] -mod g7xx_test; - -use bytes::Bytes; - -use crate::error::Result; -use crate::packetizer::Payloader; - -/// G711Payloader payloads G711 packets -pub type G711Payloader = G7xxPayloader; -/// G722Payloader payloads G722 packets -pub type G722Payloader = G7xxPayloader; - -#[derive(Default, Debug, Copy, Clone)] -pub struct G7xxPayloader; - -impl Payloader for G7xxPayloader { - /// Payload fragments an G7xx packet across one or more byte arrays - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - let mut payload_data_remaining = payload.len(); - let mut payload_data_index = 0; - let mut payloads = Vec::with_capacity(payload_data_remaining / mtu); - while payload_data_remaining > 0 { - let current_fragment_size = std::cmp::min(mtu, payload_data_remaining); - payloads.push( - payload.slice(payload_data_index..payload_data_index + current_fragment_size), - ); - - payload_data_remaining -= current_fragment_size; - payload_data_index += current_fragment_size; - } - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(*self) - } -} diff --git a/rtp/src/codecs/h264/h264_test.rs b/rtp/src/codecs/h264/h264_test.rs deleted file mode 100644 index bc8aa4641..000000000 --- a/rtp/src/codecs/h264/h264_test.rs +++ /dev/null @@ -1,263 +0,0 @@ -// Silence warning on `for i in 0..vec.len() { … }`: -#![allow(clippy::needless_range_loop)] - -use super::*; - -#[test] -fn test_h264_payload() -> Result<()> { - let empty = Bytes::from_static(&[]); - let small_payload = Bytes::from_static(&[0x90, 0x90, 0x90]); - let multiple_payload = Bytes::from_static(&[0x00, 0x00, 0x01, 0x90, 0x00, 0x00, 0x01, 0x90]); - let large_payload = Bytes::from_static(&[ - 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, - 0x12, 0x13, 0x14, 0x15, - ]); - let large_payload_packetized = vec![ - Bytes::from_static(&[0x1c, 0x80, 0x01, 0x02, 0x03]), - Bytes::from_static(&[0x1c, 0x00, 0x04, 0x05, 0x06]), - Bytes::from_static(&[0x1c, 0x00, 0x07, 0x08, 0x09]), - Bytes::from_static(&[0x1c, 0x00, 0x10, 0x11, 0x12]), - Bytes::from_static(&[0x1c, 0x40, 0x13, 0x14, 0x15]), - ]; - - let mut pck = H264Payloader::default(); - - // Positive MTU, empty payload - let result = pck.payload(1, &empty)?; - assert!(result.is_empty(), "Generated payload should be empty"); - - // 0 MTU, small payload - let result = pck.payload(0, &small_payload)?; - assert_eq!(result.len(), 0, "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(1, &small_payload)?; - assert_eq!(result.len(), 0, "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(5, &small_payload)?; - assert_eq!(result.len(), 1, "Generated payload should be the 1"); - assert_eq!( - result[0].len(), - small_payload.len(), - "Generated payload should be the same size as original payload size" - ); - - // Multiple NALU in a single payload - let result = pck.payload(5, &multiple_payload)?; - assert_eq!(result.len(), 2, "2 nal units should be broken out"); - for i in 0..2 { - assert_eq!( - result[i].len(), - 1, - "Payload {} of 2 is packed incorrectly", - i + 1, - ); - } - - // Large Payload split across multiple RTP Packets - let result = pck.payload(5, &large_payload)?; - assert_eq!( - result, large_payload_packetized, - "FU-A packetization failed" - ); - - // Nalu type 9 or 12 - let small_payload2 = Bytes::from_static(&[0x09, 0x00, 0x00]); - let result = pck.payload(5, &small_payload2)?; - assert_eq!(result.len(), 0, "Generated payload should be empty"); - - Ok(()) -} - -#[test] -fn test_h264_packet_unmarshal() -> Result<()> { - let single_payload = Bytes::from_static(&[0x90, 0x90, 0x90]); - let single_payload_unmarshaled = - Bytes::from_static(&[0x00, 0x00, 0x00, 0x01, 0x90, 0x90, 0x90]); - let single_payload_unmarshaled_avc = - Bytes::from_static(&[0x00, 0x00, 0x00, 0x03, 0x90, 0x90, 0x90]); - - let large_payload = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, - ]); - let large_payload_avc = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, - ]); - let large_payload_packetized = vec![ - Bytes::from_static(&[0x1c, 0x80, 0x01, 0x02, 0x03]), - Bytes::from_static(&[0x1c, 0x00, 0x04, 0x05, 0x06]), - Bytes::from_static(&[0x1c, 0x00, 0x07, 0x08, 0x09]), - Bytes::from_static(&[0x1c, 0x00, 0x10, 0x11, 0x12]), - Bytes::from_static(&[0x1c, 0x40, 0x13, 0x14, 0x15]), - ]; - - let single_payload_multi_nalu = Bytes::from_static(&[ - 0x78, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, - 0x22, 0x11, 0xa8, 0x00, 0x05, 0x68, 0x1a, 0x34, 0xe3, 0xc8, - ]); - let single_payload_multi_nalu_unmarshaled = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, - 0x3c, 0x22, 0x11, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x68, 0x1a, 0x34, 0xe3, 0xc8, - ]); - let single_payload_multi_nalu_unmarshaled_avc = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, - 0x3c, 0x22, 0x11, 0xa8, 0x00, 0x00, 0x00, 0x05, 0x68, 0x1a, 0x34, 0xe3, 0xc8, - ]); - - let incomplete_single_payload_multi_nalu = Bytes::from_static(&[ - 0x78, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, - 0x22, 0x11, - ]); - - let mut pkt = H264Packet::default(); - let mut avc_pkt = H264Packet { - is_avc: true, - ..Default::default() - }; - - let data = Bytes::from_static(&[]); - let result = pkt.depacketize(&data); - assert!(result.is_err(), "Unmarshal did not fail on nil payload"); - - let data = Bytes::from_static(&[0x00, 0x00]); - let result = pkt.depacketize(&data); - assert!( - result.is_err(), - "Unmarshal accepted a packet that is too small for a payload and header" - ); - - let data = Bytes::from_static(&[0xFF, 0x00, 0x00]); - let result = pkt.depacketize(&data); - assert!( - result.is_err(), - "Unmarshal accepted a packet with a NALU Type we don't handle" - ); - - let result = pkt.depacketize(&incomplete_single_payload_multi_nalu); - assert!( - result.is_err(), - "Unmarshal accepted a STAP-A packet with insufficient data" - ); - - let payload = pkt.depacketize(&single_payload)?; - assert_eq!( - payload, single_payload_unmarshaled, - "Unmarshalling a single payload shouldn't modify the payload" - ); - - let payload = avc_pkt.depacketize(&single_payload)?; - assert_eq!( - payload, single_payload_unmarshaled_avc, - "Unmarshalling a single payload into avc stream shouldn't modify the payload" - ); - - let mut large_payload_result = BytesMut::new(); - for p in &large_payload_packetized { - let payload = pkt.depacketize(p)?; - large_payload_result.put(&*payload.clone()); - } - assert_eq!( - large_payload_result.freeze(), - large_payload, - "Failed to unmarshal a large payload" - ); - - let mut large_payload_result_avc = BytesMut::new(); - for p in &large_payload_packetized { - let payload = avc_pkt.depacketize(p)?; - large_payload_result_avc.put(&*payload.clone()); - } - assert_eq!( - large_payload_result_avc.freeze(), - large_payload_avc, - "Failed to unmarshal a large payload into avc stream" - ); - - let payload = pkt.depacketize(&single_payload_multi_nalu)?; - assert_eq!( - payload, single_payload_multi_nalu_unmarshaled, - "Failed to unmarshal a single packet with multiple NALUs" - ); - - let payload = avc_pkt.depacketize(&single_payload_multi_nalu)?; - assert_eq!( - payload, single_payload_multi_nalu_unmarshaled_avc, - "Failed to unmarshal a single packet with multiple NALUs into avc stream" - ); - - Ok(()) -} - -#[test] -fn test_h264_partition_head_checker_is_partition_head() -> Result<()> { - let h264 = H264Packet::default(); - let empty_nalu = Bytes::from_static(&[]); - assert!( - !h264.is_partition_head(&empty_nalu), - "empty nalu must not be a partition head" - ); - - let single_nalu = Bytes::from_static(&[1, 0]); - assert!( - h264.is_partition_head(&single_nalu), - "single nalu must be a partition head" - ); - - let stapa_nalu = Bytes::from_static(&[STAPA_NALU_TYPE, 0]); - assert!( - h264.is_partition_head(&stapa_nalu), - "stapa nalu must be a partition head" - ); - - let fua_start_nalu = Bytes::from_static(&[FUA_NALU_TYPE, FU_START_BITMASK]); - assert!( - h264.is_partition_head(&fua_start_nalu), - "fua start nalu must be a partition head" - ); - - let fua_end_nalu = Bytes::from_static(&[FUA_NALU_TYPE, FU_END_BITMASK]); - assert!( - !h264.is_partition_head(&fua_end_nalu), - "fua end nalu must not be a partition head" - ); - - let fub_start_nalu = Bytes::from_static(&[FUB_NALU_TYPE, FU_START_BITMASK]); - assert!( - h264.is_partition_head(&fub_start_nalu), - "fub start nalu must be a partition head" - ); - - let fub_end_nalu = Bytes::from_static(&[FUB_NALU_TYPE, FU_END_BITMASK]); - assert!( - !h264.is_partition_head(&fub_end_nalu), - "fub end nalu must not be a partition head" - ); - - Ok(()) -} - -#[test] -fn test_h264_payloader_payload_sps_and_pps_handling() -> Result<()> { - let mut pck = H264Payloader::default(); - let expected = vec![ - Bytes::from_static(&[ - 0x78, 0x00, 0x03, 0x07, 0x00, 0x01, 0x00, 0x03, 0x08, 0x02, 0x03, - ]), - Bytes::from_static(&[0x05, 0x04, 0x05]), - ]; - - // When packetizing SPS and PPS are emitted with following NALU - let res = pck.payload(1500, &Bytes::from_static(&[0x07, 0x00, 0x01]))?; - assert!(res.is_empty(), "Generated payload should be empty"); - - let res = pck.payload(1500, &Bytes::from_static(&[0x08, 0x02, 0x03]))?; - assert!(res.is_empty(), "Generated payload should be empty"); - - let actual = pck.payload(1500, &Bytes::from_static(&[0x05, 0x04, 0x05]))?; - assert_eq!(actual, expected, "SPS and PPS aren't packed together"); - - Ok(()) -} diff --git a/rtp/src/codecs/h264/mod.rs b/rtp/src/codecs/h264/mod.rs deleted file mode 100644 index 104b6ddde..000000000 --- a/rtp/src/codecs/h264/mod.rs +++ /dev/null @@ -1,310 +0,0 @@ -#[cfg(test)] -mod h264_test; - -use bytes::{BufMut, Bytes, BytesMut}; - -use crate::error::{Error, Result}; -use crate::packetizer::{Depacketizer, Payloader}; - -/// H264Payloader payloads H264 packets -#[derive(Default, Debug, Clone)] -pub struct H264Payloader { - sps_nalu: Option, - pps_nalu: Option, -} - -pub const STAPA_NALU_TYPE: u8 = 24; -pub const FUA_NALU_TYPE: u8 = 28; -pub const FUB_NALU_TYPE: u8 = 29; -pub const SPS_NALU_TYPE: u8 = 7; -pub const PPS_NALU_TYPE: u8 = 8; -pub const AUD_NALU_TYPE: u8 = 9; -pub const FILLER_NALU_TYPE: u8 = 12; - -pub const FUA_HEADER_SIZE: usize = 2; -pub const STAPA_HEADER_SIZE: usize = 1; -pub const STAPA_NALU_LENGTH_SIZE: usize = 2; - -pub const NALU_TYPE_BITMASK: u8 = 0x1F; -pub const NALU_REF_IDC_BITMASK: u8 = 0x60; -pub const FU_START_BITMASK: u8 = 0x80; -pub const FU_END_BITMASK: u8 = 0x40; - -pub const OUTPUT_STAP_AHEADER: u8 = 0x78; - -pub static ANNEXB_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x00, 0x01]); - -impl H264Payloader { - fn next_ind(nalu: &Bytes, start: usize) -> (isize, isize) { - let mut zero_count = 0; - - for (i, &b) in nalu[start..].iter().enumerate() { - if b == 0 { - zero_count += 1; - continue; - } else if b == 1 && zero_count >= 2 { - return ((start + i - zero_count) as isize, zero_count as isize + 1); - } - zero_count = 0 - } - (-1, -1) - } - - fn emit(&mut self, nalu: &Bytes, mtu: usize, payloads: &mut Vec) { - if nalu.is_empty() { - return; - } - - let nalu_type = nalu[0] & NALU_TYPE_BITMASK; - let nalu_ref_idc = nalu[0] & NALU_REF_IDC_BITMASK; - - if nalu_type == AUD_NALU_TYPE || nalu_type == FILLER_NALU_TYPE { - return; - } else if nalu_type == SPS_NALU_TYPE { - self.sps_nalu = Some(nalu.clone()); - return; - } else if nalu_type == PPS_NALU_TYPE { - self.pps_nalu = Some(nalu.clone()); - return; - } else if let (Some(sps_nalu), Some(pps_nalu)) = (&self.sps_nalu, &self.pps_nalu) { - // Pack current NALU with SPS and PPS as STAP-A - let sps_len = (sps_nalu.len() as u16).to_be_bytes(); - let pps_len = (pps_nalu.len() as u16).to_be_bytes(); - - let mut stap_a_nalu = Vec::with_capacity(1 + 2 + sps_nalu.len() + 2 + pps_nalu.len()); - stap_a_nalu.push(OUTPUT_STAP_AHEADER); - stap_a_nalu.extend(sps_len); - stap_a_nalu.extend_from_slice(sps_nalu); - stap_a_nalu.extend(pps_len); - stap_a_nalu.extend_from_slice(pps_nalu); - if stap_a_nalu.len() <= mtu { - payloads.push(Bytes::from(stap_a_nalu)); - } - } - - if self.sps_nalu.is_some() && self.pps_nalu.is_some() { - self.sps_nalu = None; - self.pps_nalu = None; - } - - // Single NALU - if nalu.len() <= mtu { - payloads.push(nalu.clone()); - return; - } - - // FU-A - let max_fragment_size = mtu as isize - FUA_HEADER_SIZE as isize; - - // The FU payload consists of fragments of the payload of the fragmented - // NAL unit so that if the fragmentation unit payloads of consecutive - // FUs are sequentially concatenated, the payload of the fragmented NAL - // unit can be reconstructed. The NAL unit type octet of the fragmented - // NAL unit is not included as such in the fragmentation unit payload, - // but rather the information of the NAL unit type octet of the - // fragmented NAL unit is conveyed in the F and NRI fields of the FU - // indicator octet of the fragmentation unit and in the type field of - // the FU header. An FU payload MAY have any number of octets and MAY - // be empty. - - let nalu_data = nalu; - // According to the RFC, the first octet is skipped due to redundant information - let mut nalu_data_index = 1; - let nalu_data_length = nalu.len() as isize - nalu_data_index; - let mut nalu_data_remaining = nalu_data_length; - - if std::cmp::min(max_fragment_size, nalu_data_remaining) <= 0 { - return; - } - - while nalu_data_remaining > 0 { - let current_fragment_size = std::cmp::min(max_fragment_size, nalu_data_remaining); - //out: = make([]byte, fuaHeaderSize + currentFragmentSize) - let mut out = BytesMut::with_capacity(FUA_HEADER_SIZE + current_fragment_size as usize); - // +---------------+ - // |0|1|2|3|4|5|6|7| - // +-+-+-+-+-+-+-+-+ - // |F|NRI| Type | - // +---------------+ - let b0 = FUA_NALU_TYPE | nalu_ref_idc; - out.put_u8(b0); - - // +---------------+ - //|0|1|2|3|4|5|6|7| - //+-+-+-+-+-+-+-+-+ - //|S|E|R| Type | - //+---------------+ - - let mut b1 = nalu_type; - if nalu_data_remaining == nalu_data_length { - // Set start bit - b1 |= 1 << 7; - } else if nalu_data_remaining - current_fragment_size == 0 { - // Set end bit - b1 |= 1 << 6; - } - out.put_u8(b1); - - out.put( - &nalu_data - [nalu_data_index as usize..(nalu_data_index + current_fragment_size) as usize], - ); - payloads.push(out.freeze()); - - nalu_data_remaining -= current_fragment_size; - nalu_data_index += current_fragment_size; - } - } -} - -impl Payloader for H264Payloader { - /// Payload fragments a H264 packet across one or more byte arrays - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - let mut payloads = vec![]; - - let (mut next_ind_start, mut next_ind_len) = H264Payloader::next_ind(payload, 0); - if next_ind_start == -1 { - self.emit(payload, mtu, &mut payloads); - } else { - while next_ind_start != -1 { - let prev_start = (next_ind_start + next_ind_len) as usize; - let (next_ind_start2, next_ind_len2) = H264Payloader::next_ind(payload, prev_start); - next_ind_start = next_ind_start2; - next_ind_len = next_ind_len2; - if next_ind_start != -1 { - self.emit( - &payload.slice(prev_start..next_ind_start as usize), - mtu, - &mut payloads, - ); - } else { - // Emit until end of stream, no end indicator found - self.emit(&payload.slice(prev_start..), mtu, &mut payloads); - } - } - } - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} - -/// H264Packet represents the H264 header that is stored in the payload of an RTP Packet -#[derive(PartialEq, Eq, Debug, Default, Clone)] -pub struct H264Packet { - pub is_avc: bool, - fua_buffer: Option, -} - -impl Depacketizer for H264Packet { - /// depacketize parses the passed byte slice and stores the result in the H264Packet this method is called upon - fn depacketize(&mut self, packet: &Bytes) -> Result { - if packet.len() <= 2 { - return Err(Error::ErrShortPacket); - } - - let mut payload = BytesMut::new(); - - // NALU Types - // https://tools.ietf.org/html/rfc6184#section-5.4 - let b0 = packet[0]; - let nalu_type = b0 & NALU_TYPE_BITMASK; - - match nalu_type { - 1..=23 => { - if self.is_avc { - payload.put_u32(packet.len() as u32); - } else { - payload.put(&*ANNEXB_NALUSTART_CODE); - } - payload.put(&*packet.clone()); - Ok(payload.freeze()) - } - STAPA_NALU_TYPE => { - let mut curr_offset = STAPA_HEADER_SIZE; - while curr_offset < packet.len() { - let nalu_size = - ((packet[curr_offset] as usize) << 8) | packet[curr_offset + 1] as usize; - curr_offset += STAPA_NALU_LENGTH_SIZE; - - if packet.len() < curr_offset + nalu_size { - return Err(Error::StapASizeLargerThanBuffer( - nalu_size, - packet.len() - curr_offset, - )); - } - - if self.is_avc { - payload.put_u32(nalu_size as u32); - } else { - payload.put(&*ANNEXB_NALUSTART_CODE); - } - payload.put(&*packet.slice(curr_offset..curr_offset + nalu_size)); - curr_offset += nalu_size; - } - - Ok(payload.freeze()) - } - FUA_NALU_TYPE => { - if packet.len() < FUA_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - if self.fua_buffer.is_none() { - self.fua_buffer = Some(BytesMut::new()); - } - - if let Some(fua_buffer) = &mut self.fua_buffer { - fua_buffer.put(&*packet.slice(FUA_HEADER_SIZE..)); - } - - let b1 = packet[1]; - if b1 & FU_END_BITMASK != 0 { - let nalu_ref_idc = b0 & NALU_REF_IDC_BITMASK; - let fragmented_nalu_type = b1 & NALU_TYPE_BITMASK; - - if let Some(fua_buffer) = self.fua_buffer.take() { - if self.is_avc { - payload.put_u32((fua_buffer.len() + 1) as u32); - } else { - payload.put(&*ANNEXB_NALUSTART_CODE); - } - payload.put_u8(nalu_ref_idc | fragmented_nalu_type); - payload.put(fua_buffer); - } - - Ok(payload.freeze()) - } else { - Ok(Bytes::new()) - } - } - _ => Err(Error::NaluTypeIsNotHandled(nalu_type)), - } - } - - /// is_partition_head checks if this is the head of a packetized nalu stream. - fn is_partition_head(&self, payload: &Bytes) -> bool { - if payload.len() < 2 { - return false; - } - - if payload[0] & NALU_TYPE_BITMASK == FUA_NALU_TYPE - || payload[0] & NALU_TYPE_BITMASK == FUB_NALU_TYPE - { - (payload[1] & FU_START_BITMASK) != 0 - } else { - true - } - } - - fn is_partition_tail(&self, marker: bool, _payload: &Bytes) -> bool { - marker - } -} diff --git a/rtp/src/codecs/h265/h265_test.rs b/rtp/src/codecs/h265/h265_test.rs deleted file mode 100644 index 6ace5ff89..000000000 --- a/rtp/src/codecs/h265/h265_test.rs +++ /dev/null @@ -1,889 +0,0 @@ -use super::*; - -#[test] -fn test_h265_nalu_header() -> Result<()> { - #[derive(Default)] - struct TestType { - raw_header: Bytes, - - fbit: bool, - typ: u8, - layer_id: u8, - tid: u8, - - is_ap: bool, - is_fu: bool, - is_paci: bool, - } - - let tests = vec![ - // fbit - TestType { - raw_header: Bytes::from_static(&[0x80, 0x00]), - typ: 0, - layer_id: 0, - tid: 0, - fbit: true, - ..Default::default() - }, - // VPS_NUT - TestType { - raw_header: Bytes::from_static(&[0x40, 0x01]), - typ: 32, - layer_id: 0, - tid: 1, - ..Default::default() - }, - // SPS_NUT - TestType { - raw_header: Bytes::from_static(&[0x42, 0x01]), - typ: 33, - layer_id: 0, - tid: 1, - ..Default::default() - }, - // PPS_NUT - TestType { - raw_header: Bytes::from_static(&[0x44, 0x01]), - typ: 34, - layer_id: 0, - tid: 1, - ..Default::default() - }, - // PREFIX_SEI_NUT - TestType { - raw_header: Bytes::from_static(&[0x4e, 0x01]), - typ: 39, - layer_id: 0, - tid: 1, - ..Default::default() - }, - // Fragmentation Unit - TestType { - raw_header: Bytes::from_static(&[0x62, 0x01]), - typ: H265NALU_FRAGMENTATION_UNIT_TYPE, - layer_id: 0, - tid: 1, - is_fu: true, - ..Default::default() - }, - ]; - - for cur in tests { - let header = H265NALUHeader::new(cur.raw_header[0], cur.raw_header[1]); - - assert_eq!(header.f(), cur.fbit, "invalid F bit"); - assert_eq!(header.nalu_type(), cur.typ, "invalid type"); - - // For any type < 32, NAL is a VLC NAL unit. - assert_eq!( - header.is_type_vcl_unit(), - (header.nalu_type() < 32), - "invalid IsTypeVCLUnit" - ); - assert_eq!( - header.is_aggregation_packet(), - cur.is_ap, - "invalid type (aggregation packet)" - ); - assert_eq!( - header.is_fragmentation_unit(), - cur.is_fu, - "invalid type (fragmentation unit)" - ); - assert_eq!(header.is_paci_packet(), cur.is_paci, "invalid type (PACI)"); - assert_eq!(header.layer_id(), cur.layer_id, "invalid layer_id"); - assert_eq!(header.tid(), cur.tid, "invalid tid"); - } - - Ok(()) -} - -#[test] -fn test_h265_fu_header() -> Result<()> { - #[derive(Default)] - struct TestType { - header: H265FragmentationUnitHeader, - - s: bool, - e: bool, - typ: u8, - } - - let tests = vec![ - // Start | IDR_W_RADL - TestType { - header: H265FragmentationUnitHeader(0x93), - s: true, - e: false, - typ: 19, - }, - // Continuation | IDR_W_RADL - TestType { - header: H265FragmentationUnitHeader(0x13), - s: false, - e: false, - typ: 19, - }, - // End | IDR_W_RADL - TestType { - header: H265FragmentationUnitHeader(0x53), - s: false, - e: true, - typ: 19, - }, - // Start | TRAIL_R - TestType { - header: H265FragmentationUnitHeader(0x81), - s: true, - e: false, - typ: 1, - }, - // Continuation | TRAIL_R - TestType { - header: H265FragmentationUnitHeader(0x01), - s: false, - e: false, - typ: 1, - }, - // End | TRAIL_R - TestType { - header: H265FragmentationUnitHeader(0x41), - s: false, - e: true, - typ: 1, - }, - ]; - - for cur in tests { - assert_eq!(cur.header.s(), cur.s, "invalid s field"); - assert_eq!(cur.header.e(), cur.e, "invalid e field"); - assert_eq!(cur.header.fu_type(), cur.typ, "invalid FuType field"); - } - - Ok(()) -} - -#[test] -fn test_h265_single_nalunit_packet() -> Result<()> { - #[derive(Default)] - struct TestType { - raw: Bytes, - with_donl: bool, - expected_packet: Option, - expected_err: Option, - } - - let tests = vec![ - TestType { - raw: Bytes::from_static(&[]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // FBit enabled in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrH265CorruptedPacket), - ..Default::default() - }, - // Type '49' in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - // Type '50' in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x64, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x01, 0x01, 0xab, 0xcd, 0xef]), - expected_packet: Some(H265SingleNALUnitPacket { - payload_header: H265NALUHeader::new(0x01, 0x01), - payload: Bytes::from_static(&[0xab, 0xcd, 0xef]), - ..Default::default() - }), - ..Default::default() - }, - // DONL, payload too small - TestType { - raw: Bytes::from_static(&[0x01, 0x01, 0x93, 0xaf]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x01, 0x01, 0xaa, 0xbb, 0xcc]), - expected_packet: Some(H265SingleNALUnitPacket { - payload_header: H265NALUHeader::new(0x01, 0x01), - donl: Some((0xaa << 8) | 0xbb), - payload: Bytes::from_static(&[0xcc]), - ..Default::default() - }), - with_donl: true, - ..Default::default() - }, - ]; - - for cur in tests { - let mut parsed = H265SingleNALUnitPacket::default(); - if cur.with_donl { - parsed.with_donl(cur.with_donl); - } - - let result = parsed.depacketize(&cur.raw); - - if cur.expected_err.is_some() && result.is_ok() { - panic!("should error"); - } else if cur.expected_err.is_none() && result.is_err() { - panic!("should not error"); - } - - if let Some(expected_packet) = cur.expected_packet { - assert_eq!( - parsed.payload_header(), - expected_packet.payload_header(), - "invalid payload header" - ); - assert_eq!(parsed.donl(), expected_packet.donl(), "invalid DONL"); - - assert_eq!( - parsed.payload(), - expected_packet.payload(), - "invalid payload" - ); - } - } - - Ok(()) -} - -#[test] -fn test_h265_aggregation_packet() -> Result<()> { - #[derive(Default)] - struct TestType { - raw: Bytes, - with_donl: bool, - expected_packet: Option, - expected_err: Option, - } - - let tests = vec![ - TestType { - raw: Bytes::from_static(&[]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // FBit enabled in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrH265CorruptedPacket), - ..Default::default() - }, - // Type '48' in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0xE0, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - // Small payload - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00, 0x1]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // Small payload - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Small payload - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00, 0x1]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Small payload - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00, 0x01, 0x02]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Single Aggregation Unit - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Incomplete second Aggregation Unit - TestType { - raw: Bytes::from_static(&[ - 0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, // DONL - 0x00, - ]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Incomplete second Aggregation Unit - TestType { - raw: Bytes::from_static(&[ - 0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, - // DONL, NAL Unit size (2 bytes) - 0x00, 0x55, 0x55, - ]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Valid Second Aggregation Unit - TestType { - raw: Bytes::from_static(&[ - 0x60, 0x01, 0xcc, 0xdd, 0x00, 0x02, 0xff, 0xee, - // DONL, NAL Unit size (2 bytes), Payload - 0x77, 0x00, 0x01, 0xaa, - ]), - with_donl: true, - expected_packet: Some(H265AggregationPacket { - first_unit: Some(H265AggregationUnitFirst { - donl: Some(0xccdd), - nal_unit_size: 2, - nal_unit: Bytes::from_static(&[0xff, 0xee]), - }), - other_units: vec![H265AggregationUnit { - dond: Some(0x77), - nal_unit_size: 1, - nal_unit: Bytes::from_static(&[0xaa]), - }], - might_need_donl: false, - }), - ..Default::default() - }, - ]; - - for cur in tests { - let mut parsed = H265AggregationPacket::default(); - if cur.with_donl { - parsed.with_donl(cur.with_donl); - } - - let result = parsed.depacketize(&cur.raw); - - if cur.expected_err.is_some() && result.is_ok() { - panic!("should error"); - } else if cur.expected_err.is_none() && result.is_err() { - panic!("should not error"); - } - - if let Some(expected_packet) = cur.expected_packet { - if let (Some(first_unit), Some(parsed_first_unit)) = - (expected_packet.first_unit(), parsed.first_unit()) - { - assert_eq!( - parsed_first_unit.nal_unit_size, first_unit.nal_unit_size, - "invalid first unit NALUSize" - ); - assert_eq!( - parsed_first_unit.donl(), - first_unit.donl(), - "invalid first unit DONL" - ); - assert_eq!( - parsed_first_unit.nal_unit(), - first_unit.nal_unit(), - "invalid first unit NalUnit" - ); - } - - assert_eq!( - parsed.other_units().len(), - expected_packet.other_units().len(), - "number of other units mismatch" - ); - - for ndx in 0..expected_packet.other_units().len() { - assert_eq!( - parsed.other_units()[ndx].nalu_size(), - expected_packet.other_units()[ndx].nalu_size(), - "invalid unit NALUSize" - ); - - assert_eq!( - parsed.other_units()[ndx].dond(), - expected_packet.other_units()[ndx].dond(), - "invalid unit DOND" - ); - - assert_eq!( - parsed.other_units()[ndx].nal_unit(), - expected_packet.other_units()[ndx].nal_unit(), - "invalid first unit NalUnit" - ); - } - - assert_eq!( - parsed.other_units(), - expected_packet.other_units(), - "invalid payload" - ); - } - } - - Ok(()) -} - -#[test] -fn test_h265_fragmentation_unit_packet() -> Result<()> { - #[derive(Default)] - struct TestType { - raw: Bytes, - with_donl: bool, - expected_fu: Option, - expected_err: Option, - } - let tests = vec![ - TestType { - raw: Bytes::from_static(&[]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // FBit enabled in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x80, 0x01, 0x93, 0xaf]), - expected_err: Some(Error::ErrH265CorruptedPacket), - ..Default::default() - }, - // Type not '49' in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x40, 0x01, 0x93, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93, 0xaf]), - expected_fu: Some(H265FragmentationUnitPacket { - payload_header: H265NALUHeader::new(0x62, 0x01), - fu_header: H265FragmentationUnitHeader(0x93), - donl: None, - payload: Bytes::from_static(&[0xaf]), - might_need_donl: false, - }), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93, 0xcc]), - with_donl: true, - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93, 0xcc, 0xdd, 0xaf, 0x0d, 0x5a]), - with_donl: true, - expected_fu: Some(H265FragmentationUnitPacket { - payload_header: H265NALUHeader::new(0x62, 0x01), - fu_header: H265FragmentationUnitHeader(0x93), - donl: Some((0xcc << 8) | 0xdd), - payload: Bytes::from_static(&[0xaf, 0x0d, 0x5a]), - might_need_donl: false, - }), - ..Default::default() - }, - ]; - - for cur in tests { - let mut parsed = H265FragmentationUnitPacket::default(); - if cur.with_donl { - parsed.with_donl(cur.with_donl); - } - - let result = parsed.depacketize(&cur.raw); - - if cur.expected_err.is_some() && result.is_ok() { - panic!("should error"); - } else if cur.expected_err.is_none() && result.is_err() { - panic!("should not error"); - } - - if let Some(expected_fu) = &cur.expected_fu { - assert_eq!( - parsed.payload_header(), - expected_fu.payload_header(), - "invalid payload header" - ); - assert_eq!( - parsed.fu_header(), - expected_fu.fu_header(), - "invalid FU header" - ); - assert_eq!(parsed.donl(), expected_fu.donl(), "invalid DONL"); - assert_eq!(parsed.payload(), expected_fu.payload(), "invalid Payload"); - } - } - - Ok(()) -} - -#[test] -fn test_h265_temporal_scalability_control_information() -> Result<()> { - #[derive(Default)] - struct TestType { - value: H265TSCI, - expected_tl0picidx: u8, - expected_irap_pic_id: u8, - expected_s: bool, - expected_e: bool, - expected_res: u8, - } - - let tests = vec![ - TestType { - value: H265TSCI(((0xCA) << 24) | ((0xFE) << 16)), - expected_tl0picidx: 0xCA, - expected_irap_pic_id: 0xFE, - ..Default::default() - }, - TestType { - value: H265TSCI((1) << 15), - expected_s: true, - ..Default::default() - }, - TestType { - value: H265TSCI((1) << 14), - expected_e: true, - ..Default::default() - }, - TestType { - value: H265TSCI((0x0A) << 8), - expected_res: 0x0A, - ..Default::default() - }, - // Sets RES, and force sets S and E to 0. - TestType { - value: H265TSCI(((0xAA) << 8) & (u32::MAX ^ ((1) << 15)) & (u32::MAX ^ ((1) << 14))), - expected_res: 0xAA & 0b00111111, - ..Default::default() - }, - ]; - - for cur in tests { - assert_eq!( - cur.value.tl0picidx(), - cur.expected_tl0picidx, - "invalid TL0PICIDX" - ); - assert_eq!( - cur.value.irap_pic_id(), - cur.expected_irap_pic_id, - "invalid IrapPicID" - ); - assert_eq!(cur.value.s(), cur.expected_s, "invalid S"); - assert_eq!(cur.value.e(), cur.expected_e, "invalid E"); - assert_eq!(cur.value.res(), cur.expected_res, "invalid RES"); - } - - Ok(()) -} - -#[test] -fn test_h265_paci_packet() -> Result<()> { - #[derive(Default)] - struct TestType { - raw: Bytes, - expected_fu: Option, - expected_err: Option, - } - - let tests = vec![ - TestType { - raw: Bytes::from_static(&[]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // FBit enabled in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrH265CorruptedPacket), - ..Default::default() - }, - // Type not '50' in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x40, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - // Invalid header extension size - TestType { - raw: Bytes::from_static(&[0x64, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrInvalidH265PacketType), - ..Default::default() - }, - // No Header Extension - TestType { - raw: Bytes::from_static(&[0x64, 0x01, 0x64, 0x00, 0xab, 0xcd, 0xef]), - expected_fu: Some(H265PACIPacket { - payload_header: H265NALUHeader::new(0x64, 0x01), - paci_header_fields: ((0x64) << 8), - phes: Bytes::from_static(&[]), - payload: Bytes::from_static(&[0xab, 0xcd, 0xef]), - }), - ..Default::default() - }, - // Header Extension 1 byte - TestType { - raw: Bytes::from_static(&[0x64, 0x01, 0x64, 0x10, 0xff, 0xab, 0xcd, 0xef]), - expected_fu: Some(H265PACIPacket { - payload_header: H265NALUHeader::new(0x64, 0x01), - paci_header_fields: ((0x64) << 8) | (0x10), - phes: Bytes::from_static(&[0xff]), - payload: Bytes::from_static(&[0xab, 0xcd, 0xef]), - }), - ..Default::default() - }, - // Header Extension TSCI - TestType { - raw: Bytes::from_static(&[ - 0x64, 0x01, 0x64, 0b00111000, 0xaa, 0xbb, 0x80, 0xab, 0xcd, 0xef, - ]), - expected_fu: Some(H265PACIPacket { - payload_header: H265NALUHeader::new(0x64, 0x01), - paci_header_fields: ((0x64) << 8) | (0b00111000), - phes: Bytes::from_static(&[0xaa, 0xbb, 0x80]), - payload: Bytes::from_static(&[0xab, 0xcd, 0xef]), - }), - ..Default::default() - }, - ]; - - for cur in tests { - let mut parsed = H265PACIPacket::default(); - - let result = parsed.depacketize(&cur.raw); - - if cur.expected_err.is_some() && result.is_ok() { - panic!("should error"); - } else if cur.expected_err.is_none() && result.is_err() { - panic!("should not error"); - } - - if let Some(expected_fu) = &cur.expected_fu { - assert_eq!( - parsed.payload_header(), - expected_fu.payload_header(), - "invalid PayloadHeader" - ); - assert_eq!(parsed.a(), expected_fu.a(), "invalid A"); - assert_eq!(parsed.ctype(), expected_fu.ctype(), "invalid CType"); - assert_eq!(parsed.phs_size(), expected_fu.phs_size(), "invalid PHSsize"); - assert_eq!(parsed.f0(), expected_fu.f0(), "invalid F0"); - assert_eq!(parsed.f1(), expected_fu.f1(), "invalid F1"); - assert_eq!(parsed.f2(), expected_fu.f2(), "invalid F2"); - assert_eq!(parsed.y(), expected_fu.y(), "invalid Y"); - assert_eq!(parsed.phes(), expected_fu.phes(), "invalid PHES"); - assert_eq!(parsed.payload(), expected_fu.payload(), "invalid Payload"); - assert_eq!(parsed.tsci(), expected_fu.tsci(), "invalid TSCI"); - } - } - - Ok(()) -} - -#[test] -fn test_h265_packet() -> Result<()> { - #[derive(Default)] - struct TestType { - raw: Bytes, - with_donl: bool, - expected_packet_type: Option, - expected_err: Option, - } - let tests = vec![ - TestType { - raw: Bytes::from_static(&[]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x64, 0x01, 0x93, 0xaf]), - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - TestType { - raw: Bytes::from_static(&[0x01, 0x01]), - with_donl: true, - expected_err: Some(Error::ErrShortPacket), - ..Default::default() - }, - // FBit enabled in H265NALUHeader - TestType { - raw: Bytes::from_static(&[0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf]), - expected_err: Some(Error::ErrH265CorruptedPacket), - ..Default::default() - }, - // Valid H265SingleNALUnitPacket - TestType { - raw: Bytes::from_static(&[0x01, 0x01, 0xab, 0xcd, 0xef]), - expected_packet_type: Some(H265Payload::H265SingleNALUnitPacket( - H265SingleNALUnitPacket::default(), - )), - ..Default::default() - }, - // Invalid H265SingleNALUnitPacket - TestType { - raw: Bytes::from_static(&[0x01, 0x01, 0x93, 0xaf]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - // Valid H265PACIPacket - TestType { - raw: Bytes::from_static(&[ - 0x64, 0x01, 0x64, 0b00111000, 0xaa, 0xbb, 0x80, 0xab, 0xcd, 0xef, - ]), - expected_packet_type: Some(H265Payload::H265PACIPacket(H265PACIPacket::default())), - ..Default::default() - }, - // Valid H265FragmentationUnitPacket - TestType { - raw: Bytes::from_static(&[0x62, 0x01, 0x93, 0xcc, 0xdd, 0xaf, 0x0d, 0x5a]), - expected_packet_type: Some(H265Payload::H265FragmentationUnitPacket( - H265FragmentationUnitPacket::default(), - )), - with_donl: true, - ..Default::default() - }, - // Valid H265AggregationPacket - TestType { - raw: Bytes::from_static(&[ - 0x60, 0x01, 0xcc, 0xdd, 0x00, 0x02, 0xff, 0xee, 0x77, 0x00, 0x01, 0xaa, - ]), - expected_packet_type: Some(H265Payload::H265AggregationPacket( - H265AggregationPacket::default(), - )), - with_donl: true, - ..Default::default() - }, - // Invalid H265AggregationPacket - TestType { - raw: Bytes::from_static(&[0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00]), - expected_err: Some(Error::ErrShortPacket), - with_donl: true, - ..Default::default() - }, - ]; - - for cur in tests { - let mut pck = H265Packet::default(); - if cur.with_donl { - pck.with_donl(true); - } - - let result = pck.depacketize(&cur.raw); - - if cur.expected_err.is_some() && result.is_ok() { - panic!("should error"); - } else if cur.expected_err.is_none() && result.is_err() { - panic!("should not error"); - } - - if cur.expected_err.is_some() { - continue; - } - - if let Some(expected_packet_type) = &cur.expected_packet_type { - //TODO: assert_eq!(pck.packet(), expected_packet_type, "invalid packet type"); - let pck_packet = pck.payload(); - match (pck_packet, expected_packet_type) { - ( - &H265Payload::H265SingleNALUnitPacket(_), - &H265Payload::H265SingleNALUnitPacket(_), - ) => {} - ( - &H265Payload::H265FragmentationUnitPacket(_), - &H265Payload::H265FragmentationUnitPacket(_), - ) => {} - ( - &H265Payload::H265AggregationPacket(_), - &H265Payload::H265AggregationPacket(_), - ) => {} - (&H265Payload::H265PACIPacket(_), &H265Payload::H265PACIPacket(_)) => {} - _ => panic!(), - }; - } - } - - Ok(()) -} - -#[test] -fn test_h265_packet_real() -> Result<()> { - // Tests decoding of real H265 payloads extracted from a Wireshark dump. - let tests = vec![ - b"\x40\x01\x0c\x01\xff\xff\x01\x60\x00\x00\x03\x00\xb0\x00\x00\x03\x00\x00\x03\x00\x7b\xac\x09".to_vec(), - b"\x42\x01\x01\x01\x60\x00\x00\x03\x00\xb0\x00\x00\x03\x00\x00\x03\x00\x7b\xa0\x03\xc0\x80\x10\xe5\x8d\xae\x49\x32\xf4\xdc\x04\x04\x04\x02".to_vec(), - b"\x44\x01\xc0\xf2\xf0\x3c\x90".to_vec(), - b"\x4e\x01\xe5\x04\x61\x0c\x00\x00\x80".to_vec(), - b"\x62\x01\x93\xaf\x0d\x5a\xfe\x67\x77\x29\xc0\x74\xf3\x57\x4c\x16\x94\xaa\x7c\x2a\x64\x5f\xe9\xa5\xb7\x2a\xa3\x95\x9d\x94\xa7\xb4\xd3\xc4\x4a\xb1\xb7\x69\xca\xbe\x75\xc5\x64\xa8\x97\x4b\x8a\xbf\x7e\xf0\x0f\xc3\x22\x60\x67\xab\xae\x96\xd6\x99\xca\x7a\x8d\x35\x93\x1a\x67\x60\xe7\xbe\x7e\x13\x95\x3c\xe0\x11\xc1\xc1\xa7\x48\xef\xf7\x7b\xb0\xeb\x35\x49\x81\x4e\x4e\x54\xf7\x31\x6a\x38\xa1\xa7\x0c\xd6\xbe\x3b\x25\xba\x08\x19\x0b\x49\xfd\x90\xbb\x73\x7a\x45\x8c\xb9\x73\x43\x04\xc5\x5f\xda\x0f\xd5\x70\x4c\x11\xee\x72\xb8\x6a\xb4\x95\x62\x64\xb6\x23\x14\x7e\xdb\x0e\xa5\x0f\x86\x31\xe4\xd1\x64\x56\x43\xf6\xb7\xe7\x1b\x93\x4a\xeb\xd0\xa6\xe3\x1f\xce\xda\x15\x67\x05\xb6\x77\x36\x8b\x27\x5b\xc6\xf2\x95\xb8\x2b\xcc\x9b\x0a\x03\x05\xbe\xc3\xd3\x85\xf5\x69\xb6\x19\x1f\x63\x2d\x8b\x65\x9e\xc3\x9d\xd2\x44\xb3\x7c\x86\x3b\xea\xa8\x5d\x02\xe5\x40\x03\x20\x76\x48\xff\xf6\x2b\x0d\x18\xd6\x4d\x49\x70\x1a\x5e\xb2\x89\xca\xec\x71\x41\x79\x4e\x94\x17\x0c\x57\x51\x55\x14\x61\x40\x46\x4b\x3e\x17\xb2\xc8\xbd\x1c\x06\x13\x91\x72\xf8\xc8\xfc\x6f\xb0\x30\x9a\xec\x3b\xa6\xc9\x33\x0b\xa5\xe5\xf4\x65\x7a\x29\x8b\x76\x62\x81\x12\xaf\x20\x4c\xd9\x21\x23\x9e\xeb\xc9\x0e\x5b\x29\x35\x7f\x41\xcd\xce\xa1\xc4\xbe\x01\x30\xb9\x11\xc3\xb1\xe4\xce\x45\xd2\x5c\xb3\x1e\x69\x78\xba\xb1\x72\xe4\x88\x54\xd8\x5d\xd0\xa8\x3a\x74\xad\xe5\xc7\xc1\x59\x7c\x78\x15\x26\x37\x3d\x50\xae\xb3\xa4\x5b\x6c\x7d\x65\x66\x85\x4d\x16\x9a\x67\x74\xad\x55\x32\x3a\x84\x85\x0b\x6a\xeb\x24\x97\xb4\x20\x4d\xca\x41\x61\x7a\xd1\x7b\x60\xdb\x7f\xd5\x61\x22\xcf\xd1\x7e\x4c\xf3\x85\xfd\x13\x63\xe4\x9d\xed\xac\x13\x0a\xa0\x92\xb7\x34\xde\x65\x0f\xd9\x0f\x9b\xac\xe2\x47\xe8\x5c\xb3\x11\x8e\xc6\x08\x19\xd0\xb0\x85\x52\xc8\x5c\x1b\x08\x0a\xce\xc9\x6b\xa7\xef\x95\x2f\xd0\xb8\x63\xe5\x4c\xd4\xed\x6e\x87\xe9\xd4\x0a\xe6\x11\x44\x63\x00\x94\x18\xe9\x28\xba\xcf\x92\x43\x06\x59\xdd\x37\x4f\xd3\xef\x9d\x31\x5e\x9b\x48\xf9\x1f\x3e\x7b\x95\x3a\xbd\x1f\x71\x55\x0c\x06\xf9\x86\xf8\x3d\x39\x16\x50\xb3\x21\x11\x19\x6f\x70\xa9\x48\xe8\xbb\x0a\x11\x23\xf8\xab\xfe\x44\xe0\xbb\xe8\x64\xfa\x85\xe4\x02\x55\x88\x41\xc6\x30\x7f\x10\xad\x75\x02\x4b\xef\xe1\x0b\x06\x3c\x10\x49\x83\xf9\xd1\x3e\x3e\x67\x86\x4c\xf8\x9d\xde\x5a\xc4\xc8\xcf\xb6\xf4\xb0\xd3\x34\x58\xd4\x7b\x4d\xd3\x37\x63\xb2\x48\x8a\x7e\x20\x00\xde\xb4\x42\x8f\xda\xe9\x43\x9e\x0c\x16\xce\x79\xac\x2c\x70\xc1\x89\x05\x36\x62\x6e\xd9\xbc\xfb\x63\xc6\x79\x89\x3c\x90\x89\x2b\xd1\x8c\xe0\xc2\x54\xc7\xd6\xb4\xe8\x9e\x96\x55\x6e\x7b\xd5\x7f\xac\xd4\xa7\x1c\xa0\xdf\x01\x30\xad\xc0\x9f\x69\x06\x10\x43\x7f\xf4\x5d\x62\xa3\xea\x73\xf2\x14\x79\x19\x13\xea\x59\x14\x79\xa8\xe7\xce\xce\x44\x25\x13\x41\x18\x57\xdd\xce\xe4\xbe\xcc\x20\x80\x29\x71\x73\xa7\x7c\x86\x39\x76\xf4\xa7\x1c\x63\x24\x21\x93\x1e\xb5\x9a\x5c\x8a\x9e\xda\x8b\x9d\x88\x97\xfc\x98\x7d\x26\x74\x04\x1f\xa8\x10\x4f\x45\xcd\x46\xe8\x28\xe4\x8e\x59\x67\x63\x4a\xcf\x1e\xed\xdd\xbb\x79\x2f\x8d\x94\xab\xfc\xdb\xc5\x79\x1a\x4d\xcd\x53\x41\xdf\xd1\x7a\x8f\x46\x3e\x1f\x79\x88\xe3\xee\x9f\xc4\xc1\xe6\x2e\x89\x4d\x28\xc9\xca\x28\xc2\x0a\xc5\xc7\xf1\x22\xcd\xb3\x36\xfa\xe3\x7e\xa6\xcd\x95\x55\x5e\x0e\x1a\x75\x7f\x65\x27\xd3\x37\x4f\x23\xc5\xab\x49\x68\x4e\x02\xb5\xbf\xd7\x95\xc0\x78\x67\xbc\x1a\xe9\xae\x6f\x44\x58\x8a\xc2\xce\x42\x98\x4e\x77\xc7\x2a\xa0\xa7\x7d\xe4\x3b\xd1\x20\x82\x1a\xd3\xe2\xc7\x76\x5d\x06\x46\xb5\x24\xd7\xfb\x57\x63\x2b\x19\x51\x48\x65\x6d\xfb\xe0\x98\xd1\x14\x0e\x17\x64\x29\x34\x6f\x6e\x66\x9e\x8d\xc9\x89\x49\x69\xee\x74\xf3\x35\xe6\x8b\x67\x56\x95\x7f\x1b\xe9\xed\x8c\x0f\xe2\x19\x59\xbf\x03\x35\x55\x3c\x04\xbc\x40\x52\x90\x10\x08\xad\xa7\x65\xe0\x31\xcb\xcf\x3d\xd4\x62\x68\x01\x0d\xed\xf5\x28\x64\x2d\xaa\x7c\x99\x15\x8d\x70\x32\x53\xb8\x9d\x0a\x3c\xbf\x91\x02\x04\xd0\xee\x87\xce\x04\xcc\x3e\xa8\x20\xfd\x97\xdf\xbf\x4a\xbc\xfc\xc9\x7c\x77\x21\xcc\x23\x6f\x59\x38\xd8\xd9\xa0\x0e\xb1\x23\x4e\x04\x3f\x14\x9e\xcc\x05\x54\xab\x20\x69\xed\xa4\xd5\x1d\xb4\x1b\x52\xed\x6a\xea\xeb\x7f\xd1\xbc\xfd\x75\x20\xa0\x1c\x59\x8c\x5a\xa1\x2a\x70\x64\x11\xb1\x7b\xc1\x24\x80\x28\x51\x4c\x94\xa1\x95\x64\x72\xe8\x90\x67\x38\x74\x2b\xab\x38\x46\x12\x71\xce\x19\x98\x98\xf7\x89\xd4\xfe\x2f\x2a\xc5\x61\x20\xd0\xa4\x1a\x51\x3c\x82\xc8\x18\x31\x7a\x10\xe8\x1c\xc6\x95\x5a\xa0\x82\x88\xce\x8f\x4b\x47\x85\x7e\x89\x95\x95\x52\x1e\xac\xce\x45\x57\x61\x38\x97\x2b\x62\xa5\x14\x6f\xc3\xaa\x6c\x35\x83\xc9\xa3\x1e\x30\x89\xf4\xb1\xea\x4f\x39\xde\xde\xc7\x46\x5c\x0e\x85\x41\xec\x6a\xa4\xcb\xee\x70\x9c\x57\xd9\xf4\xa1\xc3\x9c\x2a\x0a\xf0\x5d\x58\xb0\xae\xd4\xdc\xc5\x6a\xa8\x34\xfa\x23\xef\xef\x08\x39\xc3\x3d\xea\x11\x6e\x6a\xe0\x1e\xd0\x52\xa8\xc3\x6e\xc9\x1c\xfc\xd0\x0c\x4c\xea\x0d\x82\xcb\xdd\x29\x1a\xc4\x4f\x6e\xa3\x4d\xcb\x7a\x38\x77\xe5\x15\x6e\xad\xfa\x9d\x2f\x02\xb6\x39\x84\x3a\x60\x8f\x71\x9f\x92\xe5\x24\x4f\xbd\x18\x49\xd5\xef\xbf\x70\xfb\xd1\x4c\x2e\xfc\x2f\x36\xf3\x00\x31\x2e\x90\x18\xcc\xf4\x71\xb9\xe4\xf9\xbe\xcb\x5e\xff\xf3\xe7\xf8\xca\x03\x60\x66\xb3\xc9\x5a\xf9\x74\x09\x02\x57\xb6\x90\x94\xfc\x41\x35\xdc\x35\x3f\x32\x7a\xa6\xa5\xcd\x8a\x8f\xc8\x3d\xc8\x81\xc3\xec\x37\x74\x86\x61\x41\x0d\xc5\xe2\xc8\x0c\x84\x2b\x3b\x71\x58\xde\x1b\xe3\x20\x65\x2e\x76\xf4\x98\xd8\xaa\x78\xe6\xeb\xb8\x85\x0d\xa0\xd0\xf5\x57\x64\x01\x58\x55\x82\xd5\x0f\x2d\x9c\x3e\x2a\xa0\x7e\xaf\x42\xf3\x37\xd1\xb3\xaf\xda\x5b\xa9\xda\xe3\x89\x5d\xf1\xca\xa5\x12\x3d\xe7\x91\x95\x53\x21\x72\xca\x7f\xf6\x79\x59\x21\xcf\x30\x18\xfb\x78\x55\x40\x59\xc3\xf9\xf1\xdd\x58\x44\x5e\x83\x11\x5c\x2d\x1d\x91\xf6\x01\x3d\x3f\xd4\x33\x81\x66\x6c\x40\x7a\x9d\x70\x10\x58\xe6\x53\xad\x85\x11\x99\x3e\x4b\xbc\x31\xc6\x78\x9d\x79\xc5\xde\x9f\x2e\x43\xfa\x76\x84\x2f\xfd\x28\x75\x12\x48\x25\xfd\x15\x8c\x29\x6a\x91\xa4\x63\xc0\xa2\x8c\x41\x3c\xf1\xb0\xf8\xdf\x66\xeb\xbd\x14\x88\xa9\x81\xa7\x35\xc4\x41\x40\x6c\x10\x3f\x09\xbd\xb5\xd3\x7a\xee\x4b\xd5\x86\xff\x36\x03\x6b\x78\xde".to_vec(), - b"\x62\x01\x53\x8a\xe9\x25\xe1\x06\x09\x8e\xba\x12\x74\x87\x09\x9a\x95\xe4\x86\x62\x2b\x4b\xf9\xa6\x2e\x7b\x35\x43\xf7\x39\x99\x0f\x3b\x6f\xfd\x1a\x6e\x23\x54\x70\xb5\x1d\x10\x1c\x63\x40\x96\x99\x41\xb6\x96\x0b\x70\x98\xec\x17\xb0\xaa\xdc\x4a\xab\xe8\x3b\xb7\x6b\x00\x1c\x5b\xc3\xe0\xa2\x8b\x7c\x17\xc8\x92\xc9\xb0\x92\xb6\x70\x84\x95\x30".to_vec(), - b"\x4e\x01\xe5\x04\x35\xac\x00\x00\x80".to_vec(), - b"\x62\x01\x41\xb0\x75\x5c\x27\x46\xef\x8a\xe7\x1d\x50\x38\xb2\x13\x33\xe0\x79\x35\x1b\xc2\xb5\x79\x73\xe7\xc2\x6f\xb9\x1a\x8c\x21\x0e\xa9\x54\x17\x6c\x41\xab\xc8\x16\x57\xec\x5e\xeb\x89\x3b\xa9\x90\x8c\xff\x4d\x46\x8b\xf0\xd9\xc0\xd0\x51\xcf\x8b\x88\xf1\x5f\x1e\x9e\xc1\xb9\x1f\xe3\x06\x45\x35\x8a\x47\xe8\x9a\xf2\x4f\x19\x4c\xf8\xce\x68\x1b\x63\x34\x11\x75\xea\xe5\xb1\x0f\x38\xcc\x05\x09\x8b\x3e\x2b\x88\x84\x9d\xc5\x03\xc3\xc0\x90\x32\xe2\x45\x69\xb1\xe5\xf7\x68\x6b\x16\x90\xa0\x40\xe6\x18\x74\xd8\x68\xf3\x34\x38\x99\xf2\x6c\xb7\x1a\x35\x21\xca\x52\x56\x4c\x7f\xb2\xa3\xd5\xb8\x40\x50\x48\x3e\xdc\xdf\x0b\xf5\x54\x5a\x15\x1a\xe2\xc3\xb4\x94\xda\x3f\xb5\x34\xa2\xca\xbc\x2f\xe0\xa4\xe5\x69\xf4\xbf\x62\x4d\x15\x21\x1b\x11\xfc\x39\xaa\x86\x74\x96\x63\xfd\x07\x53\x26\xf6\x34\x72\xeb\x14\x37\x98\x0d\xf4\x68\x91\x2c\x6b\x46\x83\x88\x82\x04\x8b\x9f\xb8\x32\x73\x75\x8b\xf9\xac\x71\x42\xd1\x2d\xb4\x28\x28\xf5\x78\xe0\x32\xf3\xe1\xfc\x43\x6b\xf9\x92\xf7\x48\xfe\x7f\xc0\x17\xbd\xfd\xba\x2f\x58\x6f\xee\x84\x03\x18\xce\xb0\x9d\x8d\xeb\x22\xf1\xfc\xb1\xcf\xff\x2f\xb2\x9f\x6c\xe5\xb4\x69\xdc\xdd\x20\x93\x00\x30\xad\x56\x04\x66\x7e\xa3\x3c\x18\x4b\x43\x66\x00\x27\x1e\x1c\x09\x11\xd8\xf4\x8a\x9e\xc5\x6a\x94\xe5\xae\x0b\x8a\xbe\x84\xda\xe5\x44\x7f\x38\x1c\xe7\xbb\x03\x19\x66\xe1\x5d\x1d\xc1\xbd\x3d\xc6\xb7\xe3\xff\x7f\x8e\xff\x1e\xf6\x9e\x6f\x58\x27\x74\x65\xef\x02\x5d\xa4\xde\x27\x7f\x51\xe3\x4b\x9e\x3f\x79\x83\xbd\x1b\x8f\x0d\x77\xfb\xbc\xc5\x9f\x15\xa7\x4e\x05\x8a\x24\x97\x66\xb2\x7c\xf6\xe1\x84\x54\xdb\x39\x5e\xf6\x1b\x8f\x05\x73\x1d\xb6\x8e\xd7\x09\x9a\xc5\x92\x80".to_vec(), - ]; - - for cur in tests { - let mut pck = H265Packet::default(); - let _ = pck.depacketize(&Bytes::from(cur))?; - } - - Ok(()) -} diff --git a/rtp/src/codecs/h265/mod.rs b/rtp/src/codecs/h265/mod.rs deleted file mode 100644 index 8faeb6256..000000000 --- a/rtp/src/codecs/h265/mod.rs +++ /dev/null @@ -1,1020 +0,0 @@ -use bytes::{BufMut, Bytes, BytesMut}; - -use super::h264::ANNEXB_NALUSTART_CODE; -use crate::error::{Error, Result}; -use crate::packetizer::{Depacketizer, Payloader}; - -#[cfg(test)] -mod h265_test; - -pub static ANNEXB_3_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x01]); -pub static SING_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x1C, 0x01]); -pub static AGGR_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x60, 0x01]); -pub static FRAG_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x62, 0x01]); -pub static FU_HDR_IDR_S: u8 = 0x93; -pub static FU_HDR_IDR_M: u8 = 0x13; -pub static FU_HDR_IDR_E: u8 = 0x53; -pub static FU_HDR_P_S: u8 = 0x81; -pub static FU_HDR_P_M: u8 = 0x01; -pub static FU_HDR_P_E: u8 = 0x41; -pub static FU_HDR_B_S: u8 = 0x80; -pub static FU_HDR_B_M: u8 = 0x00; -pub static FU_HDR_B_E: u8 = 0x40; -pub const RTP_OUTBOUND_MTU: usize = 1200; -pub const H265FRAGMENTATION_UNIT_HEADER_SIZE: usize = 1; -pub const NAL_HEADER_SIZE: usize = 2; - -#[derive(PartialEq, Hash, Debug, Copy, Clone)] -pub enum UnitType { - VPS = 32, - SPS = 33, - PPS = 34, - CRA = 21, - SEI = 39, - IDR = 19, - PFR = 1, - BFR = 0, - IGNORE = -1, -} -impl UnitType { - pub fn for_id(id: u8) -> Result { - if id > 64 { - Err(Error::ErrUnhandledNaluType) - } else { - let t = match id { - 32 => UnitType::VPS, - 33 => UnitType::SPS, - 34 => UnitType::PPS, - 21 => UnitType::CRA, - 39 => UnitType::SEI, - 19 => UnitType::IDR, - 1 => UnitType::PFR, - 0 => UnitType::BFR, - _ => UnitType::IGNORE, // shouldn't happen - }; - Ok(t) - } - } -} - -#[derive(Default, Debug, Clone)] -pub struct HevcPayloader { - vps_nalu: Option, - sps_nalu: Option, - pps_nalu: Option, -} - -impl HevcPayloader { - pub fn parse(nalu: &Bytes) -> (Vec, usize) { - let finder = memchr::memmem::Finder::new(&ANNEXB_NALUSTART_CODE); - let nals = finder.find_iter(nalu).collect::>(); - if nals.is_empty() { - let finder = memchr::memmem::Finder::new(&ANNEXB_3_NALUSTART_CODE); - return (finder.find_iter(nalu).collect::>(), 3); - } - (nals, 4) - } - - fn emit(&mut self, nalu: &Bytes, mtu: usize, payloads: &mut Vec) { - if nalu.is_empty() { - return; - } - let payload_header = H265NALUHeader::new(nalu[0], nalu[1]); - let payload_nalu_type = payload_header.nalu_type(); - let nalu_type = UnitType::for_id(payload_nalu_type).unwrap_or(UnitType::IGNORE); - if nalu_type == UnitType::IGNORE { - return; - } else if nalu_type == UnitType::VPS { - self.vps_nalu.replace(nalu.clone()); - } else if nalu_type == UnitType::SPS { - self.sps_nalu.replace(nalu.clone()); - } else if nalu_type == UnitType::PPS { - self.pps_nalu.replace(nalu.clone()); - } - if let (Some(vps_nalu), Some(sps_nalu), Some(pps_nalu)) = - (&self.vps_nalu, &self.sps_nalu, &self.pps_nalu) - { - // Pack current NALU with SPS and PPS as STAP-A - let vps_len = (vps_nalu.len() as u16).to_be_bytes(); - let sps_len = (sps_nalu.len() as u16).to_be_bytes(); - let pps_len = (pps_nalu.len() as u16).to_be_bytes(); - - // TODO DONL not impl yet - let mut aggr_nalu = BytesMut::new(); - aggr_nalu.extend_from_slice(&AGGR_PAYLOAD_HDR); - aggr_nalu.extend_from_slice(&vps_len); - aggr_nalu.extend_from_slice(vps_nalu); - aggr_nalu.extend_from_slice(&sps_len); - aggr_nalu.extend_from_slice(sps_nalu); - aggr_nalu.extend_from_slice(&pps_len); - aggr_nalu.extend_from_slice(pps_nalu); - if aggr_nalu.len() <= mtu { - payloads.push(Bytes::from(aggr_nalu)); - self.vps_nalu.take(); - self.sps_nalu.take(); - self.pps_nalu.take(); - return; - } - } else if nalu_type == UnitType::VPS - || nalu_type == UnitType::SPS - || nalu_type == UnitType::PPS - { - return; - } - // if self.sps_nalu.is_some() && self.pps_nalu.is_some() { - // self.sps_nalu = None; - // self.pps_nalu = None; - // } - - // Single NALU - if nalu.len() <= mtu { - payloads.push(nalu.clone()); - return; - } - let max_fragment_size = - mtu as isize - NAL_HEADER_SIZE as isize - H265FRAGMENTATION_UNIT_HEADER_SIZE as isize; - let nalu_data = nalu; - let mut nalu_data_index = 2; - let nalu_data_length = nalu.len() as isize - nalu_data_index; - let mut nalu_data_remaining = nalu_data_length; - if std::cmp::min(max_fragment_size, nalu_data_remaining) <= 0 { - return; - } - while nalu_data_remaining > 0 { - let current_fragment_size = std::cmp::min(max_fragment_size, nalu_data_remaining); - //out: = make([]byte, fuaHeaderSize + currentFragmentSize) - let mut out = BytesMut::with_capacity( - H265FRAGMENTATION_UNIT_HEADER_SIZE + current_fragment_size as usize, - ); - out.extend_from_slice(&FRAG_PAYLOAD_HDR); - let is_first = nalu_data_index == 2; - let is_last = !is_first && current_fragment_size < max_fragment_size; - /* - +---------------+ - |0|1|2|3|4|5|6|7| - +-+-+-+-+-+-+-+-+ - |S|E| fu_type | - +---------------+ - */ - if nalu_type == UnitType::IDR { - if is_first { - out.put_u8(FU_HDR_IDR_S); - } else if is_last { - out.put_u8(FU_HDR_IDR_E); - } else { - out.put_u8(FU_HDR_IDR_M); - } - } else if nalu_type == UnitType::PFR { - if is_first { - out.put_u8(FU_HDR_P_S); - } else if is_last { - out.put_u8(FU_HDR_P_E); - } else { - out.put_u8(FU_HDR_P_M); - } - } else if nalu_type == UnitType::BFR { - if is_first { - out.put_u8(FU_HDR_B_S); - } else if is_last { - out.put_u8(FU_HDR_B_E); - } else { - out.put_u8(FU_HDR_B_M); - } - } - - out.extend_from_slice( - &nalu_data - [nalu_data_index as usize..(nalu_data_index + current_fragment_size) as usize], - ); - // println!("pkt payload {:?}", &out[0..5]); - payloads.push(out.freeze()); - - nalu_data_remaining -= current_fragment_size; - nalu_data_index += current_fragment_size; - } - } -} - -impl Payloader for HevcPayloader { - /// Payload fragments a H264 packet across one or more byte arrays - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - let mut payloads = vec![]; - - let (nal_idxs, offset) = HevcPayloader::parse(payload); - let nal_len = nal_idxs.len(); - for (i, start) in nal_idxs.iter().enumerate() { - let end = if (i + 1) < nal_len { - nal_idxs[i + 1] - } else { - payload.len() - }; - // println!( - // "start {}, end {} payload {:?}", - // start, - // end, - // &payload - // .slice((start + offset)..(start + offset + 5)) - // .to_vec() - // ); - self.emit(&payload.slice((start + offset)..end), mtu, &mut payloads); - } - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} - -/// -/// Network Abstraction Unit Header implementation -/// - -const H265NALU_HEADER_SIZE: usize = 2; -/// -const H265NALU_AGGREGATION_PACKET_TYPE: u8 = 48; -/// -const H265NALU_FRAGMENTATION_UNIT_TYPE: u8 = 49; -/// -const H265NALU_PACI_PACKET_TYPE: u8 = 50; - -/// H265NALUHeader is a H265 NAL Unit Header -/// -/// +---------------+---------------+ -/// |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |F| Type | layer_id | tid | -/// +-------------+-----------------+ -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct H265NALUHeader(pub u16); - -impl H265NALUHeader { - pub fn new(high_byte: u8, low_byte: u8) -> Self { - H265NALUHeader(((high_byte as u16) << 8) | low_byte as u16) - } - - /// f is the forbidden bit, should always be 0. - pub fn f(&self) -> bool { - (self.0 >> 15) != 0 - } - - /// nalu_type of NAL Unit. - pub fn nalu_type(&self) -> u8 { - // 01111110 00000000 - const MASK: u16 = 0b01111110 << 8; - ((self.0 & MASK) >> (8 + 1)) as u8 - } - - /// is_type_vcl_unit returns whether or not the NAL Unit type is a VCL NAL unit. - pub fn is_type_vcl_unit(&self) -> bool { - // Type is coded on 6 bits - const MSB_MASK: u8 = 0b00100000; - (self.nalu_type() & MSB_MASK) == 0 - } - - /// layer_id should always be 0 in non-3D HEVC context. - pub fn layer_id(&self) -> u8 { - // 00000001 11111000 - const MASK: u16 = (0b00000001 << 8) | 0b11111000; - ((self.0 & MASK) >> 3) as u8 - } - - /// tid is the temporal identifier of the NAL unit +1. - pub fn tid(&self) -> u8 { - const MASK: u16 = 0b00000111; - (self.0 & MASK) as u8 - } - - /// is_aggregation_packet returns whether or not the packet is an Aggregation packet. - pub fn is_aggregation_packet(&self) -> bool { - self.nalu_type() == H265NALU_AGGREGATION_PACKET_TYPE - } - - /// is_fragmentation_unit returns whether or not the packet is a Fragmentation Unit packet. - pub fn is_fragmentation_unit(&self) -> bool { - self.nalu_type() == H265NALU_FRAGMENTATION_UNIT_TYPE - } - - /// is_paci_packet returns whether or not the packet is a PACI packet. - pub fn is_paci_packet(&self) -> bool { - self.nalu_type() == H265NALU_PACI_PACKET_TYPE - } -} - -/// -/// Single NAL Unit Packet implementation -/// -/// H265SingleNALUnitPacket represents a NALU packet, containing exactly one NAL unit. -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | PayloadHdr | DONL (conditional) | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// | NAL unit payload data | -/// | | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | :...OPTIONAL RTP padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265SingleNALUnitPacket { - /// payload_header is the header of the H265 packet. - payload_header: H265NALUHeader, - /// donl is a 16-bit field, that may or may not be present. - donl: Option, - /// payload of the fragmentation unit. - payload: Bytes, - - might_need_donl: bool, -} - -impl H265SingleNALUnitPacket { - /// with_donl can be called to specify whether or not DONL might be parsed. - /// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream. - pub fn with_donl(&mut self, value: bool) { - self.might_need_donl = value; - } - - /// depacketize parses the passed byte slice and stores the result in the H265SingleNALUnitPacket this method is called upon. - fn depacketize(&mut self, payload: &Bytes) -> Result<()> { - if payload.len() <= H265NALU_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - let payload_header = H265NALUHeader::new(payload[0], payload[1]); - if payload_header.f() { - return Err(Error::ErrH265CorruptedPacket); - } - if payload_header.is_fragmentation_unit() - || payload_header.is_paci_packet() - || payload_header.is_aggregation_packet() - { - return Err(Error::ErrInvalidH265PacketType); - } - - let mut payload = payload.slice(2..); - - if self.might_need_donl { - // sizeof(uint16) - if payload.len() <= 2 { - return Err(Error::ErrShortPacket); - } - - let donl = ((payload[0] as u16) << 8) | (payload[1] as u16); - self.donl = Some(donl); - payload = payload.slice(2..); - } - - self.payload_header = payload_header; - self.payload = payload; - - Ok(()) - } - - /// payload_header returns the NALU header of the packet. - pub fn payload_header(&self) -> H265NALUHeader { - self.payload_header - } - - /// donl returns the DONL of the packet. - pub fn donl(&self) -> Option { - self.donl - } - - /// payload returns the Fragmentation Unit packet payload. - pub fn payload(&self) -> Bytes { - self.payload.clone() - } -} - -/// -/// Aggregation Packets implementation -/// -/// H265AggregationUnitFirst represent the First Aggregation Unit in an AP. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// : DONL (conditional) | NALU size | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | NALU size | | -/// +-+-+-+-+-+-+-+-+ NAL unit | -/// | | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | : -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265AggregationUnitFirst { - donl: Option, - nal_unit_size: u16, - nal_unit: Bytes, -} - -impl H265AggregationUnitFirst { - /// donl field, when present, specifies the value of the 16 least - /// significant bits of the decoding order number of the aggregated NAL - /// unit. - pub fn donl(&self) -> Option { - self.donl - } - - /// nalu_size represents the size, in bytes, of the nal_unit. - pub fn nalu_size(&self) -> u16 { - self.nal_unit_size - } - - /// nal_unit payload. - pub fn nal_unit(&self) -> Bytes { - self.nal_unit.clone() - } -} - -/// H265AggregationUnit represent the an Aggregation Unit in an AP, which is not the first one. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// : DOND (cond) | NALU size | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// | NAL unit | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | : -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265AggregationUnit { - dond: Option, - nal_unit_size: u16, - nal_unit: Bytes, -} - -impl H265AggregationUnit { - /// dond field plus 1 specifies the difference between - /// the decoding order number values of the current aggregated NAL unit - /// and the preceding aggregated NAL unit in the same AP. - pub fn dond(&self) -> Option { - self.dond - } - - /// nalu_size represents the size, in bytes, of the nal_unit. - pub fn nalu_size(&self) -> u16 { - self.nal_unit_size - } - - /// nal_unit payload. - pub fn nal_unit(&self) -> Bytes { - self.nal_unit.clone() - } -} - -/// H265AggregationPacket represents an Aggregation packet. -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | PayloadHdr (Type=48) | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | -/// | | -/// | two or more aggregation units | -/// | | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | :...OPTIONAL RTP padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265AggregationPacket { - first_unit: Option, - other_units: Vec, - - might_need_donl: bool, -} - -impl H265AggregationPacket { - /// with_donl can be called to specify whether or not DONL might be parsed. - /// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream. - pub fn with_donl(&mut self, value: bool) { - self.might_need_donl = value; - } - - /// depacketize parses the passed byte slice and stores the result in the H265AggregationPacket this method is called upon. - fn depacketize(&mut self, payload: &Bytes) -> Result<()> { - if payload.len() <= H265NALU_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - let payload_header = H265NALUHeader::new(payload[0], payload[1]); - if payload_header.f() { - return Err(Error::ErrH265CorruptedPacket); - } - if !payload_header.is_aggregation_packet() { - return Err(Error::ErrInvalidH265PacketType); - } - - // First parse the first aggregation unit - let mut payload = payload.slice(2..); - let mut first_unit = H265AggregationUnitFirst::default(); - - if self.might_need_donl { - if payload.len() < 2 { - return Err(Error::ErrShortPacket); - } - - let donl = ((payload[0] as u16) << 8) | (payload[1] as u16); - first_unit.donl = Some(donl); - - payload = payload.slice(2..); - } - if payload.len() < 2 { - return Err(Error::ErrShortPacket); - } - first_unit.nal_unit_size = ((payload[0] as u16) << 8) | (payload[1] as u16); - payload = payload.slice(2..); - - if payload.len() < first_unit.nal_unit_size as usize { - return Err(Error::ErrShortPacket); - } - - first_unit.nal_unit = payload.slice(..first_unit.nal_unit_size as usize); - payload = payload.slice(first_unit.nal_unit_size as usize..); - - // Parse remaining Aggregation Units - let mut units = vec![]; //H265AggregationUnit - loop { - let mut unit = H265AggregationUnit::default(); - - if self.might_need_donl { - if payload.is_empty() { - break; - } - - let dond = payload[0]; - unit.dond = Some(dond); - - payload = payload.slice(1..); - } - - if payload.len() < 2 { - break; - } - unit.nal_unit_size = ((payload[0] as u16) << 8) | (payload[1] as u16); - payload = payload.slice(2..); - - if payload.len() < unit.nal_unit_size as usize { - break; - } - - unit.nal_unit = payload.slice(..unit.nal_unit_size as usize); - payload = payload.slice(unit.nal_unit_size as usize..); - - units.push(unit); - } - - // There need to be **at least** two Aggregation Units (first + another one) - if units.is_empty() { - return Err(Error::ErrShortPacket); - } - - self.first_unit = Some(first_unit); - self.other_units = units; - - Ok(()) - } - - /// first_unit returns the first Aggregated Unit of the packet. - pub fn first_unit(&self) -> Option<&H265AggregationUnitFirst> { - self.first_unit.as_ref() - } - - /// other_units returns the all the other Aggregated Unit of the packet (excluding the first one). - pub fn other_units(&self) -> &[H265AggregationUnit] { - self.other_units.as_slice() - } -} - -/// -/// Fragmentation Unit implementation -/// - -/// H265FragmentationUnitHeader is a H265 FU Header -/// +---------------+ -/// |0|1|2|3|4|5|6|7| -/// +-+-+-+-+-+-+-+-+ -/// |S|E| fu_type | -/// +---------------+ -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct H265FragmentationUnitHeader(pub u8); - -impl H265FragmentationUnitHeader { - /// s represents the start of a fragmented NAL unit. - pub fn s(&self) -> bool { - const MASK: u8 = 0b10000000; - ((self.0 & MASK) >> 7) != 0 - } - - /// e represents the end of a fragmented NAL unit. - pub fn e(&self) -> bool { - const MASK: u8 = 0b01000000; - ((self.0 & MASK) >> 6) != 0 - } - - /// fu_type MUST be equal to the field Type of the fragmented NAL unit. - pub fn fu_type(&self) -> u8 { - const MASK: u8 = 0b00111111; - self.0 & MASK - } -} - -/// H265FragmentationUnitPacket represents a single Fragmentation Unit packet. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | PayloadHdr (Type=49) | FU header | DONL (cond) | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| -/// | DONL (cond) | | -/// |-+-+-+-+-+-+-+-+ | -/// | FU payload | -/// | | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | :...OPTIONAL RTP padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265FragmentationUnitPacket { - /// payload_header is the header of the H265 packet. - payload_header: H265NALUHeader, - /// fu_header is the header of the fragmentation unit - fu_header: H265FragmentationUnitHeader, - /// donl is a 16-bit field, that may or may not be present. - donl: Option, - /// payload of the fragmentation unit. - payload: Bytes, - - might_need_donl: bool, -} - -impl H265FragmentationUnitPacket { - /// with_donl can be called to specify whether or not DONL might be parsed. - /// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream. - pub fn with_donl(&mut self, value: bool) { - self.might_need_donl = value; - } - - /// depacketize parses the passed byte slice and stores the result in the H265FragmentationUnitPacket this method is called upon. - fn depacketize(&mut self, payload: &Bytes) -> Result<()> { - const TOTAL_HEADER_SIZE: usize = H265NALU_HEADER_SIZE + H265FRAGMENTATION_UNIT_HEADER_SIZE; - if payload.len() <= TOTAL_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - let payload_header = H265NALUHeader::new(payload[0], payload[1]); - if payload_header.f() { - return Err(Error::ErrH265CorruptedPacket); - } - if !payload_header.is_fragmentation_unit() { - return Err(Error::ErrInvalidH265PacketType); - } - - let fu_header = H265FragmentationUnitHeader(payload[2]); - let mut payload = payload.slice(3..); - - if fu_header.s() && self.might_need_donl { - if payload.len() <= 2 { - return Err(Error::ErrShortPacket); - } - - let donl = ((payload[0] as u16) << 8) | (payload[1] as u16); - self.donl = Some(donl); - payload = payload.slice(2..); - } - - self.payload_header = payload_header; - self.fu_header = fu_header; - self.payload = payload; - - Ok(()) - } - - /// payload_header returns the NALU header of the packet. - pub fn payload_header(&self) -> H265NALUHeader { - self.payload_header - } - - /// fu_header returns the Fragmentation Unit Header of the packet. - pub fn fu_header(&self) -> H265FragmentationUnitHeader { - self.fu_header - } - - /// donl returns the DONL of the packet. - pub fn donl(&self) -> Option { - self.donl - } - - /// payload returns the Fragmentation Unit packet payload. - pub fn payload(&self) -> Bytes { - self.payload.clone() - } -} - -/// -/// PACI implementation -/// - -/// H265PACIPacket represents a single H265 PACI packet. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | PayloadHdr (Type=50) |A| cType | phssize |F0..2|Y| -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | payload Header Extension Structure (phes) | -/// |=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=| -/// | | -/// | PACI payload: NAL unit | -/// | . . . | -/// | | -/// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | :...OPTIONAL RTP padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Reference: -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265PACIPacket { - /// payload_header is the header of the H265 packet. - payload_header: H265NALUHeader, - - /// Field which holds value for `A`, `cType`, `phssize`, `F0`, `F1`, `F2` and `Y` fields. - paci_header_fields: u16, - - /// phes is a header extension, of byte length `phssize` - phes: Bytes, - - /// payload contains NAL units & optional padding - payload: Bytes, -} - -impl H265PACIPacket { - /// payload_header returns the NAL Unit Header. - pub fn payload_header(&self) -> H265NALUHeader { - self.payload_header - } - - /// a copies the F bit of the PACI payload NALU. - pub fn a(&self) -> bool { - const MASK: u16 = 0b10000000 << 8; - (self.paci_header_fields & MASK) != 0 - } - - /// ctype copies the Type field of the PACI payload NALU. - pub fn ctype(&self) -> u8 { - const MASK: u16 = 0b01111110 << 8; - ((self.paci_header_fields & MASK) >> (8 + 1)) as u8 - } - - /// phs_size indicates the size of the phes field. - pub fn phs_size(&self) -> u8 { - const MASK: u16 = (0b00000001 << 8) | 0b11110000; - ((self.paci_header_fields & MASK) >> 4) as u8 - } - - /// f0 indicates the presence of a Temporal Scalability support extension in the phes. - pub fn f0(&self) -> bool { - const MASK: u16 = 0b00001000; - (self.paci_header_fields & MASK) != 0 - } - - /// f1 must be zero, reserved for future extensions. - pub fn f1(&self) -> bool { - const MASK: u16 = 0b00000100; - (self.paci_header_fields & MASK) != 0 - } - - /// f2 must be zero, reserved for future extensions. - pub fn f2(&self) -> bool { - const MASK: u16 = 0b00000010; - (self.paci_header_fields & MASK) != 0 - } - - /// y must be zero, reserved for future extensions. - pub fn y(&self) -> bool { - const MASK: u16 = 0b00000001; - (self.paci_header_fields & MASK) != 0 - } - - /// phes contains header extensions. Its size is indicated by phssize. - pub fn phes(&self) -> Bytes { - self.phes.clone() - } - - /// payload is a single NALU or NALU-like struct, not including the first two octets (header). - pub fn payload(&self) -> Bytes { - self.payload.clone() - } - - /// tsci returns the Temporal Scalability Control Information extension, if present. - pub fn tsci(&self) -> Option { - if !self.f0() || self.phs_size() < 3 { - return None; - } - - Some(H265TSCI( - ((self.phes[0] as u32) << 16) | ((self.phes[1] as u32) << 8) | self.phes[0] as u32, - )) - } - - /// depacketize parses the passed byte slice and stores the result in the H265PACIPacket this method is called upon. - fn depacketize(&mut self, payload: &Bytes) -> Result<()> { - const TOTAL_HEADER_SIZE: usize = H265NALU_HEADER_SIZE + 2; - if payload.len() <= TOTAL_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - let payload_header = H265NALUHeader::new(payload[0], payload[1]); - if payload_header.f() { - return Err(Error::ErrH265CorruptedPacket); - } - if !payload_header.is_paci_packet() { - return Err(Error::ErrInvalidH265PacketType); - } - - let paci_header_fields = ((payload[2] as u16) << 8) | (payload[3] as u16); - let mut payload = payload.slice(4..); - - self.paci_header_fields = paci_header_fields; - let header_extension_size = self.phs_size(); - - if payload.len() < header_extension_size as usize + 1 { - self.paci_header_fields = 0; - return Err(Error::ErrShortPacket); - } - - self.payload_header = payload_header; - - if header_extension_size > 0 { - self.phes = payload.slice(..header_extension_size as usize); - } - - payload = payload.slice(header_extension_size as usize..); - self.payload = payload; - - Ok(()) - } -} - -/// -/// Temporal Scalability Control Information -/// - -/// H265TSCI is a Temporal Scalability Control Information header extension. -/// Reference: -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub struct H265TSCI(pub u32); - -impl H265TSCI { - /// tl0picidx see RFC7798 for more details. - pub fn tl0picidx(&self) -> u8 { - const M1: u32 = 0xFFFF0000; - const M2: u32 = 0xFF00; - ((((self.0 & M1) >> 16) & M2) >> 8) as u8 - } - - /// irap_pic_id see RFC7798 for more details. - pub fn irap_pic_id(&self) -> u8 { - const M1: u32 = 0xFFFF0000; - const M2: u32 = 0x00FF; - (((self.0 & M1) >> 16) & M2) as u8 - } - - /// s see RFC7798 for more details. - pub fn s(&self) -> bool { - const M1: u32 = 0xFF00; - const M2: u32 = 0b10000000; - (((self.0 & M1) >> 8) & M2) != 0 - } - - /// e see RFC7798 for more details. - pub fn e(&self) -> bool { - const M1: u32 = 0xFF00; - const M2: u32 = 0b01000000; - (((self.0 & M1) >> 8) & M2) != 0 - } - - /// res see RFC7798 for more details. - pub fn res(&self) -> u8 { - const M1: u32 = 0xFF00; - const M2: u32 = 0b00111111; - (((self.0 & M1) >> 8) & M2) as u8 - } -} - -/// -/// H265 Payload Enum -/// -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum H265Payload { - H265SingleNALUnitPacket(H265SingleNALUnitPacket), - H265FragmentationUnitPacket(H265FragmentationUnitPacket), - H265AggregationPacket(H265AggregationPacket), - H265PACIPacket(H265PACIPacket), -} - -impl Default for H265Payload { - fn default() -> Self { - H265Payload::H265SingleNALUnitPacket(H265SingleNALUnitPacket::default()) - } -} - -/// -/// Packet implementation -/// - -/// H265Packet represents a H265 packet, stored in the payload of an RTP packet. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct H265Packet { - payload: H265Payload, - might_need_donl: bool, -} - -impl H265Packet { - /// with_donl can be called to specify whether or not DONL might be parsed. - /// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream. - pub fn with_donl(&mut self, value: bool) { - self.might_need_donl = value; - } - - /// payload returns the populated payload. - /// Must be casted to one of: - /// - H265SingleNALUnitPacket - /// - H265FragmentationUnitPacket - /// - H265AggregationPacket - /// - H265PACIPacket - pub fn payload(&self) -> &H265Payload { - &self.payload - } -} - -impl Depacketizer for H265Packet { - /// depacketize parses the passed byte slice and stores the result in the H265Packet this method is called upon - fn depacketize(&mut self, payload: &Bytes) -> Result { - if payload.len() <= H265NALU_HEADER_SIZE { - return Err(Error::ErrShortPacket); - } - - let payload_header = H265NALUHeader::new(payload[0], payload[1]); - if payload_header.f() { - return Err(Error::ErrH265CorruptedPacket); - } - - if payload_header.is_paci_packet() { - let mut decoded = H265PACIPacket::default(); - decoded.depacketize(payload)?; - - self.payload = H265Payload::H265PACIPacket(decoded); - } else if payload_header.is_fragmentation_unit() { - let mut decoded = H265FragmentationUnitPacket::default(); - decoded.with_donl(self.might_need_donl); - - decoded.depacketize(payload)?; - - self.payload = H265Payload::H265FragmentationUnitPacket(decoded); - } else if payload_header.is_aggregation_packet() { - let mut decoded = H265AggregationPacket::default(); - decoded.with_donl(self.might_need_donl); - - decoded.depacketize(payload)?; - - self.payload = H265Payload::H265AggregationPacket(decoded); - } else { - let mut decoded = H265SingleNALUnitPacket::default(); - decoded.with_donl(self.might_need_donl); - - decoded.depacketize(payload)?; - - self.payload = H265Payload::H265SingleNALUnitPacket(decoded); - } - - Ok(payload.clone()) - } - - /// is_partition_head checks if this is the head of a packetized nalu stream. - fn is_partition_head(&self, _payload: &Bytes) -> bool { - //TODO: - true - } - - fn is_partition_tail(&self, marker: bool, _payload: &Bytes) -> bool { - marker - } -} diff --git a/rtp/src/codecs/mod.rs b/rtp/src/codecs/mod.rs deleted file mode 100644 index 0296e20c0..000000000 --- a/rtp/src/codecs/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod av1; -pub mod g7xx; -pub mod h264; -pub mod h265; -pub mod opus; -pub mod vp8; -pub mod vp9; diff --git a/rtp/src/codecs/opus/mod.rs b/rtp/src/codecs/opus/mod.rs deleted file mode 100644 index 3ae991e6e..000000000 --- a/rtp/src/codecs/opus/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[cfg(test)] -mod opus_test; - -use bytes::Bytes; - -use crate::error::{Error, Result}; -use crate::packetizer::{Depacketizer, Payloader}; - -#[derive(Default, Debug, Copy, Clone)] -pub struct OpusPayloader; - -impl Payloader for OpusPayloader { - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - Ok(vec![payload.clone()]) - } - - fn clone_to(&self) -> Box { - Box::new(*self) - } -} - -/// OpusPacket represents the Opus header that is stored in the payload of an RTP Packet -#[derive(PartialEq, Eq, Debug, Default, Clone)] -pub struct OpusPacket; - -impl Depacketizer for OpusPacket { - fn depacketize(&mut self, packet: &Bytes) -> Result { - if packet.is_empty() { - Err(Error::ErrShortPacket) - } else { - Ok(packet.clone()) - } - } - - fn is_partition_head(&self, _payload: &Bytes) -> bool { - true - } - - fn is_partition_tail(&self, _marker: bool, _payload: &Bytes) -> bool { - true - } -} diff --git a/rtp/src/codecs/opus/opus_test.rs b/rtp/src/codecs/opus/opus_test.rs deleted file mode 100644 index a3966d0ff..000000000 --- a/rtp/src/codecs/opus/opus_test.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::*; - -#[test] -fn test_opus_unmarshal() -> Result<()> { - let mut pck = OpusPacket; - - // Empty packet - let empty_bytes = Bytes::from_static(&[]); - let result = pck.depacketize(&empty_bytes); - assert!(result.is_err(), "Result should be err in case of error"); - - // Normal packet - let raw_bytes = Bytes::from_static(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x90]); - let payload = pck.depacketize(&raw_bytes)?; - assert_eq!(&raw_bytes, &payload, "Payload must be same"); - - Ok(()) -} - -#[test] -fn test_opus_payload() -> Result<()> { - let mut pck = OpusPayloader; - let empty = Bytes::from_static(&[]); - let payload = Bytes::from_static(&[0x90, 0x90, 0x90]); - - // Positive MTU, empty payload - let result = pck.payload(1, &empty)?; - assert!(result.is_empty(), "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(1, &payload)?; - assert_eq!(result.len(), 1, "Generated payload should be the 1"); - - // Positive MTU, small payload - let result = pck.payload(2, &payload)?; - assert_eq!(result.len(), 1, "Generated payload should be the 1"); - - Ok(()) -} - -#[test] -fn test_opus_is_partition_head() -> Result<()> { - let opus = OpusPacket; - //"NormalPacket" - assert!( - opus.is_partition_head(&Bytes::from_static(&[0x00, 0x00])), - "All OPUS RTP packet should be the head of a new partition" - ); - - Ok(()) -} diff --git a/rtp/src/codecs/vp8/mod.rs b/rtp/src/codecs/vp8/mod.rs deleted file mode 100644 index 20b2d3d04..000000000 --- a/rtp/src/codecs/vp8/mod.rs +++ /dev/null @@ -1,246 +0,0 @@ -#[cfg(test)] -mod vp8_test; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use crate::error::{Error, Result}; -use crate::packetizer::{Depacketizer, Payloader}; - -pub const VP8_HEADER_SIZE: usize = 1; - -/// Vp8Payloader payloads VP8 packets -#[derive(Default, Debug, Copy, Clone)] -pub struct Vp8Payloader { - pub enable_picture_id: bool, - picture_id: u16, -} - -impl Payloader for Vp8Payloader { - /// Payload fragments a VP8 packet across one or more byte arrays - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - /* - * https://tools.ietf.org/html/rfc7741#section-4.2 - * - * 0 1 2 3 4 5 6 7 - * +-+-+-+-+-+-+-+-+ - * |X|R|N|S|R| PID | (REQUIRED) - * +-+-+-+-+-+-+-+-+ - * X: |I|L|T|K| RSV | (OPTIONAL) - * +-+-+-+-+-+-+-+-+ - * I: |M| PictureID | (OPTIONAL) - * +-+-+-+-+-+-+-+-+ - * L: | tl0picidx | (OPTIONAL) - * +-+-+-+-+-+-+-+-+ - * T/K: |tid|Y| KEYIDX | (OPTIONAL) - * +-+-+-+-+-+-+-+-+ - * S: Start of VP8 partition. SHOULD be set to 1 when the first payload - * octet of the RTP packet is the beginning of a new VP8 partition, - * and MUST NOT be 1 otherwise. The S bit MUST be set to 1 for the - * first packet of each encoded frame. - */ - let using_header_size = if self.enable_picture_id { - if self.picture_id == 0 || self.picture_id < 128 { - VP8_HEADER_SIZE + 2 - } else { - VP8_HEADER_SIZE + 3 - } - } else { - VP8_HEADER_SIZE - }; - - let max_fragment_size = mtu as isize - using_header_size as isize; - let mut payload_data_remaining = payload.len() as isize; - let mut payload_data_index: usize = 0; - let mut payloads = vec![]; - - // Make sure the fragment/payload size is correct - if std::cmp::min(max_fragment_size, payload_data_remaining) <= 0 { - return Ok(payloads); - } - - let mut first = true; - while payload_data_remaining > 0 { - let current_fragment_size = - std::cmp::min(max_fragment_size, payload_data_remaining) as usize; - let mut out = BytesMut::with_capacity(using_header_size + current_fragment_size); - let mut buf = [0u8; 4]; - if first { - buf[0] = 0x10; - first = false; - } - - if self.enable_picture_id { - if using_header_size == VP8_HEADER_SIZE + 2 { - buf[0] |= 0x80; - buf[1] |= 0x80; - buf[2] |= (self.picture_id & 0x7F) as u8; - } else if using_header_size == VP8_HEADER_SIZE + 3 { - buf[0] |= 0x80; - buf[1] |= 0x80; - buf[2] |= 0x80 | ((self.picture_id >> 8) & 0x7F) as u8; - buf[3] |= (self.picture_id & 0xFF) as u8; - } - } - - out.put(&buf[..using_header_size]); - - out.put( - &*payload.slice(payload_data_index..payload_data_index + current_fragment_size), - ); - payloads.push(out.freeze()); - - payload_data_remaining -= current_fragment_size as isize; - payload_data_index += current_fragment_size; - } - - self.picture_id += 1; - self.picture_id &= 0x7FFF; - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(*self) - } -} - -/// Vp8Packet represents the VP8 header that is stored in the payload of an RTP Packet -#[derive(PartialEq, Eq, Debug, Default, Clone)] -pub struct Vp8Packet { - /// Required Header - /// extended controlbits present - pub x: u8, - /// when set to 1 this frame can be discarded - pub n: u8, - /// start of VP8 partition - pub s: u8, - /// partition index - pub pid: u8, - - /// Extended control bits - /// 1 if PictureID is present - pub i: u8, - /// 1 if tl0picidx is present - pub l: u8, - /// 1 if tid is present - pub t: u8, - /// 1 if KEYIDX is present - pub k: u8, - - /// Optional extension - /// 8 or 16 bits, picture ID - pub picture_id: u16, - /// 8 bits temporal level zero index - pub tl0_pic_idx: u8, - /// 2 bits temporal layer index - pub tid: u8, - /// 1 bit layer sync bit - pub y: u8, - /// 5 bits temporal key frame index - pub key_idx: u8, -} - -impl Depacketizer for Vp8Packet { - /// depacketize parses the passed byte slice and stores the result in the VP8Packet this method is called upon - fn depacketize(&mut self, packet: &Bytes) -> Result { - let payload_len = packet.len(); - if payload_len < 4 { - return Err(Error::ErrShortPacket); - } - // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - // |X|R|N|S|R| PID | (REQUIRED) |X|R|N|S|R| PID | (REQUIRED) - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - // X: |I|L|T|K| RSV | (OPTIONAL) X: |I|L|T|K| RSV | (OPTIONAL) - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - // I: |M| PictureID | (OPTIONAL) I: |M| PictureID | (OPTIONAL) - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - // L: | tl0picidx | (OPTIONAL) | PictureID | - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - //T/K:|tid|Y| KEYIDX | (OPTIONAL) L: | tl0picidx | (OPTIONAL) - // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ - //T/K:|tid|Y| KEYIDX | (OPTIONAL) - // +-+-+-+-+-+-+-+-+ - - let reader = &mut packet.clone(); - let mut payload_index = 0; - - let mut b = reader.get_u8(); - payload_index += 1; - - self.x = (b & 0x80) >> 7; - self.n = (b & 0x20) >> 5; - self.s = (b & 0x10) >> 4; - self.pid = b & 0x07; - - if self.x == 1 { - b = reader.get_u8(); - payload_index += 1; - self.i = (b & 0x80) >> 7; - self.l = (b & 0x40) >> 6; - self.t = (b & 0x20) >> 5; - self.k = (b & 0x10) >> 4; - } - - if self.i == 1 { - b = reader.get_u8(); - payload_index += 1; - // PID present? - if b & 0x80 > 0 { - // M == 1, PID is 16bit - self.picture_id = (((b & 0x7f) as u16) << 8) | (reader.get_u8() as u16); - payload_index += 1; - } else { - self.picture_id = b as u16; - } - } - - if payload_index >= payload_len { - return Err(Error::ErrShortPacket); - } - - if self.l == 1 { - self.tl0_pic_idx = reader.get_u8(); - payload_index += 1; - } - - if payload_index >= payload_len { - return Err(Error::ErrShortPacket); - } - - if self.t == 1 || self.k == 1 { - let b = reader.get_u8(); - if self.t == 1 { - self.tid = b >> 6; - self.y = (b >> 5) & 0x1; - } - if self.k == 1 { - self.key_idx = b & 0x1F; - } - payload_index += 1; - } - - if payload_index >= packet.len() { - return Err(Error::ErrShortPacket); - } - - Ok(packet.slice(payload_index..)) - } - - /// is_partition_head checks whether if this is a head of the VP8 partition - fn is_partition_head(&self, payload: &Bytes) -> bool { - if payload.is_empty() { - false - } else { - (payload[0] & 0x10) != 0 - } - } - - fn is_partition_tail(&self, marker: bool, _payload: &Bytes) -> bool { - marker - } -} diff --git a/rtp/src/codecs/vp8/vp8_test.rs b/rtp/src/codecs/vp8/vp8_test.rs deleted file mode 100644 index 32fd7343d..000000000 --- a/rtp/src/codecs/vp8/vp8_test.rs +++ /dev/null @@ -1,225 +0,0 @@ -use super::*; - -#[test] -fn test_vp8_unmarshal() -> Result<()> { - let mut pck = Vp8Packet::default(); - - // Empty packet - let empty_bytes = Bytes::from_static(&[]); - let result = pck.depacketize(&empty_bytes); - assert!(result.is_err(), "Result should be err in case of error"); - - // Payload smaller than header size - let small_bytes = Bytes::from_static(&[0x00, 0x11, 0x22]); - let result = pck.depacketize(&small_bytes); - assert!(result.is_err(), "Result should be err in case of error"); - - // Payload smaller than header size - let small_bytes = Bytes::from_static(&[0x00, 0x11]); - let result = pck.depacketize(&small_bytes); - assert!(result.is_err(), "Result should be err in case of error"); - - // Normal packet - let raw_bytes = Bytes::from_static(&[0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x90]); - let payload = pck.depacketize(&raw_bytes).expect("Normal packet"); - assert!(!payload.is_empty(), "Payload must be not empty"); - - // Header size, only X - let raw_bytes = Bytes::from_static(&[0x80, 0x00, 0x00, 0x00]); - let payload = pck.depacketize(&raw_bytes).expect("Only X"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 0, "I must be 0"); - assert_eq!(pck.l, 0, "L must be 0"); - assert_eq!(pck.t, 0, "T must be 0"); - assert_eq!(pck.k, 0, "K must be 0"); - - // Header size, X and I, PID 16bits - let raw_bytes = Bytes::from_static(&[0x80, 0x80, 0x81, 0x00, 0x00]); - let payload = pck.depacketize(&raw_bytes).expect("X and I, PID 16bits"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 1, "I must be 1"); - assert_eq!(pck.l, 0, "L must be 0"); - assert_eq!(pck.t, 0, "T must be 0"); - assert_eq!(pck.k, 0, "K must be 0"); - - // Header size, X and L - let raw_bytes = Bytes::from_static(&[0x80, 0x40, 0x00, 0x00]); - let payload = pck.depacketize(&raw_bytes).expect("X and L"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 0, "I must be 0"); - assert_eq!(pck.l, 1, "L must be 1"); - assert_eq!(pck.t, 0, "T must be 0"); - assert_eq!(pck.k, 0, "K must be 0"); - - // Header size, X and T - let raw_bytes = Bytes::from_static(&[0x80, 0x20, 0x00, 0x00]); - let payload = pck.depacketize(&raw_bytes).expect("X and T"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 0, "I must be 0"); - assert_eq!(pck.l, 0, "L must be 0"); - assert_eq!(pck.t, 1, "T must be 1"); - assert_eq!(pck.k, 0, "K must be 0"); - - // Header size, X and K - let raw_bytes = Bytes::from_static(&[0x80, 0x10, 0x00, 0x00]); - let payload = pck.depacketize(&raw_bytes).expect("X and K"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 0, "I must be 0"); - assert_eq!(pck.l, 0, "L must be 0"); - assert_eq!(pck.t, 0, "T must be 0"); - assert_eq!(pck.k, 1, "K must be 1"); - - // Header size, all flags and 8bit picture_id - let raw_bytes = Bytes::from_static(&[0xff, 0xff, 0x00, 0x00, 0x00, 0x00]); - let payload = pck - .depacketize(&raw_bytes) - .expect("all flags and 8bit picture_id"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 1, "I must be 1"); - assert_eq!(pck.l, 1, "L must be 1"); - assert_eq!(pck.t, 1, "T must be 1"); - assert_eq!(pck.k, 1, "K must be 1"); - - // Header size, all flags and 16bit picture_id - let raw_bytes = Bytes::from_static(&[0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00]); - let payload = pck - .depacketize(&raw_bytes) - .expect("all flags and 16bit picture_id"); - assert!(!payload.is_empty(), "Payload must be not empty"); - assert_eq!(pck.x, 1, "X must be 1"); - assert_eq!(pck.i, 1, "I must be 1"); - assert_eq!(pck.l, 1, "L must be 1"); - assert_eq!(pck.t, 1, "T must be 1"); - assert_eq!(pck.k, 1, "K must be 1"); - - Ok(()) -} - -#[test] -fn test_vp8_payload() -> Result<()> { - let tests = vec![ - ( - "WithoutPictureID", - Vp8Payloader::default(), - 2, - vec![ - Bytes::from_static(&[0x90, 0x90, 0x90]), - Bytes::from_static(&[0x91, 0x91]), - ], - vec![ - vec![ - Bytes::from_static(&[0x10, 0x90]), - Bytes::from_static(&[0x00, 0x90]), - Bytes::from_static(&[0x00, 0x90]), - ], - vec![ - Bytes::from_static(&[0x10, 0x91]), - Bytes::from_static(&[0x00, 0x91]), - ], - ], - ), - ( - "WithPictureID_1byte", - Vp8Payloader { - enable_picture_id: true, - picture_id: 0x20, - }, - 5, - vec![ - Bytes::from_static(&[0x90, 0x90, 0x90]), - Bytes::from_static(&[0x91, 0x91]), - ], - vec![ - vec![ - Bytes::from_static(&[0x90, 0x80, 0x20, 0x90, 0x90]), - Bytes::from_static(&[0x80, 0x80, 0x20, 0x90]), - ], - vec![Bytes::from_static(&[0x90, 0x80, 0x21, 0x91, 0x91])], - ], - ), - ( - "WithPictureID_2bytes", - Vp8Payloader { - enable_picture_id: true, - picture_id: 0x120, - }, - 6, - vec![ - Bytes::from_static(&[0x90, 0x90, 0x90]), - Bytes::from_static(&[0x91, 0x91]), - ], - vec![ - vec![ - Bytes::from_static(&[0x90, 0x80, 0x81, 0x20, 0x90, 0x90]), - Bytes::from_static(&[0x80, 0x80, 0x81, 0x20, 0x90]), - ], - vec![Bytes::from_static(&[0x90, 0x80, 0x81, 0x21, 0x91, 0x91])], - ], - ), - ]; - - for (name, mut pck, mtu, payloads, expected) in tests { - for (i, payload) in payloads.iter().enumerate() { - let actual = pck.payload(mtu, payload)?; - assert_eq!(expected[i], actual, "{name}: Generated packet[{i}] differs"); - } - } - - Ok(()) -} - -#[test] -fn test_vp8_payload_error() -> Result<()> { - let mut pck = Vp8Payloader::default(); - let empty = Bytes::from_static(&[]); - let payload = Bytes::from_static(&[0x90, 0x90, 0x90]); - - // Positive MTU, empty payload - let result = pck.payload(1, &empty)?; - assert!(result.is_empty(), "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(1, &payload)?; - assert_eq!(result.len(), 0, "Generated payload should be empty"); - - // Positive MTU, small payload - let result = pck.payload(2, &payload)?; - assert_eq!( - result.len(), - payload.len(), - "Generated payload should be the same size as original payload size" - ); - - Ok(()) -} - -#[test] -fn test_vp8_partition_head_checker_is_partition_head() -> Result<()> { - let vp8 = Vp8Packet::default(); - - //"SmallPacket" - assert!( - !vp8.is_partition_head(&Bytes::from_static(&[0x00])), - "Small packet should not be the head of a new partition" - ); - - //"SFlagON", - assert!( - vp8.is_partition_head(&Bytes::from_static(&[0x10, 0x00, 0x00, 0x00])), - "Packet with S flag should be the head of a new partition" - ); - - //"SFlagOFF" - assert!( - !vp8.is_partition_head(&Bytes::from_static(&[0x00, 0x00, 0x00, 0x00])), - "Packet without S flag should not be the head of a new partition" - ); - - Ok(()) -} diff --git a/rtp/src/codecs/vp9/mod.rs b/rtp/src/codecs/vp9/mod.rs deleted file mode 100644 index 3a3f130a3..000000000 --- a/rtp/src/codecs/vp9/mod.rs +++ /dev/null @@ -1,467 +0,0 @@ -#[cfg(test)] -mod vp9_test; - -use std::fmt; -use std::sync::Arc; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use crate::error::{Error, Result}; -use crate::packetizer::{Depacketizer, Payloader}; - -/// Flexible mode 15 bit picture ID -const VP9HEADER_SIZE: usize = 3; -const MAX_SPATIAL_LAYERS: u8 = 5; -const MAX_VP9REF_PICS: usize = 3; - -/// InitialPictureIDFn is a function that returns random initial picture ID. -pub type InitialPictureIDFn = Arc u16) + Send + Sync>; - -/// Vp9Payloader payloads VP9 packets -#[derive(Default, Clone)] -pub struct Vp9Payloader { - picture_id: u16, - initialized: bool, - - pub initial_picture_id_fn: Option, -} - -impl fmt::Debug for Vp9Payloader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Vp9Payloader") - .field("picture_id", &self.picture_id) - .field("initialized", &self.initialized) - .finish() - } -} - -impl Payloader for Vp9Payloader { - /// Payload fragments an Vp9Payloader packet across one or more byte arrays - fn payload(&mut self, mtu: usize, payload: &Bytes) -> Result> { - /* - * https://www.ietf.org/id/draft-ietf-payload-vp9-13.txt - * - * Flexible mode (F=1) - * 0 1 2 3 4 5 6 7 - * +-+-+-+-+-+-+-+-+ - * |I|P|L|F|B|E|V|Z| (REQUIRED) - * +-+-+-+-+-+-+-+-+ - * I: |M| PICTURE ID | (REQUIRED) - * +-+-+-+-+-+-+-+-+ - * M: | EXTENDED PID | (RECOMMENDED) - * +-+-+-+-+-+-+-+-+ - * L: | tid |U| SID |D| (CONDITIONALLY RECOMMENDED) - * +-+-+-+-+-+-+-+-+ -\ - * P,F: | P_DIFF |N| (CONDITIONALLY REQUIRED) - up to 3 times - * +-+-+-+-+-+-+-+-+ -/ - * V: | SS | - * | .. | - * +-+-+-+-+-+-+-+-+ - * - * Non-flexible mode (F=0) - * 0 1 2 3 4 5 6 7 - * +-+-+-+-+-+-+-+-+ - * |I|P|L|F|B|E|V|Z| (REQUIRED) - * +-+-+-+-+-+-+-+-+ - * I: |M| PICTURE ID | (RECOMMENDED) - * +-+-+-+-+-+-+-+-+ - * M: | EXTENDED PID | (RECOMMENDED) - * +-+-+-+-+-+-+-+-+ - * L: | tid |U| SID |D| (CONDITIONALLY RECOMMENDED) - * +-+-+-+-+-+-+-+-+ - * | tl0picidx | (CONDITIONALLY REQUIRED) - * +-+-+-+-+-+-+-+-+ - * V: | SS | - * | .. | - * +-+-+-+-+-+-+-+-+ - */ - - if payload.is_empty() || mtu == 0 { - return Ok(vec![]); - } - - if !self.initialized { - if self.initial_picture_id_fn.is_none() { - self.initial_picture_id_fn = - Some(Arc::new(|| -> u16 { rand::random::() & 0x7FFF })); - } - self.picture_id = if let Some(f) = &self.initial_picture_id_fn { - f() - } else { - 0 - }; - self.initialized = true; - } - - let max_fragment_size = mtu as isize - VP9HEADER_SIZE as isize; - let mut payloads = vec![]; - let mut payload_data_remaining = payload.len(); - let mut payload_data_index = 0; - - if std::cmp::min(max_fragment_size, payload_data_remaining as isize) <= 0 { - return Ok(vec![]); - } - - while payload_data_remaining > 0 { - let current_fragment_size = - std::cmp::min(max_fragment_size as usize, payload_data_remaining); - let mut out = BytesMut::with_capacity(VP9HEADER_SIZE + current_fragment_size); - let mut buf = [0u8; VP9HEADER_SIZE]; - buf[0] = 0x90; // F=1 I=1 - if payload_data_index == 0 { - buf[0] |= 0x08; // B=1 - } - if payload_data_remaining == current_fragment_size { - buf[0] |= 0x04; // E=1 - } - buf[1] = (self.picture_id >> 8) as u8 | 0x80; - buf[2] = (self.picture_id & 0xFF) as u8; - - out.put(&buf[..]); - - out.put( - &*payload.slice(payload_data_index..payload_data_index + current_fragment_size), - ); - - payloads.push(out.freeze()); - - payload_data_remaining -= current_fragment_size; - payload_data_index += current_fragment_size; - } - - self.picture_id += 1; - self.picture_id &= 0x7FFF; - - Ok(payloads) - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} - -/// Vp9Packet represents the VP9 header that is stored in the payload of an RTP Packet -#[derive(PartialEq, Eq, Debug, Default, Clone)] -pub struct Vp9Packet { - /// picture ID is present - pub i: bool, - /// inter-picture predicted frame. - pub p: bool, - /// layer indices present - pub l: bool, - /// flexible mode - pub f: bool, - /// start of frame. beginning of new vp9 frame - pub b: bool, - /// end of frame - pub e: bool, - /// scalability structure (SS) present - pub v: bool, - /// Not a reference frame for upper spatial layers - pub z: bool, - - /// Recommended headers - /// 7 or 16 bits, picture ID. - pub picture_id: u16, - - /// Conditionally recommended headers - /// Temporal layer ID - pub tid: u8, - /// Switching up point - pub u: bool, - /// Spatial layer ID - pub sid: u8, - /// Inter-layer dependency used - pub d: bool, - - /// Conditionally required headers - /// Reference index (F=1) - pub pdiff: Vec, - /// Temporal layer zero index (F=0) - pub tl0picidx: u8, - - /// Scalability structure headers - /// N_S + 1 indicates the number of spatial layers present in the VP9 stream - pub ns: u8, - /// Each spatial layer's frame resolution present - pub y: bool, - /// PG description present flag. - pub g: bool, - /// N_G indicates the number of pictures in a Picture Group (PG) - pub ng: u8, - pub width: Vec, - pub height: Vec, - /// Temporal layer ID of pictures in a Picture Group - pub pgtid: Vec, - /// Switching up point of pictures in a Picture Group - pub pgu: Vec, - /// Reference indices of pictures in a Picture Group - pub pgpdiff: Vec>, -} - -impl Depacketizer for Vp9Packet { - /// depacketize parses the passed byte slice and stores the result in the Vp9Packet this method is called upon - fn depacketize(&mut self, packet: &Bytes) -> Result { - if packet.is_empty() { - return Err(Error::ErrShortPacket); - } - - let reader = &mut packet.clone(); - let b = reader.get_u8(); - - self.i = (b & 0x80) != 0; - self.p = (b & 0x40) != 0; - self.l = (b & 0x20) != 0; - self.f = (b & 0x10) != 0; - self.b = (b & 0x08) != 0; - self.e = (b & 0x04) != 0; - self.v = (b & 0x02) != 0; - self.z = (b & 0x01) != 0; - - let mut payload_index = 1; - - if self.i { - payload_index = self.parse_picture_id(reader, payload_index)?; - } - - if self.l { - payload_index = self.parse_layer_info(reader, payload_index)?; - } - - if self.f && self.p { - payload_index = self.parse_ref_indices(reader, payload_index)?; - } - - if self.v { - payload_index = self.parse_ssdata(reader, payload_index)?; - } - - Ok(packet.slice(payload_index..)) - } - - /// is_partition_head checks whether if this is a head of the VP9 partition - fn is_partition_head(&self, payload: &Bytes) -> bool { - if payload.is_empty() { - false - } else { - (payload[0] & 0x08) != 0 - } - } - - fn is_partition_tail(&self, marker: bool, _payload: &Bytes) -> bool { - marker - } -} - -impl Vp9Packet { - // Picture ID: - // - // +-+-+-+-+-+-+-+-+ - // I: |M| PICTURE ID | M:0 => picture id is 7 bits. - // +-+-+-+-+-+-+-+-+ M:1 => picture id is 15 bits. - // M: | EXTENDED PID | - // +-+-+-+-+-+-+-+-+ - // - fn parse_picture_id( - &mut self, - reader: &mut dyn Buf, - mut payload_index: usize, - ) -> Result { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - let b = reader.get_u8(); - payload_index += 1; - // PID present? - if (b & 0x80) != 0 { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - // M == 1, PID is 15bit - self.picture_id = (((b & 0x7f) as u16) << 8) | (reader.get_u8() as u16); - payload_index += 1; - } else { - self.picture_id = (b & 0x7F) as u16; - } - - Ok(payload_index) - } - - fn parse_layer_info( - &mut self, - reader: &mut dyn Buf, - mut payload_index: usize, - ) -> Result { - payload_index = self.parse_layer_info_common(reader, payload_index)?; - - if self.f { - Ok(payload_index) - } else { - self.parse_layer_info_non_flexible_mode(reader, payload_index) - } - } - - // Layer indices (flexible mode): - // - // +-+-+-+-+-+-+-+-+ - // L: | T |U| S |D| - // +-+-+-+-+-+-+-+-+ - // - fn parse_layer_info_common( - &mut self, - reader: &mut dyn Buf, - mut payload_index: usize, - ) -> Result { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - let b = reader.get_u8(); - payload_index += 1; - - self.tid = b >> 5; - self.u = b & 0x10 != 0; - self.sid = (b >> 1) & 0x7; - self.d = b & 0x01 != 0; - - if self.sid >= MAX_SPATIAL_LAYERS { - Err(Error::ErrTooManySpatialLayers) - } else { - Ok(payload_index) - } - } - - // Layer indices (non-flexible mode): - // - // +-+-+-+-+-+-+-+-+ - // L: | T |U| S |D| - // +-+-+-+-+-+-+-+-+ - // | tl0picidx | - // +-+-+-+-+-+-+-+-+ - // - fn parse_layer_info_non_flexible_mode( - &mut self, - reader: &mut dyn Buf, - mut payload_index: usize, - ) -> Result { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - self.tl0picidx = reader.get_u8(); - payload_index += 1; - Ok(payload_index) - } - - // Reference indices: - // - // +-+-+-+-+-+-+-+-+ P=1,F=1: At least one reference index - // P,F: | P_DIFF |N| up to 3 times has to be specified. - // +-+-+-+-+-+-+-+-+ N=1: An additional P_DIFF follows - // current P_DIFF. - // - fn parse_ref_indices( - &mut self, - reader: &mut dyn Buf, - mut payload_index: usize, - ) -> Result { - let mut b = 1u8; - while (b & 0x1) != 0 { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - b = reader.get_u8(); - payload_index += 1; - - self.pdiff.push(b >> 1); - if self.pdiff.len() >= MAX_VP9REF_PICS { - return Err(Error::ErrTooManyPDiff); - } - } - - Ok(payload_index) - } - - // Scalability structure (SS): - // - // +-+-+-+-+-+-+-+-+ - // V: | N_S |Y|G|-|-|-| - // +-+-+-+-+-+-+-+-+ -| - // Y: | WIDTH | (OPTIONAL) . - // + + . - // | | (OPTIONAL) . - // +-+-+-+-+-+-+-+-+ . N_S + 1 times - // | HEIGHT | (OPTIONAL) . - // + + . - // | | (OPTIONAL) . - // +-+-+-+-+-+-+-+-+ -| - // G: | N_G | (OPTIONAL) - // +-+-+-+-+-+-+-+-+ -| - // N_G: | T |U| R |-|-| (OPTIONAL) . - // +-+-+-+-+-+-+-+-+ -| . N_G times - // | P_DIFF | (OPTIONAL) . R times . - // +-+-+-+-+-+-+-+-+ -| -| - // - fn parse_ssdata(&mut self, reader: &mut dyn Buf, mut payload_index: usize) -> Result { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - - let b = reader.get_u8(); - payload_index += 1; - - self.ns = b >> 5; - self.y = b & 0x10 != 0; - self.g = (b >> 1) & 0x7 != 0; - - let ns = (self.ns + 1) as usize; - self.ng = 0; - - if self.y { - if reader.remaining() < 4 * ns { - return Err(Error::ErrShortPacket); - } - - self.width = vec![0u16; ns]; - self.height = vec![0u16; ns]; - for i in 0..ns { - self.width[i] = reader.get_u16(); - self.height[i] = reader.get_u16(); - } - payload_index += 4 * ns; - } - - if self.g { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - - self.ng = reader.get_u8(); - payload_index += 1; - } - - for i in 0..self.ng as usize { - if reader.remaining() == 0 { - return Err(Error::ErrShortPacket); - } - let b = reader.get_u8(); - payload_index += 1; - - self.pgtid.push(b >> 5); - self.pgu.push(b & 0x10 != 0); - - let r = ((b >> 2) & 0x3) as usize; - if reader.remaining() < r { - return Err(Error::ErrShortPacket); - } - - self.pgpdiff.push(vec![]); - for _ in 0..r { - let b = reader.get_u8(); - payload_index += 1; - - self.pgpdiff[i].push(b); - } - } - - Ok(payload_index) - } -} diff --git a/rtp/src/codecs/vp9/vp9_test.rs b/rtp/src/codecs/vp9/vp9_test.rs deleted file mode 100644 index 596d423ba..000000000 --- a/rtp/src/codecs/vp9/vp9_test.rs +++ /dev/null @@ -1,364 +0,0 @@ -use super::*; - -#[test] -fn test_vp9_packet_unmarshal() -> Result<()> { - let tests = vec![ - ( - "Empty", - Bytes::from_static(&[]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "NonFlexible", - Bytes::from_static(&[0x00, 0xAA]), - Vp9Packet::default(), - Bytes::from_static(&[0xAA]), - None, - ), - ( - "NonFlexiblePictureID", - Bytes::from_static(&[0x80, 0x02, 0xAA]), - Vp9Packet { - i: true, - picture_id: 0x02, - ..Default::default() - }, - Bytes::from_static(&[0xAA]), - None, - ), - ( - "NonFlexiblePictureIDExt", - Bytes::from_static(&[0x80, 0x81, 0xFF, 0xAA]), - Vp9Packet { - i: true, - picture_id: 0x01FF, - ..Default::default() - }, - Bytes::from_static(&[0xAA]), - None, - ), - ( - "NonFlexiblePictureIDExt_ShortPacket0", - Bytes::from_static(&[0x80, 0x81]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "NonFlexiblePictureIDExt_ShortPacket1", - Bytes::from_static(&[0x80]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "NonFlexibleLayerIndicePictureID", - Bytes::from_static(&[0xA0, 0x02, 0x23, 0x01, 0xAA]), - Vp9Packet { - i: true, - l: true, - picture_id: 0x02, - tid: 0x01, - sid: 0x01, - d: true, - tl0picidx: 0x01, - ..Default::default() - }, - Bytes::from_static(&[0xAA]), - None, - ), - ( - "FlexibleLayerIndicePictureID", - Bytes::from_static(&[0xB0, 0x02, 0x23, 0x01, 0xAA]), - Vp9Packet { - f: true, - i: true, - l: true, - picture_id: 0x02, - tid: 0x01, - sid: 0x01, - d: true, - ..Default::default() - }, - Bytes::from_static(&[0x01, 0xAA]), - None, - ), - ( - "NonFlexibleLayerIndicePictureID_ShortPacket0", - Bytes::from_static(&[0xA0, 0x02, 0x23]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "NonFlexibleLayerIndicePictureID_ShortPacket1", - Bytes::from_static(&[0xA0, 0x02]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "FlexiblePictureIDRefIndex", - Bytes::from_static(&[0xD0, 0x02, 0x03, 0x04, 0xAA]), - Vp9Packet { - i: true, - p: true, - f: true, - picture_id: 0x02, - pdiff: vec![0x01, 0x02], - ..Default::default() - }, - Bytes::from_static(&[0xAA]), - None, - ), - ( - "FlexiblePictureIDRefIndex_TooManyPDiff", - Bytes::from_static(&[0xD0, 0x02, 0x03, 0x05, 0x07, 0x09, 0x10, 0xAA]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrTooManyPDiff), - ), - ( - "FlexiblePictureIDRefIndexNoPayload", - Bytes::from_static(&[0xD0, 0x02, 0x03, 0x04]), - Vp9Packet { - i: true, - p: true, - f: true, - picture_id: 0x02, - pdiff: vec![0x01, 0x02], - ..Default::default() - }, - Bytes::from_static(&[]), - None, - ), - ( - "FlexiblePictureIDRefIndex_ShortPacket0", - Bytes::from_static(&[0xD0, 0x02, 0x03]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "FlexiblePictureIDRefIndex_ShortPacket1", - Bytes::from_static(&[0xD0, 0x02]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "FlexiblePictureIDRefIndex_ShortPacket2", - Bytes::from_static(&[0xD0]), - Vp9Packet::default(), - Bytes::new(), - Some(Error::ErrShortPacket), - ), - ( - "ScalabilityStructureResolutionsNoPayload", - Bytes::from_static(&[ - 0x0A, - (1 << 5) | (1 << 4), // NS:1 Y:1 G:0 - (640 >> 8) as u8, - (640 & 0xff) as u8, - (360 >> 8) as u8, - (360 & 0xff) as u8, - (1280 >> 8) as u8, - (1280 & 0xff) as u8, - (720 >> 8) as u8, - (720 & 0xff) as u8, - ]), - Vp9Packet { - b: true, - v: true, - ns: 1, - y: true, - g: false, - ng: 0, - width: vec![640, 1280], - height: vec![360, 720], - ..Default::default() - }, - Bytes::new(), - None, - ), - ( - "ScalabilityStructureNoPayload", - Bytes::from_static(&[ - 0x0A, - (1 << 5) | (1 << 3), // NS:1 Y:0 G:1 - 2, - (1 << 4), // T:0 U:1 R:0 - - (2 << 5) | (1 << 2), // T:2 U:0 R:1 - - 33, - ]), - Vp9Packet { - b: true, - v: true, - ns: 1, - y: false, - g: true, - ng: 2, - pgtid: vec![0, 2], - pgu: vec![true, false], - pgpdiff: vec![vec![], vec![33]], - ..Default::default() - }, - Bytes::new(), - None, - ), - ]; - - for (name, b, pkt, expected, err) in tests { - let mut p = Vp9Packet::default(); - - if let Some(expected) = err { - if let Err(actual) = p.depacketize(&b) { - assert_eq!( - expected, actual, - "{name}: expected {expected}, but got {actual}" - ); - } else { - panic!("{name}: expected error, but got passed"); - } - } else { - let payload = p.depacketize(&b)?; - assert_eq!(pkt, p, "{name}: expected {pkt:?}, but got {p:?}"); - assert_eq!(payload, expected); - } - } - - Ok(()) -} - -#[test] -fn test_vp9_payloader_payload() -> Result<()> { - let mut r0 = 8692; - let mut rands = vec![]; - for _ in 0..10 { - rands.push(vec![(r0 >> 8) as u8 | 0x80, (r0 & 0xFF) as u8]); - r0 += 1; - } - - let tests = vec![ - ("NilPayload", vec![Bytes::new()], 100, vec![]), - ("SmallMTU", vec![Bytes::from(vec![0x00, 0x00])], 1, vec![]), - ( - "NegativeMTU", - vec![Bytes::from(vec![0x00, 0x00])], - 0, - vec![], - ), - ( - "OnePacket", - vec![Bytes::from(vec![0x01, 0x02])], - 10, - vec![Bytes::from(vec![ - 0x9C, - rands[0][0], - rands[0][1], - 0x01, - 0x02, - ])], - ), - ( - "TwoPackets", - vec![Bytes::from(vec![0x01, 0x02])], - 4, - vec![ - Bytes::from(vec![0x98, rands[0][0], rands[0][1], 0x01]), - Bytes::from(vec![0x94, rands[0][0], rands[0][1], 0x02]), - ], - ), - ( - "ThreePackets", - vec![Bytes::from(vec![0x01, 0x02, 0x03])], - 4, - vec![ - Bytes::from(vec![0x98, rands[0][0], rands[0][1], 0x01]), - Bytes::from(vec![0x90, rands[0][0], rands[0][1], 0x02]), - Bytes::from(vec![0x94, rands[0][0], rands[0][1], 0x03]), - ], - ), - ( - "TwoFramesFourPackets", - vec![Bytes::from(vec![0x01, 0x02, 0x03]), Bytes::from(vec![0x04])], - 5, - vec![ - Bytes::from(vec![0x98, rands[0][0], rands[0][1], 0x01, 0x02]), - Bytes::from(vec![0x94, rands[0][0], rands[0][1], 0x03]), - Bytes::from(vec![0x9C, rands[1][0], rands[1][1], 0x04]), - ], - ), - ]; - - for (name, bs, mtu, expected) in tests { - let mut pck = Vp9Payloader { - initial_picture_id_fn: Some(Arc::new(|| -> u16 { 8692 })), - ..Default::default() - }; - - let mut actual = vec![]; - for b in &bs { - actual.extend(pck.payload(mtu, b)?); - } - assert_eq!(actual, expected, "{name}: Payloaded packet"); - } - - //"PictureIDOverflow" - { - let mut pck = Vp9Payloader { - initial_picture_id_fn: Some(Arc::new(|| -> u16 { 8692 })), - ..Default::default() - }; - let mut p_prev = Vp9Packet::default(); - for i in 0..0x8000 { - let res = pck.payload(4, &Bytes::from_static(&[0x01]))?; - let mut p = Vp9Packet::default(); - p.depacketize(&res[0])?; - - if i > 0 { - if p_prev.picture_id == 0x7FFF { - assert_eq!( - p.picture_id, 0, - "Picture ID next to 0x7FFF must be 0, got {}", - p.picture_id - ); - } else if p_prev.picture_id + 1 != p.picture_id { - panic!( - "Picture ID next must be incremented by 1: {} -> {}", - p_prev.picture_id, p.picture_id, - ); - } - } - - p_prev = p; - } - } - - Ok(()) -} - -#[test] -fn test_vp9_partition_head_checker_is_partition_head() -> Result<()> { - let vp9 = Vp9Packet::default(); - - //"SmallPacket" - assert!( - !vp9.is_partition_head(&Bytes::new()), - "Small packet should not be the head of a new partition" - ); - - //"NormalPacket" - assert!( - vp9.is_partition_head(&Bytes::from_static(&[0x18, 0x00, 0x00])), - "VP9 RTP packet with B flag should be head of a new partition" - ); - assert!( - !vp9.is_partition_head(&Bytes::from_static(&[0x10, 0x00, 0x00])), - "VP9 RTP packet without B flag should not be head of a new partition" - ); - - Ok(()) -} diff --git a/rtp/src/error.rs b/rtp/src/error.rs deleted file mode 100644 index 72612f744..000000000 --- a/rtp/src/error.rs +++ /dev/null @@ -1,86 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("RTP header size insufficient")] - ErrHeaderSizeInsufficient, - #[error("RTP header size insufficient for extension")] - ErrHeaderSizeInsufficientForExtension, - #[error("buffer too small")] - ErrBufferTooSmall, - #[error("extension not enabled")] - ErrHeaderExtensionsNotEnabled, - #[error("extension not found")] - ErrHeaderExtensionNotFound, - - #[error("header extension id must be between 1 and 14 for RFC 5285 extensions")] - ErrRfc8285oneByteHeaderIdrange, - #[error("header extension payload must be 16bytes or less for RFC 5285 one byte extensions")] - ErrRfc8285oneByteHeaderSize, - - #[error("header extension id must be between 1 and 255 for RFC 5285 extensions")] - ErrRfc8285twoByteHeaderIdrange, - #[error("header extension payload must be 255bytes or less for RFC 5285 two byte extensions")] - ErrRfc8285twoByteHeaderSize, - - #[error("header extension id must be 0 for none RFC 5285 extensions")] - ErrRfc3550headerIdrange, - - #[error("packet is not large enough")] - ErrShortPacket, - #[error("invalid nil packet")] - ErrNilPacket, - #[error("too many PDiff")] - ErrTooManyPDiff, - #[error("too many spatial layers")] - ErrTooManySpatialLayers, - #[error("NALU Type is unhandled")] - ErrUnhandledNaluType, - - #[error("corrupted h265 packet")] - ErrH265CorruptedPacket, - #[error("invalid h265 packet type")] - ErrInvalidH265PacketType, - - #[error("payload is too small for OBU extension header")] - ErrPayloadTooSmallForObuExtensionHeader, - #[error("payload is too small for OBU payload size")] - ErrPayloadTooSmallForObuPayloadSize, - - #[error("extension_payload must be in 32-bit words")] - HeaderExtensionPayloadNot32BitWords, - #[error("audio level overflow")] - AudioLevelOverflow, - #[error("playout delay overflow")] - PlayoutDelayOverflow, - #[error("payload is not large enough")] - PayloadIsNotLargeEnough, - #[error("STAP-A declared size({0}) is larger than buffer({1})")] - StapASizeLargerThanBuffer(usize, usize), - #[error("nalu type {0} is currently not handled")] - NaluTypeIsNotHandled(u8), - #[error("{0}")] - Util(#[from] util::Error), - - #[error("{0}")] - Other(String), -} - -impl From for util::Error { - fn from(e: Error) -> Self { - util::Error::from_std(e) - } -} - -impl PartialEq for Error { - fn eq(&self, other: &util::Error) -> bool { - if let Some(down) = other.downcast_ref::() { - self == down - } else { - false - } - } -} diff --git a/rtp/src/extension/abs_send_time_extension/abs_send_time_extension_test.rs b/rtp/src/extension/abs_send_time_extension/abs_send_time_extension_test.rs deleted file mode 100644 index 152ee5f95..000000000 --- a/rtp/src/extension/abs_send_time_extension/abs_send_time_extension_test.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::time::Duration; - -use bytes::BytesMut; -use chrono::prelude::*; - -use super::*; -use crate::error::Result; - -const ABS_SEND_TIME_RESOLUTION: i128 = 1000; - -#[test] -fn test_ntp_conversion() -> Result<()> { - let loc = FixedOffset::west_opt(5 * 60 * 60).unwrap(); // UTC-5 - let tests = vec![ - ( - loc.with_ymd_and_hms(1985, 6, 23, 4, 0, 0).unwrap(), - 0xa0c65b1000000000_u64, - ), - ( - // TODO: fix this. MA: There's only so long I will stare at - // APIs that sacrifice convenience for correctness. - #[allow(deprecated)] - loc.ymd(1999, 12, 31) - .and_hms_nano_opt(23, 59, 59, 500000) - .unwrap(), - 0xbc18084f0020c49b_u64, - ), - ( - #[allow(deprecated)] - loc.ymd(2019, 3, 27) - .and_hms_nano_opt(13, 39, 30, 8675309) - .unwrap(), - 0xe04641e202388b88_u64, - ), - ]; - - for (t, n) in &tests { - let st = UNIX_EPOCH - .checked_add(Duration::from_nanos(t.timestamp_nanos_opt().unwrap() as u64)) - .unwrap_or(UNIX_EPOCH); - let ntp = unix2ntp(st); - - if cfg!(target_os = "windows") { - let actual = ntp as i128; - let expected = *n as i128; - let diff = actual - expected; - if !(-ABS_SEND_TIME_RESOLUTION..=ABS_SEND_TIME_RESOLUTION).contains(&diff) { - panic!("unix2ntp error, expected: {:?}, got: {:?}", ntp, *n,); - } - } else { - assert_eq!(ntp, *n, "unix2ntp error"); - } - } - - for (t, n) in &tests { - let output = ntp2unix(*n); - let input = UNIX_EPOCH - .checked_add(Duration::from_nanos(t.timestamp_nanos_opt().unwrap() as u64)) - .unwrap_or(UNIX_EPOCH); - let diff = input.duration_since(output).unwrap().as_nanos() as i128; - if !(-ABS_SEND_TIME_RESOLUTION..=ABS_SEND_TIME_RESOLUTION).contains(&diff) { - panic!( - "Converted time.Time from NTP time differs, expected: {input:?}, got: {output:?}", - ); - } - } - - Ok(()) -} - -#[test] -fn test_abs_send_time_extension_roundtrip() -> Result<()> { - let tests = vec![ - AbsSendTimeExtension { timestamp: 123456 }, - AbsSendTimeExtension { timestamp: 654321 }, - ]; - - for test in &tests { - let mut raw = BytesMut::with_capacity(test.marshal_size()); - raw.resize(test.marshal_size(), 0); - test.marshal_to(&mut raw)?; - let raw = raw.freeze(); - let buf = &mut raw.clone(); - let out = AbsSendTimeExtension::unmarshal(buf)?; - assert_eq!(test.timestamp, out.timestamp); - } - - Ok(()) -} - -#[test] -fn test_abs_send_time_extension_estimate() -> Result<()> { - let tests = vec![ - //FFFFFFC000000000 mask of second - (0xa0c65b1000100000, 0xa0c65b1001000000), // not carried - (0xa0c65b3f00000000, 0xa0c65b4001000000), // carried during transmission - ]; - - for (send_ntp, receive_ntp) in tests { - let in_time = ntp2unix(send_ntp); - let send = AbsSendTimeExtension { - timestamp: send_ntp >> 14, - }; - let mut raw = BytesMut::with_capacity(send.marshal_size()); - raw.resize(send.marshal_size(), 0); - send.marshal_to(&mut raw)?; - let raw = raw.freeze(); - let buf = &mut raw.clone(); - let receive = AbsSendTimeExtension::unmarshal(buf)?; - - let estimated = receive.estimate(ntp2unix(receive_ntp)); - let diff = estimated.duration_since(in_time).unwrap().as_nanos() as i128; - if !(-ABS_SEND_TIME_RESOLUTION..=ABS_SEND_TIME_RESOLUTION).contains(&diff) { - panic!( - "Converted time.Time from NTP time differs, expected: {in_time:?}, got: {estimated:?}", - ); - } - } - - Ok(()) -} diff --git a/rtp/src/extension/abs_send_time_extension/mod.rs b/rtp/src/extension/abs_send_time_extension/mod.rs deleted file mode 100644 index 33000b47a..000000000 --- a/rtp/src/extension/abs_send_time_extension/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -#[cfg(test)] -mod abs_send_time_extension_test; - -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use bytes::{Buf, BufMut}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -pub const ABS_SEND_TIME_EXTENSION_SIZE: usize = 3; - -/// AbsSendTimeExtension is a extension payload format in -/// http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] -pub struct AbsSendTimeExtension { - pub timestamp: u64, -} - -impl Unmarshal for AbsSendTimeExtension { - /// Unmarshal parses the passed byte slice and stores the result in the members. - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < ABS_SEND_TIME_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - - let b0 = raw_packet.get_u8(); - let b1 = raw_packet.get_u8(); - let b2 = raw_packet.get_u8(); - let timestamp = (b0 as u64) << 16 | (b1 as u64) << 8 | b2 as u64; - - Ok(AbsSendTimeExtension { timestamp }) - } -} - -impl MarshalSize for AbsSendTimeExtension { - /// MarshalSize returns the size of the AbsSendTimeExtension once marshaled. - fn marshal_size(&self) -> usize { - ABS_SEND_TIME_EXTENSION_SIZE - } -} - -impl Marshal for AbsSendTimeExtension { - /// MarshalTo serializes the members to buffer. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < ABS_SEND_TIME_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - - buf.put_u8(((self.timestamp & 0xFF0000) >> 16) as u8); - buf.put_u8(((self.timestamp & 0xFF00) >> 8) as u8); - buf.put_u8((self.timestamp & 0xFF) as u8); - - Ok(ABS_SEND_TIME_EXTENSION_SIZE) - } -} - -impl AbsSendTimeExtension { - /// Estimate absolute send time according to the receive time. - /// Note that if the transmission delay is larger than 64 seconds, estimated time will be wrong. - pub fn estimate(&self, receive: SystemTime) -> SystemTime { - let receive_ntp = unix2ntp(receive); - let mut ntp = receive_ntp & 0xFFFFFFC000000000 | (self.timestamp & 0xFFFFFF) << 14; - if receive_ntp < ntp { - // Receive time must be always later than send time - ntp -= 0x1000000 << 14; - } - - ntp2unix(ntp) - } - - /// NewAbsSendTimeExtension makes new AbsSendTimeExtension from time.Time. - pub fn new(send_time: SystemTime) -> Self { - AbsSendTimeExtension { - timestamp: unix2ntp(send_time) >> 14, - } - } -} - -pub fn unix2ntp(st: SystemTime) -> u64 { - let u = st - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_nanos() as u64; - let mut s = u / 1_000_000_000; - s += 0x83AA7E80; //offset in seconds between unix epoch and ntp epoch - let mut f = u % 1_000_000_000; - f <<= 32; - f /= 1_000_000_000; - s <<= 32; - - s | f -} - -pub fn ntp2unix(t: u64) -> SystemTime { - let mut s = t >> 32; - let mut f = t & 0xFFFFFFFF; - f *= 1_000_000_000; - f >>= 32; - s -= 0x83AA7E80; - let u = s * 1_000_000_000 + f; - - UNIX_EPOCH - .checked_add(Duration::new(u / 1_000_000_000, (u % 1_000_000_000) as u32)) - .unwrap_or(UNIX_EPOCH) -} diff --git a/rtp/src/extension/audio_level_extension/audio_level_extension_test.rs b/rtp/src/extension/audio_level_extension/audio_level_extension_test.rs deleted file mode 100644 index 89814d763..000000000 --- a/rtp/src/extension/audio_level_extension/audio_level_extension_test.rs +++ /dev/null @@ -1,66 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_audio_level_extension_too_small() -> Result<()> { - let mut buf = &vec![0u8; 0][..]; - let result = AudioLevelExtension::unmarshal(&mut buf); - assert!(result.is_err()); - - Ok(()) -} - -#[test] -fn test_audio_level_extension_voice_true() -> Result<()> { - let raw = Bytes::from_static(&[0x88]); - let buf = &mut raw.clone(); - let a1 = AudioLevelExtension::unmarshal(buf)?; - let a2 = AudioLevelExtension { - level: 8, - voice: true, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_audio_level_extension_voice_false() -> Result<()> { - let raw = Bytes::from_static(&[0x8]); - let buf = &mut raw.clone(); - let a1 = AudioLevelExtension::unmarshal(buf)?; - let a2 = AudioLevelExtension { - level: 8, - voice: false, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_audio_level_extension_level_overflow() -> Result<()> { - let a = AudioLevelExtension { - level: 128, - voice: false, - }; - - let mut dst = BytesMut::with_capacity(a.marshal_size()); - dst.resize(a.marshal_size(), 0); - let result = a.marshal_to(&mut dst); - assert!(result.is_err()); - - Ok(()) -} diff --git a/rtp/src/extension/audio_level_extension/mod.rs b/rtp/src/extension/audio_level_extension/mod.rs deleted file mode 100644 index 2dce2be10..000000000 --- a/rtp/src/extension/audio_level_extension/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -#[cfg(test)] -mod audio_level_extension_test; - -use bytes::{Buf, BufMut}; -use serde::{Deserialize, Serialize}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -// AUDIO_LEVEL_EXTENSION_SIZE One byte header size -pub const AUDIO_LEVEL_EXTENSION_SIZE: usize = 1; - -/// AudioLevelExtension is a extension payload format described in -/// https://tools.ietf.org/html/rfc6464 -/// -/// Implementation based on: -/// https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#49 -/// -/// One byte format: -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ID | len=0 |V| level | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Two byte format: -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ID | len=1 |V| level | 0 (pad) | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] -pub struct AudioLevelExtension { - pub level: u8, - pub voice: bool, -} - -impl Unmarshal for AudioLevelExtension { - /// Unmarshal parses the passed byte slice and stores the result in the members - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < AUDIO_LEVEL_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - - let b = raw_packet.get_u8(); - - Ok(AudioLevelExtension { - level: b & 0x7F, - voice: (b & 0x80) != 0, - }) - } -} - -impl MarshalSize for AudioLevelExtension { - /// MarshalSize returns the size of the AudioLevelExtension once marshaled. - fn marshal_size(&self) -> usize { - AUDIO_LEVEL_EXTENSION_SIZE - } -} - -impl Marshal for AudioLevelExtension { - /// MarshalTo serializes the members to buffer - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < AUDIO_LEVEL_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - if self.level > 127 { - return Err(Error::AudioLevelOverflow.into()); - } - let voice = if self.voice { 0x80u8 } else { 0u8 }; - - buf.put_u8(voice | self.level); - - Ok(AUDIO_LEVEL_EXTENSION_SIZE) - } -} diff --git a/rtp/src/extension/mod.rs b/rtp/src/extension/mod.rs deleted file mode 100644 index 6e2817409..000000000 --- a/rtp/src/extension/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::borrow::Cow; -use std::fmt; - -use util::{Marshal, MarshalSize}; - -pub mod abs_send_time_extension; -pub mod audio_level_extension; -pub mod playout_delay_extension; -pub mod transport_cc_extension; -pub mod video_orientation_extension; - -/// A generic RTP header extension. -pub enum HeaderExtension { - AbsSendTime(abs_send_time_extension::AbsSendTimeExtension), - AudioLevel(audio_level_extension::AudioLevelExtension), - PlayoutDelay(playout_delay_extension::PlayoutDelayExtension), - TransportCc(transport_cc_extension::TransportCcExtension), - VideoOrientation(video_orientation_extension::VideoOrientationExtension), - - /// A custom extension - Custom { - uri: Cow<'static, str>, - extension: Box, - }, -} - -impl HeaderExtension { - pub fn uri(&self) -> Cow<'static, str> { - use HeaderExtension::*; - - match self { - AbsSendTime(_) => "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time".into(), - AudioLevel(_) => "urn:ietf:params:rtp-hdrext:ssrc-audio-level".into(), - PlayoutDelay(_) => "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay".into(), - TransportCc(_) => { - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".into() - } - VideoOrientation(_) => "urn:3gpp:video-orientation".into(), - Custom { uri, .. } => uri.clone(), - } - } - - pub fn is_same(&self, other: &Self) -> bool { - use HeaderExtension::*; - match (self, other) { - (AbsSendTime(_), AbsSendTime(_)) => true, - (AudioLevel(_), AudioLevel(_)) => true, - (TransportCc(_), TransportCc(_)) => true, - (VideoOrientation(_), VideoOrientation(_)) => true, - (Custom { uri, .. }, Custom { uri: other_uri, .. }) => uri == other_uri, - _ => false, - } - } -} - -impl MarshalSize for HeaderExtension { - fn marshal_size(&self) -> usize { - use HeaderExtension::*; - match self { - AbsSendTime(ext) => ext.marshal_size(), - AudioLevel(ext) => ext.marshal_size(), - PlayoutDelay(ext) => ext.marshal_size(), - TransportCc(ext) => ext.marshal_size(), - VideoOrientation(ext) => ext.marshal_size(), - Custom { extension: ext, .. } => ext.marshal_size(), - } - } -} - -impl Marshal for HeaderExtension { - fn marshal_to(&self, buf: &mut [u8]) -> util::Result { - use HeaderExtension::*; - match self { - AbsSendTime(ext) => ext.marshal_to(buf), - AudioLevel(ext) => ext.marshal_to(buf), - PlayoutDelay(ext) => ext.marshal_to(buf), - TransportCc(ext) => ext.marshal_to(buf), - VideoOrientation(ext) => ext.marshal_to(buf), - Custom { extension: ext, .. } => ext.marshal_to(buf), - } - } -} - -impl fmt::Debug for HeaderExtension { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use HeaderExtension::*; - - match self { - AbsSendTime(ext) => f.debug_tuple("AbsSendTime").field(ext).finish(), - AudioLevel(ext) => f.debug_tuple("AudioLevel").field(ext).finish(), - PlayoutDelay(ext) => f.debug_tuple("PlayoutDelay").field(ext).finish(), - TransportCc(ext) => f.debug_tuple("TransportCc").field(ext).finish(), - VideoOrientation(ext) => f.debug_tuple("VideoOrientation").field(ext).finish(), - Custom { uri, extension: _ } => f.debug_struct("Custom").field("uri", uri).finish(), - } - } -} diff --git a/rtp/src/extension/playout_delay_extension/mod.rs b/rtp/src/extension/playout_delay_extension/mod.rs deleted file mode 100644 index 55e264cc8..000000000 --- a/rtp/src/extension/playout_delay_extension/mod.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[cfg(test)] -mod playout_delay_extension_test; - -use bytes::BufMut; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -pub const PLAYOUT_DELAY_EXTENSION_SIZE: usize = 3; -pub const PLAYOUT_DELAY_MAX_VALUE: u16 = (1 << 12) - 1; - -/// PlayoutDelayExtension is an extension payload format described in -/// http://www.webrtc.org/experiments/rtp-hdrext/playout-delay -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ID | len=2 | MIN delay | MAX delay | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] -pub struct PlayoutDelayExtension { - pub min_delay: u16, - pub max_delay: u16, -} - -impl Unmarshal for PlayoutDelayExtension { - /// Unmarshal parses the passed byte slice and stores the result in the members. - fn unmarshal(buf: &mut B) -> util::Result - where - Self: Sized, - B: bytes::Buf, - { - if buf.remaining() < PLAYOUT_DELAY_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - - let b0 = buf.get_u8(); - let b1 = buf.get_u8(); - let b2 = buf.get_u8(); - - let min_delay = u16::from_be_bytes([b0, b1]) >> 4; - let max_delay = u16::from_be_bytes([b1, b2]) & 0x0FFF; - - Ok(PlayoutDelayExtension { - min_delay, - max_delay, - }) - } -} - -impl MarshalSize for PlayoutDelayExtension { - /// MarshalSize returns the size of the PlayoutDelayExtension once marshaled. - fn marshal_size(&self) -> usize { - PLAYOUT_DELAY_EXTENSION_SIZE - } -} - -impl Marshal for PlayoutDelayExtension { - /// MarshalTo serializes the members to buffer - fn marshal_to(&self, mut buf: &mut [u8]) -> util::Result { - if buf.remaining_mut() < PLAYOUT_DELAY_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - if self.min_delay > PLAYOUT_DELAY_MAX_VALUE || self.max_delay > PLAYOUT_DELAY_MAX_VALUE { - return Err(Error::PlayoutDelayOverflow.into()); - } - - buf.put_u8((self.min_delay >> 4) as u8); - buf.put_u8(((self.min_delay << 4) as u8) | (self.max_delay >> 8) as u8); - buf.put_u8(self.max_delay as u8); - - Ok(PLAYOUT_DELAY_EXTENSION_SIZE) - } -} - -impl PlayoutDelayExtension { - pub fn new(min_delay: u16, max_delay: u16) -> Self { - PlayoutDelayExtension { - min_delay, - max_delay, - } - } -} diff --git a/rtp/src/extension/playout_delay_extension/playout_delay_extension_test.rs b/rtp/src/extension/playout_delay_extension/playout_delay_extension_test.rs deleted file mode 100644 index 1cb7a5faf..000000000 --- a/rtp/src/extension/playout_delay_extension/playout_delay_extension_test.rs +++ /dev/null @@ -1,38 +0,0 @@ -use bytes::BytesMut; - -use crate::error::Result; - -use super::*; - -#[test] -fn test_playout_delay_extension_roundtrip() -> Result<()> { - let test = PlayoutDelayExtension { - max_delay: 2345, - min_delay: 1234, - }; - - let mut raw = BytesMut::with_capacity(test.marshal_size()); - raw.resize(test.marshal_size(), 0); - test.marshal_to(&mut raw)?; - let raw = raw.freeze(); - let buf = &mut raw.clone(); - let out = PlayoutDelayExtension::unmarshal(buf)?; - assert_eq!(test, out); - - Ok(()) -} - -#[test] -fn test_playout_delay_value_overflow() -> Result<()> { - let test = PlayoutDelayExtension { - max_delay: u16::MAX, - min_delay: u16::MAX, - }; - - let mut dst = BytesMut::with_capacity(test.marshal_size()); - dst.resize(test.marshal_size(), 0); - let result = test.marshal_to(&mut dst); - assert!(result.is_err()); - - Ok(()) -} diff --git a/rtp/src/extension/transport_cc_extension/mod.rs b/rtp/src/extension/transport_cc_extension/mod.rs deleted file mode 100644 index cb72ed1df..000000000 --- a/rtp/src/extension/transport_cc_extension/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[cfg(test)] -mod transport_cc_extension_test; - -use bytes::{Buf, BufMut}; -use serde::{Deserialize, Serialize}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -// transport-wide sequence -pub const TRANSPORT_CC_EXTENSION_SIZE: usize = 2; - -/// TransportCCExtension is a extension payload format in -/// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01 -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | 0xBE | 0xDE | length=1 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ID | L=1 |transport-wide sequence number | zero padding | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] -pub struct TransportCcExtension { - pub transport_sequence: u16, -} - -impl Unmarshal for TransportCcExtension { - /// Unmarshal parses the passed byte slice and stores the result in the members - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - if raw_packet.remaining() < TRANSPORT_CC_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - let b0 = raw_packet.get_u8(); - let b1 = raw_packet.get_u8(); - - let transport_sequence = ((b0 as u16) << 8) | b1 as u16; - Ok(TransportCcExtension { transport_sequence }) - } -} - -impl MarshalSize for TransportCcExtension { - /// MarshalSize returns the size of the TransportCcExtension once marshaled. - fn marshal_size(&self) -> usize { - TRANSPORT_CC_EXTENSION_SIZE - } -} - -impl Marshal for TransportCcExtension { - /// Marshal serializes the members to buffer - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < TRANSPORT_CC_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - buf.put_u16(self.transport_sequence); - Ok(TRANSPORT_CC_EXTENSION_SIZE) - } -} diff --git a/rtp/src/extension/transport_cc_extension/transport_cc_extension_test.rs b/rtp/src/extension/transport_cc_extension/transport_cc_extension_test.rs deleted file mode 100644 index f647618b3..000000000 --- a/rtp/src/extension/transport_cc_extension/transport_cc_extension_test.rs +++ /dev/null @@ -1,44 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_transport_cc_extension_too_small() -> Result<()> { - let mut buf = &vec![0u8; 0][..]; - let result = TransportCcExtension::unmarshal(&mut buf); - assert!(result.is_err()); - - Ok(()) -} - -#[test] -fn test_transport_cc_extension() -> Result<()> { - let raw = Bytes::from_static(&[0x00, 0x02]); - let buf = &mut raw.clone(); - let t1 = TransportCcExtension::unmarshal(buf)?; - let t2 = TransportCcExtension { - transport_sequence: 2, - }; - assert_eq!(t1, t2); - - let mut dst = BytesMut::with_capacity(t2.marshal_size()); - dst.resize(t2.marshal_size(), 0); - t2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_transport_cc_extension_extra_bytes() -> Result<()> { - let mut raw = Bytes::from_static(&[0x00, 0x02, 0x00, 0xff, 0xff]); - let buf = &mut raw; - let t1 = TransportCcExtension::unmarshal(buf)?; - let t2 = TransportCcExtension { - transport_sequence: 2, - }; - assert_eq!(t1, t2); - - Ok(()) -} diff --git a/rtp/src/extension/video_orientation_extension/mod.rs b/rtp/src/extension/video_orientation_extension/mod.rs deleted file mode 100644 index d03ced95a..000000000 --- a/rtp/src/extension/video_orientation_extension/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -#[cfg(test)] -mod video_orientation_extension_test; - -use std::convert::{TryFrom, TryInto}; - -use bytes::BufMut; -use serde::{Deserialize, Serialize}; -use util::marshal::Unmarshal; -use util::{Marshal, MarshalSize}; - -use crate::Error; - -// One byte header size -pub const VIDEO_ORIENTATION_EXTENSION_SIZE: usize = 1; - -/// Coordination of Video Orientation in RTP streams. -/// -/// Coordination of Video Orientation consists in signaling of the current -/// orientation of the image captured on the sender side to the receiver for -/// appropriate rendering and displaying. -/// -/// C = Camera: indicates the direction of the camera used for this video -/// stream. It can be used by the MTSI client in receiver to e.g. display -/// the received video differently depending on the source camera. -/// -/// 0: Front-facing camera, facing the user. If camera direction is -/// unknown by the sending MTSI client in the terminal then this is the -/// default value used. -/// 1: Back-facing camera, facing away from the user. -/// -/// F = Flip: indicates a horizontal (left-right flip) mirror operation on -/// the video as sent on the link. -/// -/// 0 1 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | ID | len=0 |0 0 0 0 C F R R| -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] -pub struct VideoOrientationExtension { - pub direction: CameraDirection, - pub flip: bool, - pub rotation: VideoRotation, -} - -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] -pub enum CameraDirection { - #[default] - Front = 0, - Back = 1, -} - -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] -pub enum VideoRotation { - #[default] - Degree0 = 0, - Degree90 = 1, - Degree180 = 2, - Degree270 = 3, -} - -impl MarshalSize for VideoOrientationExtension { - fn marshal_size(&self) -> usize { - VIDEO_ORIENTATION_EXTENSION_SIZE - } -} - -impl Unmarshal for VideoOrientationExtension { - fn unmarshal(buf: &mut B) -> util::Result - where - Self: Sized, - B: bytes::Buf, - { - if buf.remaining() < VIDEO_ORIENTATION_EXTENSION_SIZE { - return Err(Error::ErrBufferTooSmall.into()); - } - - let b = buf.get_u8(); - - let c = (b & 0b1000) >> 3; - let f = b & 0b0100; - let r = b & 0b0011; - - Ok(VideoOrientationExtension { - direction: c.try_into()?, - flip: f > 0, - rotation: r.try_into()?, - }) - } -} - -impl Marshal for VideoOrientationExtension { - fn marshal_to(&self, mut buf: &mut [u8]) -> util::Result { - let c = (self.direction as u8) << 3; - let f = if self.flip { 0b0100 } else { 0 }; - let r = self.rotation as u8; - - buf.put_u8(c | f | r); - - Ok(VIDEO_ORIENTATION_EXTENSION_SIZE) - } -} - -impl TryFrom for CameraDirection { - type Error = util::Error; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(CameraDirection::Front), - 1 => Ok(CameraDirection::Back), - _ => Err(util::Error::Other(format!( - "Unhandled camera direction: {value}" - ))), - } - } -} - -impl TryFrom for VideoRotation { - type Error = util::Error; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(VideoRotation::Degree0), - 1 => Ok(VideoRotation::Degree90), - 2 => Ok(VideoRotation::Degree180), - 3 => Ok(VideoRotation::Degree270), - _ => Err(util::Error::Other(format!( - "Unhandled video rotation: {value}" - ))), - } - } -} diff --git a/rtp/src/extension/video_orientation_extension/video_orientation_extension_test.rs b/rtp/src/extension/video_orientation_extension/video_orientation_extension_test.rs deleted file mode 100644 index c8f680958..000000000 --- a/rtp/src/extension/video_orientation_extension/video_orientation_extension_test.rs +++ /dev/null @@ -1,113 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_video_orientation_extension_too_small() -> Result<()> { - let mut buf = &vec![0u8; 0][..]; - let result = VideoOrientationExtension::unmarshal(&mut buf); - assert!(result.is_err()); - - Ok(()) -} - -#[test] -fn test_video_orientation_extension_back_facing_camera() -> Result<()> { - let raw = Bytes::from_static(&[0b1000]); - let buf = &mut raw.clone(); - let a1 = VideoOrientationExtension::unmarshal(buf)?; - let a2 = VideoOrientationExtension { - direction: CameraDirection::Back, - flip: false, - rotation: VideoRotation::Degree0, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_video_orientation_extension_flip_true() -> Result<()> { - let raw = Bytes::from_static(&[0b0100]); - let buf = &mut raw.clone(); - let a1 = VideoOrientationExtension::unmarshal(buf)?; - let a2 = VideoOrientationExtension { - direction: CameraDirection::Front, - flip: true, - rotation: VideoRotation::Degree0, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_video_orientation_extension_degree_90() -> Result<()> { - let raw = Bytes::from_static(&[0b0001]); - let buf = &mut raw.clone(); - let a1 = VideoOrientationExtension::unmarshal(buf)?; - let a2 = VideoOrientationExtension { - direction: CameraDirection::Front, - flip: false, - rotation: VideoRotation::Degree90, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_video_orientation_extension_degree_180() -> Result<()> { - let raw = Bytes::from_static(&[0b0010]); - let buf = &mut raw.clone(); - let a1 = VideoOrientationExtension::unmarshal(buf)?; - let a2 = VideoOrientationExtension { - direction: CameraDirection::Front, - flip: false, - rotation: VideoRotation::Degree180, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} - -#[test] -fn test_video_orientation_extension_degree_270() -> Result<()> { - let raw = Bytes::from_static(&[0b0011]); - let buf = &mut raw.clone(); - let a1 = VideoOrientationExtension::unmarshal(buf)?; - let a2 = VideoOrientationExtension { - direction: CameraDirection::Front, - flip: false, - rotation: VideoRotation::Degree270, - }; - assert_eq!(a1, a2); - - let mut dst = BytesMut::with_capacity(a2.marshal_size()); - dst.resize(a2.marshal_size(), 0); - a2.marshal_to(&mut dst)?; - assert_eq!(raw, dst.freeze()); - - Ok(()) -} diff --git a/rtp/src/header.rs b/rtp/src/header.rs deleted file mode 100644 index 14097c794..000000000 --- a/rtp/src/header.rs +++ /dev/null @@ -1,477 +0,0 @@ -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; - -pub const HEADER_LENGTH: usize = 4; -pub const VERSION_SHIFT: u8 = 6; -pub const VERSION_MASK: u8 = 0x3; -pub const PADDING_SHIFT: u8 = 5; -pub const PADDING_MASK: u8 = 0x1; -pub const EXTENSION_SHIFT: u8 = 4; -pub const EXTENSION_MASK: u8 = 0x1; -pub const EXTENSION_PROFILE_ONE_BYTE: u16 = 0xBEDE; -pub const EXTENSION_PROFILE_TWO_BYTE: u16 = 0x1000; -pub const EXTENSION_ID_RESERVED: u8 = 0xF; -pub const CC_MASK: u8 = 0xF; -pub const MARKER_SHIFT: u8 = 7; -pub const MARKER_MASK: u8 = 0x1; -pub const PT_MASK: u8 = 0x7F; -pub const SEQ_NUM_OFFSET: usize = 2; -pub const SEQ_NUM_LENGTH: usize = 2; -pub const TIMESTAMP_OFFSET: usize = 4; -pub const TIMESTAMP_LENGTH: usize = 4; -pub const SSRC_OFFSET: usize = 8; -pub const SSRC_LENGTH: usize = 4; -pub const CSRC_OFFSET: usize = 12; -pub const CSRC_LENGTH: usize = 4; - -#[derive(Debug, Eq, PartialEq, Default, Clone)] -pub struct Extension { - pub id: u8, - pub payload: Bytes, -} - -/// Header represents an RTP packet header -/// NOTE: PayloadOffset is populated by Marshal/Unmarshal and should not be modified -#[derive(Debug, Eq, PartialEq, Default, Clone)] -pub struct Header { - pub version: u8, - pub padding: bool, - pub extension: bool, - pub marker: bool, - pub payload_type: u8, - pub sequence_number: u16, - pub timestamp: u32, - pub ssrc: u32, - pub csrc: Vec, - pub extension_profile: u16, - pub extensions: Vec, - pub extensions_padding: usize, -} - -impl Unmarshal for Header { - /// Unmarshal parses the passed byte slice and stores the result in the Header this method is called upon - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let raw_packet_len = raw_packet.remaining(); - if raw_packet_len < HEADER_LENGTH { - return Err(Error::ErrHeaderSizeInsufficient.into()); - } - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P|X| CC |M| PT | sequence number | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | timestamp | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | synchronization source (SSRC) identifier | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | contributing source (CSRC) identifiers | - * | .... | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let b0 = raw_packet.get_u8(); - let version = b0 >> VERSION_SHIFT & VERSION_MASK; - let padding = (b0 >> PADDING_SHIFT & PADDING_MASK) > 0; - let extension = (b0 >> EXTENSION_SHIFT & EXTENSION_MASK) > 0; - let cc = (b0 & CC_MASK) as usize; - - let mut curr_offset = CSRC_OFFSET + (cc * CSRC_LENGTH); - if raw_packet_len < curr_offset { - return Err(Error::ErrHeaderSizeInsufficient.into()); - } - - let b1 = raw_packet.get_u8(); - let marker = (b1 >> MARKER_SHIFT & MARKER_MASK) > 0; - let payload_type = b1 & PT_MASK; - - let sequence_number = raw_packet.get_u16(); - let timestamp = raw_packet.get_u32(); - let ssrc = raw_packet.get_u32(); - - let mut csrc = Vec::with_capacity(cc); - for _ in 0..cc { - csrc.push(raw_packet.get_u32()); - } - let mut extensions_padding: usize = 0; - let (extension_profile, extensions) = if extension { - let expected = curr_offset + 4; - if raw_packet_len < expected { - return Err(Error::ErrHeaderSizeInsufficientForExtension.into()); - } - let extension_profile = raw_packet.get_u16(); - curr_offset += 2; - let extension_length = raw_packet.get_u16() as usize * 4; - curr_offset += 2; - - let expected = curr_offset + extension_length; - if raw_packet_len < expected { - return Err(Error::ErrHeaderSizeInsufficientForExtension.into()); - } - - let mut extensions = vec![]; - match extension_profile { - // RFC 8285 RTP One Byte Header Extension - EXTENSION_PROFILE_ONE_BYTE => { - let end = curr_offset + extension_length; - while curr_offset < end { - let b = raw_packet.get_u8(); - if b == 0x00 { - // padding - curr_offset += 1; - extensions_padding += 1; - continue; - } - - let extid = b >> 4; - let len = ((b & (0xFF ^ 0xF0)) + 1) as usize; - curr_offset += 1; - - if extid == EXTENSION_ID_RESERVED { - break; - } - - extensions.push(Extension { - id: extid, - payload: raw_packet.copy_to_bytes(len), - }); - curr_offset += len; - } - } - // RFC 8285 RTP Two Byte Header Extension - EXTENSION_PROFILE_TWO_BYTE => { - let end = curr_offset + extension_length; - while curr_offset < end { - let b = raw_packet.get_u8(); - if b == 0x00 { - // padding - curr_offset += 1; - extensions_padding += 1; - continue; - } - - let extid = b; - curr_offset += 1; - - let len = raw_packet.get_u8() as usize; - curr_offset += 1; - - extensions.push(Extension { - id: extid, - payload: raw_packet.copy_to_bytes(len), - }); - curr_offset += len; - } - } - // RFC3550 Extension - _ => { - if raw_packet_len < curr_offset + extension_length { - return Err(Error::ErrHeaderSizeInsufficientForExtension.into()); - } - extensions.push(Extension { - id: 0, - payload: raw_packet.copy_to_bytes(extension_length), - }); - } - }; - - (extension_profile, extensions) - } else { - (0, vec![]) - }; - - Ok(Header { - version, - padding, - extension, - marker, - payload_type, - sequence_number, - timestamp, - ssrc, - csrc, - extension_profile, - extensions, - extensions_padding, - }) - } -} - -impl MarshalSize for Header { - /// MarshalSize returns the size of the packet once marshaled. - fn marshal_size(&self) -> usize { - let mut head_size = 12 + (self.csrc.len() * CSRC_LENGTH); - if self.extension { - let extension_payload_len = self.get_extension_payload_len() + self.extensions_padding; - let extension_payload_size = (extension_payload_len + 3) / 4; - head_size += 4 + extension_payload_size * 4; - } - head_size - } -} - -impl Marshal for Header { - /// Marshal serializes the header and writes to the buffer. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - /* - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |V=2|P|X| CC |M| PT | sequence number | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | timestamp | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | synchronization source (SSRC) identifier | - * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ - * | contributing source (CSRC) identifiers | - * | .... | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - let remaining_before = buf.remaining_mut(); - if remaining_before < self.marshal_size() { - return Err(Error::ErrBufferTooSmall.into()); - } - - // The first byte contains the version, padding bit, extension bit, and csrc size - let mut b0 = (self.version << VERSION_SHIFT) | self.csrc.len() as u8; - if self.padding { - b0 |= 1 << PADDING_SHIFT; - } - - if self.extension { - b0 |= 1 << EXTENSION_SHIFT; - } - buf.put_u8(b0); - - // The second byte contains the marker bit and payload type. - let mut b1 = self.payload_type; - if self.marker { - b1 |= 1 << MARKER_SHIFT; - } - buf.put_u8(b1); - - buf.put_u16(self.sequence_number); - buf.put_u32(self.timestamp); - buf.put_u32(self.ssrc); - - for csrc in &self.csrc { - buf.put_u32(*csrc); - } - - if self.extension { - buf.put_u16(self.extension_profile); - - // calculate extensions size and round to 4 bytes boundaries - let extension_payload_len = self.get_extension_payload_len(); - if self.extension_profile != EXTENSION_PROFILE_ONE_BYTE - && self.extension_profile != EXTENSION_PROFILE_TWO_BYTE - && extension_payload_len % 4 != 0 - { - //the payload must be in 32-bit words. - return Err(Error::HeaderExtensionPayloadNot32BitWords.into()); - } - let extension_payload_size = (extension_payload_len as u16 + 3) / 4; - buf.put_u16(extension_payload_size); - - match self.extension_profile { - // RFC 8285 RTP One Byte Header Extension - EXTENSION_PROFILE_ONE_BYTE => { - for extension in &self.extensions { - buf.put_u8((extension.id << 4) | (extension.payload.len() as u8 - 1)); - buf.put(&*extension.payload); - } - } - // RFC 8285 RTP Two Byte Header Extension - EXTENSION_PROFILE_TWO_BYTE => { - for extension in &self.extensions { - buf.put_u8(extension.id); - buf.put_u8(extension.payload.len() as u8); - buf.put(&*extension.payload); - } - } - // RFC3550 Extension - _ => { - if self.extensions.len() != 1 { - return Err(Error::ErrRfc3550headerIdrange.into()); - } - - if let Some(extension) = self.extensions.first() { - let ext_len = extension.payload.len(); - if ext_len % 4 != 0 { - return Err(Error::HeaderExtensionPayloadNot32BitWords.into()); - } - buf.put(&*extension.payload); - } - } - }; - - // add padding to reach 4 bytes boundaries - for _ in extension_payload_len..extension_payload_size as usize * 4 { - buf.put_u8(0); - } - } - - let remaining_after = buf.remaining_mut(); - Ok(remaining_before - remaining_after) - } -} - -impl Header { - pub fn get_extension_payload_len(&self) -> usize { - let payload_len: usize = self - .extensions - .iter() - .map(|extension| extension.payload.len()) - .sum(); - - let profile_len = self.extensions.len() - * match self.extension_profile { - EXTENSION_PROFILE_ONE_BYTE => 1, - EXTENSION_PROFILE_TWO_BYTE => 2, - _ => 0, - }; - - payload_len + profile_len - } - - /// SetExtension sets an RTP header extension - pub fn set_extension(&mut self, id: u8, payload: Bytes) -> Result<(), Error> { - let payload_len = payload.len() as isize; - if self.extension { - let extension_profile_len = match self.extension_profile { - EXTENSION_PROFILE_ONE_BYTE => { - if !(1..=14).contains(&id) { - return Err(Error::ErrRfc8285oneByteHeaderIdrange); - } - if payload_len > 16 { - return Err(Error::ErrRfc8285oneByteHeaderSize); - } - 1 - } - EXTENSION_PROFILE_TWO_BYTE => { - if id < 1 { - return Err(Error::ErrRfc8285twoByteHeaderIdrange); - } - if payload_len > 255 { - return Err(Error::ErrRfc8285twoByteHeaderSize); - } - 2 - } - _ => { - if id != 0 { - return Err(Error::ErrRfc3550headerIdrange); - } - 0 - } - }; - - let delta; - // Update existing if it exists else add new extension - if let Some(extension) = self - .extensions - .iter_mut() - .find(|extension| extension.id == id) - { - delta = payload_len - extension.payload.len() as isize; - extension.payload = payload; - } else { - delta = payload_len + extension_profile_len; - self.extensions.push(Extension { id, payload }); - } - - match delta.cmp(&0) { - std::cmp::Ordering::Less => { - self.extensions_padding = - ((self.extensions_padding as isize - delta) % 4) as usize; - } - std::cmp::Ordering::Greater => { - let extension_padding = (delta % 4) as usize; - if self.extensions_padding < extension_padding { - self.extensions_padding = (self.extensions_padding + 4) - extension_padding; - } else { - self.extensions_padding -= extension_padding - } - } - _ => {} - } - } else { - // No existing header extensions - self.extension = true; - let mut extension_profile_len = 0; - self.extension_profile = match payload_len { - 0..=16 => { - extension_profile_len = 1; - EXTENSION_PROFILE_ONE_BYTE - } - 17..=255 => { - extension_profile_len = 2; - EXTENSION_PROFILE_TWO_BYTE - } - _ => self.extension_profile, - }; - - let extension_padding = (payload.len() + extension_profile_len) % 4; - if self.extensions_padding < extension_padding { - self.extensions_padding = self.extensions_padding + 4 - extension_padding; - } else { - self.extensions_padding -= extension_padding - } - self.extensions.push(Extension { id, payload }); - } - Ok(()) - } - - /// returns an extension id array - pub fn get_extension_ids(&self) -> Vec { - if self.extension { - self.extensions.iter().map(|e| e.id).collect() - } else { - vec![] - } - } - - /// returns an RTP header extension - pub fn get_extension(&self, id: u8) -> Option { - if self.extension { - self.extensions - .iter() - .find(|extension| extension.id == id) - .map(|extension| extension.payload.clone()) - } else { - None - } - } - - /// Removes an RTP Header extension - pub fn del_extension(&mut self, id: u8) -> Result<(), Error> { - if self.extension { - if let Some(index) = self - .extensions - .iter() - .position(|extension| extension.id == id) - { - let extension = self.extensions.remove(index); - - let extension_profile_len = match self.extension_profile { - EXTENSION_PROFILE_ONE_BYTE => 1, - EXTENSION_PROFILE_TWO_BYTE => 2, - _ => 0, - }; - - let extension_padding = (extension.payload.len() + extension_profile_len) % 4; - self.extensions_padding = (self.extensions_padding + extension_padding) % 4; - - Ok(()) - } else { - Err(Error::ErrHeaderExtensionNotFound) - } - } else { - Err(Error::ErrHeaderExtensionsNotEnabled) - } - } -} diff --git a/rtp/src/lib.rs b/rtp/src/lib.rs deleted file mode 100644 index 18066e7e5..000000000 --- a/rtp/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod codecs; -mod error; -pub mod extension; -pub mod header; -pub mod packet; -pub mod packetizer; -pub mod sequence; - -pub use error::Error; diff --git a/rtp/src/packet/mod.rs b/rtp/src/packet/mod.rs deleted file mode 100644 index 3a76b7eaf..000000000 --- a/rtp/src/packet/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -#[cfg(test)] -mod packet_test; - -use std::fmt; - -use bytes::{Buf, BufMut, Bytes}; -use util::marshal::{Marshal, MarshalSize, Unmarshal}; - -use crate::error::Error; -use crate::header::*; - -/// Packet represents an RTP Packet -/// NOTE: Raw is populated by Marshal/Unmarshal and should not be modified -#[derive(Debug, Eq, PartialEq, Default, Clone)] -pub struct Packet { - pub header: Header, - pub payload: Bytes, -} - -impl fmt::Display for Packet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = "RTP PACKET:\n".to_string(); - - out += format!("\tVersion: {}\n", self.header.version).as_str(); - out += format!("\tMarker: {}\n", self.header.marker).as_str(); - out += format!("\tPayload Type: {}\n", self.header.payload_type).as_str(); - out += format!("\tSequence Number: {}\n", self.header.sequence_number).as_str(); - out += format!("\tTimestamp: {}\n", self.header.timestamp).as_str(); - out += format!("\tSSRC: {} ({:x})\n", self.header.ssrc, self.header.ssrc).as_str(); - out += format!("\tPayload Length: {}\n", self.payload.len()).as_str(); - - write!(f, "{out}") - } -} - -impl Unmarshal for Packet { - /// Unmarshal parses the passed byte slice and stores the result in the Header this method is called upon - fn unmarshal(raw_packet: &mut B) -> Result - where - Self: Sized, - B: Buf, - { - let header = Header::unmarshal(raw_packet)?; - let payload_len = raw_packet.remaining(); - let payload = raw_packet.copy_to_bytes(payload_len); - if header.padding { - if payload_len > 0 { - let padding_len = payload[payload_len - 1] as usize; - if padding_len <= payload_len { - Ok(Packet { - header, - payload: payload.slice(..payload_len - padding_len), - }) - } else { - Err(Error::ErrShortPacket.into()) - } - } else { - Err(Error::ErrShortPacket.into()) - } - } else { - Ok(Packet { header, payload }) - } - } -} - -impl MarshalSize for Packet { - /// MarshalSize returns the size of the packet once marshaled. - fn marshal_size(&self) -> usize { - let payload_len = self.payload.len(); - let padding_len = if self.header.padding { - let padding_len = get_padding(payload_len); - if padding_len == 0 { - 4 - } else { - padding_len - } - } else { - 0 - }; - self.header.marshal_size() + payload_len + padding_len - } -} - -impl Marshal for Packet { - /// MarshalTo serializes the packet and writes to the buffer. - fn marshal_to(&self, mut buf: &mut [u8]) -> Result { - if buf.remaining_mut() < self.marshal_size() { - return Err(Error::ErrBufferTooSmall.into()); - } - - let n = self.header.marshal_to(buf)?; - buf = &mut buf[n..]; - buf.put(&*self.payload); - let padding_len = if self.header.padding { - let mut padding_len = get_padding(self.payload.len()); - if padding_len == 0 { - padding_len = 4; - } - for i in 0..padding_len { - if i != padding_len - 1 { - buf.put_u8(0); - } else { - buf.put_u8(padding_len as u8); - } - } - padding_len - } else { - 0 - }; - - Ok(n + self.payload.len() + padding_len) - } -} - -/// getPadding Returns the padding required to make the length a multiple of 4 -fn get_padding(len: usize) -> usize { - if len % 4 == 0 { - 0 - } else { - 4 - (len % 4) - } -} diff --git a/rtp/src/packet/packet_test.rs b/rtp/src/packet/packet_test.rs deleted file mode 100644 index 926f922e6..000000000 --- a/rtp/src/packet/packet_test.rs +++ /dev/null @@ -1,1243 +0,0 @@ -// Silence warning on `..Default::default()` with no effect: -#![allow(clippy::needless_update)] - -use bytes::{Bytes, BytesMut}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_basic() -> Result<()> { - let mut empty_bytes = &vec![0u8; 0][..]; - let result = Packet::unmarshal(&mut empty_bytes); - assert!( - result.is_err(), - "Unmarshal did not error on zero length packet" - ); - - let raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, - 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - let parsed_packet = Packet { - header: Header { - version: 2, - padding: false, - extension: true, - marker: true, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - extension_profile: 1, - extensions: vec![Extension { - id: 0, - payload: Bytes::from_static(&[0xFF, 0xFF, 0xFF, 0xFF]), - }], - ..Default::default() - }, - payload: Bytes::from_static(&[0x98, 0x36, 0xbe, 0x88, 0x9e]), - }; - let buf = &mut raw_pkt.clone(); - let packet = Packet::unmarshal(buf)?; - assert_eq!( - packet, parsed_packet, - "TestBasic unmarshal: got {packet}, want {parsed_packet}" - ); - assert_eq!( - packet.header.marshal_size(), - 20, - "wrong computed header marshal size" - ); - assert_eq!( - packet.marshal_size(), - raw_pkt.len(), - "wrong computed marshal size" - ); - - let raw = packet.marshal()?; - let n = raw.len(); - assert_eq!(n, raw_pkt.len(), "wrong marshal size"); - - assert_eq!( - raw.len(), - raw_pkt.len(), - "wrong raw marshal size {} vs {}", - raw.len(), - raw_pkt.len() - ); - assert_eq!( - raw, raw_pkt, - "TestBasic marshal: got {raw:?}, want {raw_pkt:?}" - ); - - Ok(()) -} - -#[test] -fn test_extension() -> Result<()> { - let mut missing_extension_pkt = Bytes::from_static(&[ - 0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, - ]); - let buf = &mut missing_extension_pkt; - let result = Packet::unmarshal(buf); - assert!( - result.is_err(), - "Unmarshal did not error on packet with missing extension data" - ); - - let mut invalid_extension_length_pkt = Bytes::from_static(&[ - 0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x99, 0x99, 0x99, - 0x99, - ]); - let buf = &mut invalid_extension_length_pkt; - let result = Packet::unmarshal(buf); - assert!( - result.is_err(), - "Unmarshal did not error on packet with invalid extension length" - ); - - let packet = Packet { - header: Header { - extension: true, - extension_profile: 3, - extensions: vec![Extension { - id: 0, - payload: Bytes::from_static(&[0]), - }], - ..Default::default() - }, - payload: Bytes::from_static(&[]), - }; - - let mut raw = BytesMut::new(); - let result = packet.marshal_to(&mut raw); - assert!( - result.is_err(), - "Marshal did not error on packet with invalid extension length" - ); - if let Err(err) = result { - assert_eq!(Error::ErrBufferTooSmall, err); - } - - Ok(()) -} - -#[test] -fn test_padding() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0xa0, 0x60, 0x19, 0x58, 0x63, 0xff, 0x7d, 0x7c, 0x4b, 0x98, 0xd4, 0x0a, 0x67, 0x4d, 0x00, - 0x29, 0x9a, 0x64, 0x03, 0xc0, 0x11, 0x3f, 0x2c, 0xd4, 0x04, 0x04, 0x05, 0x00, 0x00, 0x03, - 0x03, 0xe8, 0x00, 0x00, 0xea, 0x60, 0x04, 0x00, 0x00, 0x03, - ]); - let buf = &mut raw_pkt.clone(); - let packet = Packet::unmarshal(buf)?; - assert_eq!(&packet.payload[..], &raw_pkt[12..12 + 25]); - - let raw = packet.marshal()?; - assert_eq!(raw, raw_pkt); - - Ok(()) -} - -#[test] -fn test_packet_marshal_unmarshal() -> Result<()> { - let pkt = Packet { - header: Header { - extension: true, - csrc: vec![1, 2], - extension_profile: EXTENSION_PROFILE_TWO_BYTE, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[3, 4]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[5, 6]), - }, - ], - ..Default::default() - }, - payload: Bytes::from_static(&[0xFFu8; 15]), - ..Default::default() - }; - let mut raw = pkt.marshal()?; - let p = Packet::unmarshal(&mut raw)?; - - assert_eq!(pkt, p); - - Ok(()) -} - -#[test] -fn test_rfc_8285_one_byte_extension() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x01, 0x50, 0xAA, 0x00, 0x00, 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - let buf = &mut raw_pkt.clone(); - Packet::unmarshal(buf)?; - - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 5, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - ..Default::default() - }, - payload: raw_pkt.slice(20..), - }; - - let dst = p.marshal()?; - assert_eq!(dst, raw_pkt); - - Ok(()) -} - -#[test] -fn test_rfc_8285_one_byte_two_extension_of_two_bytes() -> Result<()> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | 0xBE | 0xDE | length=1 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID | L=0 | data | ID | L=0 | data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - let raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x01, 0x10, 0xAA, 0x20, 0xBB, // Payload - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - let buf = &mut raw_pkt.clone(); - let p = Packet::unmarshal(buf)?; - - let ext1 = p.header.get_extension(1); - let ext1_expect = Bytes::from_static(&[0xAA]); - if let Some(ext1) = ext1 { - assert_eq!(ext1, ext1_expect); - } else { - panic!("ext1 is none"); - } - - let ext2 = p.header.get_extension(2); - let ext2_expect = Bytes::from_static(&[0xBB]); - if let Some(ext2) = ext2 { - assert_eq!(ext2, ext2_expect); - } else { - panic!("ext2 is none"); - } - - // Test Marshal - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[0xBB]), - }, - ], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - csrc: vec![], - ..Default::default() - }, - payload: raw_pkt.slice(20..), - }; - - let dst = p.marshal()?; - assert_eq!(dst, raw_pkt); - - Ok(()) -} - -#[test] -fn test_rfc_8285_one_byte_multiple_extensions_with_padding() -> Result<()> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | 0xBE | 0xDE | length=3 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID | L=0 | data | ID | L=1 | data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data | 0 (pad) | 0 (pad) | ID | L=3 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | data | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - let mut raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x03, 0x10, 0xAA, 0x21, 0xBB, 0xBB, 0x00, 0x00, 0x33, 0xCC, 0xCC, 0xCC, 0xCC, - // Payload - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - let buf = &mut raw_pkt; - let packet = Packet::unmarshal(buf)?; - let ext1 = packet - .header - .get_extension(1) - .expect("Error getting header extension."); - - let ext1_expect = Bytes::from_static(&[0xAA]); - assert_eq!(ext1, ext1_expect); - - let ext2 = packet - .header - .get_extension(2) - .expect("Error getting header extension."); - - let ext2_expect = Bytes::from_static(&[0xBB, 0xBB]); - assert_eq!(ext2, ext2_expect); - - let ext3 = packet - .header - .get_extension(3) - .expect("Error getting header extension."); - - let ext3_expect = Bytes::from_static(&[0xCC, 0xCC, 0xCC, 0xCC]); - assert_eq!(ext3, ext3_expect); - - let mut dst_buf: Vec> = vec![vec![0u8; 1000], vec![0xFF; 1000], vec![0xAA; 2]]; - - let raw_pkg_marshal: [u8; 33] = [ - // padding is moved to the end by re-marshaling - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x03, 0x10, 0xAA, 0x21, 0xBB, 0xBB, 0x33, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, - // Payload - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]; - - let checker = |name: &str, buf: &mut [u8], p: &Packet| -> Result<()> { - let size = p.marshal_to(buf)?; - - assert_eq!( - &buf[..size], - &raw_pkg_marshal[..], - "Marshalled fields are not equal for {name}." - ); - - Ok(()) - }; - - checker("CleanBuffer", &mut dst_buf[0], &packet)?; - checker("DirtyBuffer", &mut dst_buf[1], &packet)?; - - let result = packet.marshal_to(&mut dst_buf[2]); - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrBufferTooSmall, err); - } - - Ok(()) -} - -fn test_rfc_8285_one_byte_multiple_extension() -> Result<()> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | 0xBE | 0xDE | length=3 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID=1 | L=0 | data | ID=2 | L=1 | data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data | ID=3 | L=3 | data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - let raw_pkt = &[ - 0x90u8, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x03, 0x10, 0xAA, 0x21, 0xBB, 0xBB, 0x33, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, - // Payload - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]; - - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[0xBB, 0xBB]), - }, - Extension { - id: 3, - payload: Bytes::from_static(&[0xCC, 0xCC]), - }, - ], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload: raw_pkt[28..].into(), - }; - - let dst_data = p.marshal()?; - assert_eq!( - &dst_data[..], - raw_pkt, - "Marshal failed raw \nMarshaled:\n{dst_data:?}\nrawPkt:\n{raw_pkt:?}", - ); - - Ok(()) -} - -fn test_rfc_8285_two_byte_extension() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x10, 0x00, 0x00, - 0x07, 0x05, 0x18, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0x00, 0x98, - 0x36, 0xbe, 0x88, 0x9e, - ]); - - let _ = Packet::unmarshal(&mut raw_pkt.clone())?; - - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0x1000, - extensions: vec![Extension { - id: 5, - payload: Bytes::from_static(&[ - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - ]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload: raw_pkt.slice(44..), - }; - - let dst_data = p.marshal()?; - assert_eq!( - dst_data, raw_pkt, - "Marshal failed raw \nMarshaled:\n{dst_data:?}\nrawPkt:\n{raw_pkt:?}" - ); - Ok(()) -} - -fn test_rfc8285_two_byte_multiple_extension_with_padding() -> Result<()> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | 0x10 | 0x00 | length=3 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID=1 | L=0 | ID=2 | L=1 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | data | 0 (pad) | ID=3 | L=4 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | data | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - let mut raw_pkt = Bytes::from_static(&[ - 0x90u8, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x10, 0x00, 0x00, - 0x03, 0x01, 0x00, 0x02, 0x01, 0xBB, 0x00, 0x03, 0x04, 0xCC, 0xCC, 0xCC, 0xCC, 0x98, 0x36, - 0xbe, 0x88, 0x9e, - ]); - - let p = Packet::unmarshal(&mut raw_pkt)?; - - let ext = p.header.get_extension(1); - let ext_expect = Some(Bytes::from_static(&[])); - assert_eq!( - ext, ext_expect, - "Extension has incorrect data. Got: {ext:?}, Expected: {ext_expect:?}" - ); - - let ext = p.header.get_extension(2); - let ext_expect = Some(Bytes::from_static(&[0xBB])); - assert_eq!( - ext, ext_expect, - "Extension has incorrect data. Got: {ext:?}, Expected: {ext_expect:?}" - ); - - let ext = p.header.get_extension(3); - let ext_expect = Some(Bytes::from_static(&[0xCC, 0xCC, 0xCC, 0xCC])); - assert_eq!( - ext, ext_expect, - "Extension has incorrect data. Got: {ext:?}, Expected: {ext_expect:?}" - ); - - Ok(()) -} - -fn test_rfc8285_two_byte_multiple_extension_with_large_extension() -> Result<()> { - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | 0x10 | 0x00 | length=3 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID=1 | L=0 | ID=2 | L=1 | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | data | ID=3 | L=17 | data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data... - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // ...data... | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - let raw_pkt = Bytes::from_static(&[ - 0x90u8, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x10, 0x00, 0x00, - 0x06, 0x01, 0x00, 0x02, 0x01, 0xBB, 0x03, 0x11, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, - 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // Payload - 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0x1000, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[0xBB]), - }, - Extension { - id: 3, - payload: Bytes::from_static(&[ - 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, - 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, - ]), - }, - ], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload: raw_pkt.slice(40..), - }; - - let dst_data = p.marshal()?; - assert_eq!( - dst_data, - raw_pkt[..], - "Marshal failed raw \nMarshaled: {dst_data:?}, \nraw_pkt:{raw_pkt:?}" - ); - - Ok(()) -} - -fn test_rfc8285_get_extension_returns_nil_when_extension_disabled() -> Result<()> { - let payload = Bytes::from_static(&[ - // Payload - 0x98u8, 0x36, 0xbe, 0x88, 0x9e, - ]); - - let p = Packet { - header: Header { - marker: true, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let res = p.header.get_extension(1); - assert!( - res.is_none(), - "Should return none on get_extension when header extension is false" - ); - - Ok(()) -} - -fn test_rfc8285_del_extension() -> Result<()> { - let payload = Bytes::from_static(&[ - // Payload - 0x98u8, 0x36, 0xbe, 0x88, 0x9e, - ]); - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let ext = p.header.get_extension(1); - assert!(ext.is_some(), "Extension should exist"); - - p.header.del_extension(1)?; - - let ext = p.header.get_extension(1); - assert!(ext.is_none(), "Extension should not exist"); - - let err = p.header.del_extension(1); - assert!( - err.is_err(), - "Should return error when deleting extension that doesnt exist" - ); - - Ok(()) -} - -fn test_rfc8285_get_extension_ids() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![ - Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }, - Extension { - id: 2, - payload: Bytes::from_static(&[0xBB]), - }, - ], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let ids = p.header.get_extension_ids(); - assert!(!ids.is_empty(), "Extensions should exist"); - - assert_eq!( - ids.len(), - p.header.extensions.len(), - "The number of IDs should be equal to the number of extensions, want={}, hanve{}", - ids.len(), - p.header.extensions.len() - ); - - for id in ids { - let ext = p.header.get_extension(id); - assert!(ext.is_some(), "Extension should exist for id: {id}") - } -} - -fn test_rfc8285_get_extension_ids_return_empty_when_extension_disabled() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let p = Packet { - header: Header { - marker: true, - extension: false, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let ids = p.header.get_extension_ids(); - assert!(ids.is_empty(), "Extensions should not exist"); -} - -fn test_rfc8285_del_extension_returns_error_when_extensions_disabled() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: false, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let ids = p.header.del_extension(1); - assert!( - ids.is_err(), - "Should return error on del_extension when header extension field is false" - ); -} - -fn test_rfc8285_one_byte_set_extension_should_enable_extension_when_adding() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: false, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let extension = Bytes::from_static(&[0xAAu8, 0xAA]); - let result = p.header.set_extension(1, extension.clone()); - assert!(result.is_ok(), "Error setting extension"); - - assert!(p.header.extension, "Extension should be set to true"); - assert_eq!( - p.header.extension_profile, 0xBEDE, - "Extension profile should be set to 0xBEDE" - ); - assert_eq!( - p.header.extensions.len(), - 1, - "Extensions len should be set to 1" - ); - assert_eq!( - p.header.get_extension(1), - Some(extension), - "Extension value is not set" - ) -} - -fn test_rfc8285_set_extension_should_set_correct_extension_profile_for_16_byte_extension() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: false, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let extension = Bytes::from_static(&[ - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, - ]); - - let res = p.header.set_extension(1, extension); - assert!(res.is_ok(), "Error setting extension"); - - assert_eq!( - p.header.extension_profile, 0xBEDE, - "Extension profile should be 0xBEDE" - ); -} - -fn test_rfc8285_set_extension_should_update_existing_extension() -> Result<()> { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - assert_eq!( - p.header.get_extension(1), - Some([0xAA][..].into()), - "Extension value not initialized properly" - ); - - let extension = Bytes::from_static(&[0xBBu8]); - p.header.set_extension(1, extension.clone())?; - - assert_eq!( - p.header.get_extension(1), - Some(extension), - "Extension value was not set" - ); - - Ok(()) -} - -fn test_rfc8285_one_byte_set_extension_should_error_when_invalid_id_provided() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - assert!( - p.header - .set_extension(0, Bytes::from_static(&[0xBBu8])) - .is_err(), - "set_extension did not error on invalid id" - ); - assert!( - p.header - .set_extension(15, Bytes::from_static(&[0xBBu8])) - .is_err(), - "set_extension did not error on invalid id" - ); -} - -fn test_rfc8285_one_byte_extension_terminate_processing_when_reserved_id_encountered() -> Result<()> -{ - let reserved_id_pkt = Bytes::from_static(&[ - 0x90u8, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0xBE, 0xDE, 0x00, - 0x01, 0xF0, 0xAA, 0x98, 0x36, 0xbe, 0x88, 0x9e, - ]); - - let p = Packet::unmarshal(&mut reserved_id_pkt.clone())?; - - assert_eq!( - p.header.extensions.len(), - 0, - "Extension should be empty for invalid ID" - ); - - let payload = reserved_id_pkt.slice(17..); - assert_eq!(p.payload, payload, "p.payload must be same as payload"); - - Ok(()) -} - -fn test_rfc8285_one_byte_set_extension_should_error_when_payload_too_large() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAAu8]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let res = p.header.set_extension( - 1, - Bytes::from_static(&[ - 0xBBu8, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, - ]), - ); - - assert!( - res.is_err(), - "set_extension did not error on too large payload" - ); -} - -fn test_rfc8285_two_bytes_set_extension_should_enable_extension_when_adding() -> Result<()> { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let extension = Bytes::from_static(&[ - 0xAAu8, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, - ]); - - p.header.set_extension(1, extension.clone())?; - - assert!(p.header.extension, "Extension should be set to true"); - assert_eq!( - p.header.extension_profile, 0x1000, - "Extension profile should be set to 0xBEDE" - ); - assert_eq!( - p.header.extensions.len(), - 1, - "Extensions should be set to 1" - ); - assert_eq!( - p.header.get_extension(1), - Some(extension), - "Extension value is not set" - ); - - Ok(()) -} - -fn test_rfc8285_two_byte_set_extension_should_update_existing_extension() -> Result<()> { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0x1000, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - assert_eq!( - p.header.get_extension(1), - Some(Bytes::from_static(&[0xAA])), - "Extension value not initialized properly" - ); - - let extension = Bytes::from_static(&[ - 0xBBu8, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, - ]); - - p.header.set_extension(1, extension.clone())?; - - assert_eq!(p.header.get_extension(1), Some(extension)); - - Ok(()) -} - -fn test_rfc8285_two_byte_set_extension_should_error_when_payload_too_large() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let res = p.header.set_extension( - 1, - Bytes::from_static(&[ - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, - 0xBB, 0xBB, 0xBB, 0xBB, - ]), - ); - - assert!( - res.is_err(), - "Set extension did not error on too large payload" - ); -} - -fn test_rfc3550_set_extension_should_error_when_non_zero() -> Result<()> { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0x1111, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0xAA]), - }], - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - p.header.set_extension(0, Bytes::from_static(&[0xBB]))?; - let res = p.header.get_extension(0); - assert_eq!( - res, - Some(Bytes::from_static(&[0xBB])), - "p.get_extension returned incorrect value" - ); - - Ok(()) -} - -fn test_rfc3550_set_extension_should_error_when_setting_non_zero_id() { - let payload = Bytes::from_static(&[0x98u8, 0x36, 0xbe, 0x88, 0x9e]); - - let mut p = Packet { - header: Header { - marker: true, - extension: true, - extension_profile: 0x1111, - version: 2, - payload_type: 96, - sequence_number: 27023, - timestamp: 3653407706, - ssrc: 476325762, - ..Default::default() - }, - payload, - ..Default::default() - }; - - let res = p.header.set_extension(1, Bytes::from_static(&[0xBB])); - assert!(res.is_err(), "set_extension did not error on invalid id"); -} - -use std::collections::HashMap; - -struct Cases { - input: Bytes, - err: Error, -} - -fn test_unmarshal_error_handling() { - let mut cases = HashMap::new(); - - cases.insert( - "ShortHeader", - Cases { - input: Bytes::from_static(&[ - 0x80, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, // timestamp - 0x1c, 0x64, 0x27, // SSRC (one byte missing) - ]), - err: Error::ErrHeaderSizeInsufficient, - }, - ); - - cases.insert( - "MissingCSRC", - Cases { - input: Bytes::from_static(&[ - 0x81, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, // timestamp - 0x1c, 0x64, 0x27, 0x82, // SSRC - ]), - err: Error::ErrHeaderSizeInsufficient, - }, - ); - - cases.insert( - "MissingExtension", - Cases { - input: Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, // timestamp - 0x1c, 0x64, 0x27, 0x82, // SSRC - ]), - err: Error::ErrHeaderSizeInsufficientForExtension, - }, - ); - - cases.insert( - "MissingExtensionData", - Cases { - input: Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, // timestamp - 0x1c, 0x64, 0x27, 0x82, // SSRC - 0xBE, 0xDE, 0x00, 0x03, // specified to have 3 extensions, but actually not - ]), - err: Error::ErrHeaderSizeInsufficientForExtension, - }, - ); - - cases.insert( - "MissingExtensionDataPayload", - Cases { - input: Bytes::from_static(&[ - 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, // timestamp - 0x1c, 0x64, 0x27, 0x82, // SSRC - 0xBE, 0xDE, 0x00, 0x01, // have 1 extension - 0x12, - 0x00, // length of the payload is expected to be 3, but actually have only 1 - ]), - err: Error::ErrHeaderSizeInsufficientForExtension, - }, - ); - - for (name, mut test_case) in cases.drain() { - let result = Header::unmarshal(&mut test_case.input); - let err = result.err().unwrap(); - assert_eq!( - test_case.err, err, - "Expected :{:?}, found: {:?} for testcase {}", - test_case.err, err, name - ) - } -} - -fn test_round_trip() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x00u8, 0x10, 0x23, 0x45, 0x12, 0x34, 0x45, 0x67, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, - 0x33, 0x44, 0x55, 0x66, 0x77, - ]); - - let payload = raw_pkt.slice(12..); - - let p = Packet::unmarshal(&mut raw_pkt.clone())?; - - assert_eq!( - payload, p.payload, - "p.payload must be same as payload.\n p.payload: {:?},\nraw_pkt: {:?}", - p.payload, payload - ); - - let buf = p.marshal()?; - - assert_eq!( - raw_pkt, buf, - "buf must be the same as raw_pkt. \n buf: {buf:?},\nraw_pkt: {raw_pkt:?}", - ); - assert_eq!( - payload, p.payload, - "p.payload must be the same as payload. \n payload: {:?},\np.payload: {:?}", - payload, p.payload, - ); - - Ok(()) -} diff --git a/rtp/src/packetizer/mod.rs b/rtp/src/packetizer/mod.rs deleted file mode 100644 index c925bc708..000000000 --- a/rtp/src/packetizer/mod.rs +++ /dev/null @@ -1,165 +0,0 @@ -#[cfg(test)] -mod packetizer_test; - -use std::fmt; -use std::sync::Arc; -use std::time::SystemTime; - -use bytes::{Bytes, BytesMut}; -use util::marshal::{Marshal, MarshalSize}; - -use crate::error::Result; -use crate::extension::abs_send_time_extension::*; -use crate::header::*; -use crate::packet::*; -use crate::sequence::*; - -/// Payloader payloads a byte array for use as rtp.Packet payloads -pub trait Payloader: fmt::Debug { - fn payload(&mut self, mtu: usize, b: &Bytes) -> Result>; - fn clone_to(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_to() - } -} - -/// Packetizer packetizes a payload -pub trait Packetizer: fmt::Debug { - fn enable_abs_send_time(&mut self, value: u8); - fn packetize(&mut self, payload: &Bytes, samples: u32) -> Result>; - fn skip_samples(&mut self, skipped_samples: u32); - fn clone_to(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_to() - } -} - -/// Depacketizer depacketizes a RTP payload, removing any RTP specific data from the payload -pub trait Depacketizer { - fn depacketize(&mut self, b: &Bytes) -> Result; - - /// Checks if the packet is at the beginning of a partition. This - /// should return false if the result could not be determined, in - /// which case the caller will detect timestamp discontinuities. - fn is_partition_head(&self, payload: &Bytes) -> bool; - - /// Checks if the packet is at the end of a partition. This should - /// return false if the result could not be determined. - fn is_partition_tail(&self, marker: bool, payload: &Bytes) -> bool; -} - -//TODO: SystemTime vs Instant? -// non-monotonic clock vs monotonically non-decreasing clock -/// FnTimeGen provides current SystemTime -pub type FnTimeGen = Arc SystemTime) + Send + Sync>; - -#[derive(Clone)] -pub(crate) struct PacketizerImpl { - pub(crate) mtu: usize, - pub(crate) payload_type: u8, - pub(crate) ssrc: u32, - pub(crate) payloader: Box, - pub(crate) sequencer: Box, - pub(crate) timestamp: u32, - pub(crate) clock_rate: u32, - pub(crate) abs_send_time: u8, //http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - pub(crate) time_gen: Option, -} - -impl fmt::Debug for PacketizerImpl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PacketizerImpl") - .field("mtu", &self.mtu) - .field("payload_type", &self.payload_type) - .field("ssrc", &self.ssrc) - .field("timestamp", &self.timestamp) - .field("clock_rate", &self.clock_rate) - .field("abs_send_time", &self.abs_send_time) - .finish() - } -} - -pub fn new_packetizer( - mtu: usize, - payload_type: u8, - ssrc: u32, - payloader: Box, - sequencer: Box, - clock_rate: u32, -) -> impl Packetizer { - PacketizerImpl { - mtu, - payload_type, - ssrc, - payloader, - sequencer, - timestamp: rand::random::(), - clock_rate, - abs_send_time: 0, - time_gen: None, - } -} - -impl Packetizer for PacketizerImpl { - fn enable_abs_send_time(&mut self, value: u8) { - self.abs_send_time = value - } - - fn packetize(&mut self, payload: &Bytes, samples: u32) -> Result> { - let payloads = self.payloader.payload(self.mtu - 12, payload)?; - let payloads_len = payloads.len(); - let mut packets = Vec::with_capacity(payloads_len); - for (i, payload) in payloads.into_iter().enumerate() { - packets.push(Packet { - header: Header { - version: 2, - padding: false, - extension: false, - marker: i == payloads_len - 1, - payload_type: self.payload_type, - sequence_number: self.sequencer.next_sequence_number(), - timestamp: self.timestamp, //TODO: Figure out how to do timestamps - ssrc: self.ssrc, - ..Default::default() - }, - payload, - }); - } - - self.timestamp = self.timestamp.wrapping_add(samples); - - if payloads_len != 0 && self.abs_send_time != 0 { - let st = if let Some(fn_time_gen) = &self.time_gen { - fn_time_gen() - } else { - SystemTime::now() - }; - let send_time = AbsSendTimeExtension::new(st); - //apply http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - let mut raw = BytesMut::with_capacity(send_time.marshal_size()); - raw.resize(send_time.marshal_size(), 0); - let _ = send_time.marshal_to(&mut raw)?; - packets[payloads_len - 1] - .header - .set_extension(self.abs_send_time, raw.freeze())?; - } - - Ok(packets) - } - - /// skip_samples causes a gap in sample count between Packetize requests so the - /// RTP payloads produced have a gap in timestamps - fn skip_samples(&mut self, skipped_samples: u32) { - self.timestamp = self.timestamp.wrapping_add(skipped_samples); - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/rtp/src/packetizer/packetizer_test.rs b/rtp/src/packetizer/packetizer_test.rs deleted file mode 100644 index 077658638..000000000 --- a/rtp/src/packetizer/packetizer_test.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::time::{Duration, UNIX_EPOCH}; - -use chrono::prelude::*; - -use super::*; -use crate::codecs::*; -use crate::error::Result; - -#[test] -fn test_packetizer() -> Result<()> { - let multiple_payload = Bytes::from_static(&[0; 128]); - let g722 = Box::new(g7xx::G722Payloader {}); - let seq = Box::new(new_random_sequencer()); - - //use the G722 payloader here, because it's very simple and all 0s is valid G722 data. - let mut packetizer = new_packetizer(100, 98, 0x1234ABCD, g722, seq, 90000); - let packets = packetizer.packetize(&multiple_payload, 2000)?; - - if packets.len() != 2 { - let mut packet_lengths = String::new(); - #[allow(clippy::needless_range_loop)] - for i in 0..packets.len() { - packet_lengths += - format!("Packet {} length {}\n", i, packets[i].payload.len()).as_str(); - } - panic!( - "Generated {} packets instead of 2\n{}", - packets.len(), - packet_lengths, - ); - } - Ok(()) -} - -#[test] -fn test_packetizer_abs_send_time() -> Result<()> { - let g722 = Box::new(g7xx::G722Payloader {}); - let sequencer = Box::new(new_fixed_sequencer(1234)); - - let time_gen: Option = Some(Arc::new(|| -> SystemTime { - let loc = FixedOffset::west_opt(5 * 60 * 60).unwrap(); // UTC-5 - let t = loc.with_ymd_and_hms(1985, 6, 23, 4, 0, 0).unwrap(); - UNIX_EPOCH - .checked_add(Duration::from_nanos(t.timestamp_nanos_opt().unwrap() as u64)) - .unwrap_or(UNIX_EPOCH) - })); - - //use the G722 payloader here, because it's very simple and all 0s is valid G722 data. - let mut pktizer = PacketizerImpl { - mtu: 100, - payload_type: 98, - ssrc: 0x1234ABCD, - payloader: g722, - sequencer, - timestamp: 45678, - clock_rate: 90000, - abs_send_time: 0, - time_gen, - }; - pktizer.enable_abs_send_time(1); - - let payload = Bytes::from_static(&[0x11, 0x12, 0x13, 0x14]); - let packets = pktizer.packetize(&payload, 2000)?; - - let expected = Packet { - header: Header { - version: 2, - padding: false, - extension: true, - marker: true, - payload_type: 98, - sequence_number: 1234, - timestamp: 45678, - ssrc: 0x1234ABCD, - csrc: vec![], - extension_profile: 0xBEDE, - extensions: vec![Extension { - id: 1, - payload: Bytes::from_static(&[0x40, 0, 0]), - }], - extensions_padding: 0, - }, - payload: Bytes::from_static(&[0x11, 0x12, 0x13, 0x14]), - }; - - if packets.len() != 1 { - panic!("Generated {} packets instead of 1", packets.len()) - } - - assert_eq!(packets[0], expected); - - Ok(()) -} - -#[test] -fn test_packetizer_timestamp_rollover_does_not_panic() -> Result<()> { - let g722 = Box::new(g7xx::G722Payloader {}); - let seq = Box::new(new_random_sequencer()); - - let payload = Bytes::from_static(&[0; 128]); - let mut packetizer = new_packetizer(100, 98, 0x1234ABCD, g722, seq, 90000); - - packetizer.packetize(&payload, 10)?; - - packetizer.packetize(&payload, u32::MAX)?; - - packetizer.skip_samples(u32::MAX); - - Ok(()) -} diff --git a/rtp/src/sequence.rs b/rtp/src/sequence.rs deleted file mode 100644 index bf61c29ed..000000000 --- a/rtp/src/sequence.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::fmt; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::{AtomicU16, AtomicU64}; - -/// Sequencer generates sequential sequence numbers for building RTP packets -pub trait Sequencer: fmt::Debug { - fn next_sequence_number(&self) -> u16; - fn roll_over_count(&self) -> u64; - fn clone_to(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_to() - } -} - -/// NewRandomSequencer returns a new sequencer starting from a random sequence -/// number -pub fn new_random_sequencer() -> impl Sequencer { - let c = Counters { - sequence_number: Arc::new(AtomicU16::new(rand::random::())), - roll_over_count: Arc::new(AtomicU64::new(0)), - }; - SequencerImpl(c) -} - -/// NewFixedSequencer returns a new sequencer starting from a specific -/// sequence number -pub fn new_fixed_sequencer(s: u16) -> impl Sequencer { - let sequence_number = if s == 0 { u16::MAX } else { s - 1 }; - - let c = Counters { - sequence_number: Arc::new(AtomicU16::new(sequence_number)), - roll_over_count: Arc::new(AtomicU64::new(0)), - }; - - SequencerImpl(c) -} - -#[derive(Debug, Clone)] -struct SequencerImpl(Counters); - -#[derive(Debug, Clone)] -struct Counters { - sequence_number: Arc, - roll_over_count: Arc, -} - -impl Sequencer for SequencerImpl { - /// NextSequenceNumber increment and returns a new sequence number for - /// building RTP packets - fn next_sequence_number(&self) -> u16 { - if self.0.sequence_number.load(Ordering::SeqCst) == u16::MAX { - self.0.roll_over_count.fetch_add(1, Ordering::SeqCst); - self.0.sequence_number.store(0, Ordering::SeqCst); - 0 - } else { - self.0.sequence_number.fetch_add(1, Ordering::SeqCst) + 1 - } - } - - /// RollOverCount returns the amount of times the 16bit sequence number - /// has wrapped - fn roll_over_count(&self) -> u64 { - self.0.roll_over_count.load(Ordering::SeqCst) - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/sctp/.gitignore b/sctp/.gitignore deleted file mode 100644 index 87ca02a58..000000000 --- a/sctp/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk \ No newline at end of file diff --git a/sctp/CHANGELOG.md b/sctp/CHANGELOG.md deleted file mode 100644 index ee7acd552..000000000 --- a/sctp/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# webrtc-sctp changelog - -## Unreleased - -* Use the new algorithm in crc crate for better throughput [#569](https://github.com/webrtc-rs/webrtc/pull/569) - -## v0.8.0 - -* Fix 'attempt to add with overflow' panic in dev profile [#393](https://github.com/webrtc-rs/webrtc/pull/393) -* Limit the bytes in the PendingQueue to avoid packets accumulating there uncontrollably [#367](https://github.com/webrtc-rs/webrtc/pull/367). -* Improve algorithm used to push to pending queue from O(n*log(n)) to O(log(n)) [#365](https://github.com/webrtc-rs/webrtc/pull/365). -* Reuse as many allocations as possible when marshaling [#364](https://github.com/webrtc-rs/webrtc/pull/364). -* The lock for the internal association was contended badly because marshaling was done while still in a critical section and also tokio was scheduling tasks badly [#363](https://github.com/webrtc-rs/webrtc/pull/363). - -### Breaking - -* Make `sctp::Stream::write` & `sctp::Stream::write_sctp` async again [#367](https://github.com/webrtc-rs/webrtc/pull/367). - -## v0.7.0 - -* Increased minimum support rust version to `1.60.0`. -* Do not loose data in `PollStream::poll_write` [#341](https://github.com/webrtc-rs/webrtc/pull/341). -* `PollStream::poll_shutdown`: make sure to flush any writes before shutting down [#340](https://github.com/webrtc-rs/webrtc/pull/340). -* Fixed a possible bug when adding chunks to pending queue [#345](https://github.com/webrtc-rs/webrtc/pull/345). -* Increased required `webrtc-util` version to `0.7.0`. - -### Breaking changes - -* Make `Stream::on_buffered_amount_low` function non-async [#338](https://github.com/webrtc-rs/webrtc/pull/338). -* Make `sctp::Stream::write` & `sctp::Stream::write_sctp` sync [#344](https://github.com/webrtc-rs/webrtc/pull/344). - -## v0.6.1 - -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). -* [#245 Fix incorrect chunk type Display for CWR](https://github.com/webrtc-rs/webrtc/pull/245) by [@k0nserv](https://github.com/k0nserv). - -## Prior to 0.6.1 - -Before 0.6.1 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/sctp/releases). diff --git a/sctp/Cargo.toml b/sctp/Cargo.toml deleted file mode 100644 index 061419603..000000000 --- a/sctp/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "webrtc-sctp" -version = "0.10.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of SCTP" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-sctp" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/sctp" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn"] } - -arc-swap = "1" -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -bytes = "1" -rand = "0.8" -crc = "3.2.1" -async-trait = "0.1" -log = "0.4" -thiserror = "1" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -lazy_static = "1" -env_logger = "0.10" -chrono = "0.4.28" -clap = "3" - -[[example]] -name = "ping" -path = "examples/ping.rs" -bench = false - -[[example]] -name = "pong" -path = "examples/pong.rs" -bench = false diff --git a/sctp/LICENSE-APACHE b/sctp/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/sctp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/sctp/LICENSE-MIT b/sctp/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/sctp/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sctp/README.md b/sctp/README.md deleted file mode 100644 index c856c7c14..000000000 --- a/sctp/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of SCTP. Rewrite Pion SCTP in Rust -

diff --git a/sctp/codecov.yml b/sctp/codecov.yml deleted file mode 100644 index ac2e61cff..000000000 --- a/sctp/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 9ec6d495-dfa7-4250-afeb-dbf342009340 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/sctp/doc/webrtc.rs.png b/sctp/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/sctp/examples/ping.rs b/sctp/examples/ping.rs deleted file mode 100644 index d8566ce39..000000000 --- a/sctp/examples/ping.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::net::Shutdown; -use std::sync::Arc; - -use bytes::Bytes; -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use tokio::signal; -use tokio::sync::mpsc; -use webrtc_sctp::association::*; -use webrtc_sctp::chunk::chunk_payload_data::PayloadProtocolIdentifier; -use webrtc_sctp::stream::*; -use webrtc_sctp::Error; - -// RUST_LOG=trace cargo run --color=always --package webrtc-sctp --example ping -- --server 0.0.0.0:5678 - -#[tokio::main] -async fn main() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let mut app = App::new("SCTP Ping") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of SCTP Client") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .long("server") - .help("SCTP Server name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); - conn.connect(server).await.unwrap(); - println!("connecting {server}.."); - - let config = Config { - net_conn: conn, - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }; - let a = Association::client(config).await?; - println!("created a client"); - - let stream = a.open_stream(0, PayloadProtocolIdentifier::String).await?; - println!("opened a stream"); - - // set unordered = true and 10ms threshold for dropping packets - stream.set_reliability_params(true, ReliabilityType::Timed, 10); - - let stream_tx = Arc::clone(&stream); - tokio::spawn(async move { - let mut ping_seq_num = 0; - while ping_seq_num < 10 { - let ping_msg = format!("ping {ping_seq_num}"); - println!("sent: {ping_msg}"); - stream_tx.write(&Bytes::from(ping_msg)).await?; - - ping_seq_num += 1; - } - - println!("finished send ping"); - Result::<(), Error>::Ok(()) - }); - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let stream_rx = Arc::clone(&stream); - tokio::spawn(async move { - let mut buff = vec![0u8; 1024]; - while let Ok(n) = stream_rx.read(&mut buff).await { - let pong_msg = String::from_utf8(buff[..n].to_vec()).unwrap(); - println!("received: {pong_msg}"); - } - - println!("finished recv pong"); - drop(done_tx); - }); - - println!("Waiting for Ctrl-C..."); - signal::ctrl_c().await.expect("failed to listen for event"); - println!("Closing stream and association..."); - - stream.shutdown(Shutdown::Both).await?; - a.close().await?; - - let _ = done_rx.recv().await; - - Ok(()) -} diff --git a/sctp/examples/pong.rs b/sctp/examples/pong.rs deleted file mode 100644 index c46b1b6db..000000000 --- a/sctp/examples/pong.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::net::Shutdown; -use std::sync::Arc; -use std::time::Duration; - -use bytes::Bytes; -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use tokio::signal; -use tokio::sync::mpsc; -use util::conn::conn_disconnected_packet::DisconnectedPacketConn; -use util::Conn; -use webrtc_sctp::association::*; -use webrtc_sctp::stream::*; -use webrtc_sctp::Error; - -// RUST_LOG=trace cargo run --color=always --package webrtc-sctp --example pong -- --host 0.0.0.0:5678 - -#[tokio::main] -async fn main() -> Result<(), Error> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let mut app = App::new("SCTP Pong") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of SCTP Server") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("host") - .required_unless("FULLHELP") - .takes_value(true) - .long("host") - .help("SCTP host name."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let host = matches.value_of("host").unwrap(); - let conn = DisconnectedPacketConn::new(Arc::new(UdpSocket::bind(host).await.unwrap())); - println!("listening {}...", conn.local_addr().unwrap()); - - let config = Config { - net_conn: Arc::new(conn), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "server".to_owned(), - }; - let a = Association::server(config).await?; - println!("created a server"); - - let stream = a.accept_stream().await.unwrap(); - println!("accepted a stream"); - - // set unordered = true and 10ms threshold for dropping packets - stream.set_reliability_params(true, ReliabilityType::Timed, 10); - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let stream2 = Arc::clone(&stream); - tokio::spawn(async move { - let mut buff = vec![0u8; 1024]; - while let Ok(n) = stream2.read(&mut buff).await { - let ping_msg = String::from_utf8(buff[..n].to_vec()).unwrap(); - println!("received: {ping_msg}"); - - let pong_msg = format!("pong [{ping_msg}]"); - println!("sent: {pong_msg}"); - stream2.write(&Bytes::from(pong_msg)).await?; - - tokio::time::sleep(Duration::from_secs(1)).await; - } - println!("finished ping-pong"); - drop(done_tx); - - Result::<(), Error>::Ok(()) - }); - - println!("Waiting for Ctrl-C..."); - signal::ctrl_c().await.expect("failed to listen for event"); - println!("Closing stream and association..."); - - stream.shutdown(Shutdown::Both).await?; - a.close().await?; - - let _ = done_rx.recv().await; - - Ok(()) -} diff --git a/sctp/examples/throughput.rs b/sctp/examples/throughput.rs deleted file mode 100644 index 31f2100e9..000000000 --- a/sctp/examples/throughput.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::io::Write; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use util::conn::conn_disconnected_packet::DisconnectedPacketConn; -use util::Conn; -use webrtc_sctp::association::*; -use webrtc_sctp::chunk::chunk_payload_data::PayloadProtocolIdentifier; -use webrtc_sctp::stream::*; -use webrtc_sctp::Error; - -fn main() -> Result<(), Error> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Warn) - .init(); - - let mut app = App::new("SCTP Throughput") - .version("0.1.0") - .about("An example of SCTP Server") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("port") - .required_unless("FULLHELP") - .takes_value(true) - .long("port") - .help("use port ."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let port1 = matches.value_of("port").unwrap().to_owned(); - let port2 = port1.clone(); - - std::thread::spawn(|| { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let conn = DisconnectedPacketConn::new(Arc::new( - UdpSocket::bind(format!("127.0.0.1:{port1}")).await.unwrap(), - )); - println!("listening {}...", conn.local_addr().unwrap()); - - let config = Config { - net_conn: Arc::new(conn), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "recver".to_owned(), - }; - let a = Association::server(config).await?; - println!("created a server"); - - let stream = a.accept_stream().await.unwrap(); - println!("accepted a stream"); - - // set unordered = true and 10ms threshold for dropping packets - stream.set_reliability_params(true, ReliabilityType::Rexmit, 0); - - let mut buff = [0u8; 65535]; - let mut recv = 0; - let mut pkt_num = 0; - let mut loop_num = 0; - let mut now = tokio::time::Instant::now(); - while let Ok(n) = stream.read(&mut buff).await { - recv += n; - if n != 0 { - pkt_num += 1; - } - loop_num += 1; - if now.elapsed().as_secs() == 1 { - println!("Throughput: {recv} Bytes/s, {pkt_num} pkts, {loop_num} loops"); - now = tokio::time::Instant::now(); - recv = 0; - loop_num = 0; - pkt_num = 0; - } - } - Result::<(), Error>::Ok(()) - }) - }); - - std::thread::spawn(|| { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); - conn.connect(format!("127.0.0.1:{port2}")).await.unwrap(); - println!("connecting 127.0.0.1:{port2}.."); - - let config = Config { - net_conn: conn, - max_receive_buffer_size: 0, - max_message_size: 0, - name: "sender".to_owned(), - }; - let a = Association::client(config).await.unwrap(); - println!("created a client"); - - let stream = a - .open_stream(0, PayloadProtocolIdentifier::Binary) - .await - .unwrap(); - println!("opened a stream"); - - //const LEN: usize = 1200; - const LEN: usize = 65535; - let buf = vec![0; LEN]; - let bytes = bytes::Bytes::from(buf); - - let mut now = tokio::time::Instant::now(); - let mut pkt_num = 0; - while stream.write(&bytes).await.is_ok() { - pkt_num += 1; - if now.elapsed().as_secs() == 1 { - println!("Send {pkt_num} pkts"); - now = tokio::time::Instant::now(); - pkt_num = 0; - } - } - Result::<(), Error>::Ok(()) - }) - }); - #[allow(clippy::empty_loop)] - loop {} -} diff --git a/sctp/fuzz/.gitignore b/sctp/fuzz/.gitignore deleted file mode 100644 index 80894b1a2..000000000 --- a/sctp/fuzz/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -corpus diff --git a/sctp/fuzz/Cargo.toml b/sctp/fuzz/Cargo.toml deleted file mode 100644 index e22f7d11f..000000000 --- a/sctp/fuzz/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "webrtc-sctp-fuzz" -version = "0.0.0" -authors = ["Automatically generated"] -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" -bytes = "*" - -[dependencies.webrtc-sctp] -path = ".." - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] - -[[bin]] -name = "packet" -path = "fuzz_targets/packet.rs" -test = false -doc = false - -[[bin]] -name = "param" -path = "fuzz_targets/param.rs" -test = false -doc = false diff --git a/sctp/fuzz/artifacts/packet/crash-16cad30042bc4791bd62c630a780add5d1220779 b/sctp/fuzz/artifacts/packet/crash-16cad30042bc4791bd62c630a780add5d1220779 deleted file mode 100644 index 2f6df735be0cb447c0338e6f2ad225df47a6408f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 ocmd0_*VoPD3UiwpVOPz}#J~Xr|Nr0rkHq}{pYuNmFft%m05bp?!T(AmBe>TW!N|Y>VPI26Pz>rh4Gl&JON)U401P}L*Z=?k diff --git a/sctp/fuzz/artifacts/packet/crash-8d90dfc8fc34fa06f161f69617ee8f48dec434cd b/sctp/fuzz/artifacts/packet/crash-8d90dfc8fc34fa06f161f69617ee8f48dec434cd deleted file mode 100644 index 92d545a3d893f469347faec3084323e4b63b1529..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85 zcmd0F)z{7B3Uf1E87;%i#2}T)00kUCn$ZOUfXv?vGl1aM42bOiET}3V#Q_BW|KI;V N?f-wy{~*A~008p%7pDLK diff --git a/sctp/fuzz/artifacts/packet/crash-b836a20af7f8af85423dbe80565465b16bb7a16f b/sctp/fuzz/artifacts/packet/crash-b836a20af7f8af85423dbe80565465b16bb7a16f deleted file mode 100644 index bd51fac255355b98bdc73fbc39349ddf78d95c65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85 zcmdPrY6AiWhDW#l*m24-xIh6&NFOStz`mu4fq~&WGN=G^s*r^s?8Bd2zJCV*sy8#; diff --git a/sctp/fuzz/artifacts/packet/crash-f940d9879efc88872145955bae11ca6ad6a4c044 b/sctp/fuzz/artifacts/packet/crash-f940d9879efc88872145955bae11ca6ad6a4c044 deleted file mode 100644 index 745cf533b4fbdd78e15033fb246d6329794a99df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmdPp3OksPo|!4^ocw@+fdR;KYHM4~@Lx&_B)TIqiIbtN4Je+O>6!Vz?f-uU1}P~i qkQk6^YXd5S0C_NF03t, - pub(crate) max_message_size: Arc, - pub(crate) inflight_queue_length: Arc, - pub(crate) will_send_shutdown: Arc, - awake_write_loop_ch: Option>>, - - peer_verification_tag: u32, - pub(crate) my_verification_tag: u32, - - pub(crate) my_next_tsn: u32, // nextTSN - peer_last_tsn: u32, // lastRcvdTSN - min_tsn2measure_rtt: u32, // for RTT measurement - will_send_forward_tsn: bool, - will_retransmit_fast: bool, - will_retransmit_reconfig: bool, - - will_send_shutdown_ack: bool, - will_send_shutdown_complete: bool, - - // Reconfig - my_next_rsn: u32, - reconfigs: HashMap, - reconfig_requests: HashMap, - - // Non-RFC internal data - source_port: u16, - destination_port: u16, - pub(crate) my_max_num_inbound_streams: u16, - pub(crate) my_max_num_outbound_streams: u16, - my_cookie: Option, - payload_queue: PayloadQueue, - inflight_queue: PayloadQueue, - pending_queue: Arc, - control_queue: ControlQueue, - pub(crate) mtu: u32, - max_payload_size: u32, // max DATA chunk payload size - cumulative_tsn_ack_point: u32, - advanced_peer_tsn_ack_point: u32, - use_forward_tsn: bool, - - // Congestion control parameters - pub(crate) max_receive_buffer_size: u32, - pub(crate) cwnd: u32, // my congestion window size - rwnd: u32, // calculated peer's receiver windows size - pub(crate) ssthresh: u32, // slow start threshold - partial_bytes_acked: u32, - pub(crate) in_fast_recovery: bool, - fast_recover_exit_point: u32, - - // RTX & Ack timer - pub(crate) rto_mgr: RtoManager, - pub(crate) t1init: Option>, - pub(crate) t1cookie: Option>, - pub(crate) t2shutdown: Option>, - pub(crate) t3rtx: Option>, - pub(crate) treconfig: Option>, - pub(crate) ack_timer: Option>, - - // Chunks stored for retransmission - pub(crate) stored_init: Option, - stored_cookie_echo: Option, - - streams: HashMap>, - - close_loop_ch_tx: Option>, - accept_ch_tx: Option>>, - handshake_completed_ch_tx: Option>>, - - // local error - silent_error: Option, - - // per inbound packet context - delayed_ack_triggered: bool, - immediate_ack_triggered: bool, - - pub(crate) stats: Arc, - ack_state: AckState, - pub(crate) ack_mode: AckMode, // for testing -} - -impl AssociationInternal { - pub(crate) fn new( - config: Config, - close_loop_ch_tx: broadcast::Sender<()>, - accept_ch_tx: mpsc::Sender>, - handshake_completed_ch_tx: mpsc::Sender>, - awake_write_loop_ch: Arc>, - ) -> Self { - let max_receive_buffer_size = if config.max_receive_buffer_size == 0 { - INITIAL_RECV_BUF_SIZE - } else { - config.max_receive_buffer_size - }; - - let max_message_size = if config.max_message_size == 0 { - DEFAULT_MAX_MESSAGE_SIZE - } else { - config.max_message_size - }; - - let inflight_queue_length = Arc::new(AtomicUsize::new(0)); - - let mut tsn = random::(); - if tsn == 0 { - tsn += 1; - } - let mut a = AssociationInternal { - name: config.name, - max_receive_buffer_size, - max_message_size: Arc::new(AtomicU32::new(max_message_size)), - - my_max_num_outbound_streams: u16::MAX, - my_max_num_inbound_streams: u16::MAX, - payload_queue: PayloadQueue::new(Arc::new(AtomicUsize::new(0))), - inflight_queue: PayloadQueue::new(Arc::clone(&inflight_queue_length)), - inflight_queue_length, - pending_queue: Arc::new(PendingQueue::new()), - control_queue: ControlQueue::new(), - mtu: INITIAL_MTU, - max_payload_size: INITIAL_MTU - (COMMON_HEADER_SIZE + DATA_CHUNK_HEADER_SIZE), - my_verification_tag: random::(), - my_next_tsn: tsn, - my_next_rsn: tsn, - min_tsn2measure_rtt: tsn, - state: Arc::new(AtomicU8::new(AssociationState::Closed as u8)), - rto_mgr: RtoManager::new(), - streams: HashMap::new(), - reconfigs: HashMap::new(), - reconfig_requests: HashMap::new(), - accept_ch_tx: Some(accept_ch_tx), - close_loop_ch_tx: Some(close_loop_ch_tx), - handshake_completed_ch_tx: Some(handshake_completed_ch_tx), - cumulative_tsn_ack_point: tsn - 1, - advanced_peer_tsn_ack_point: tsn - 1, - silent_error: Some(Error::ErrSilentlyDiscard), - stats: Arc::new(AssociationStats::default()), - awake_write_loop_ch: Some(awake_write_loop_ch), - ..Default::default() - }; - - // RFC 4690 Sec 7.2.1 - // o The initial cwnd before DATA transmission or after a sufficiently - // long idle period MUST be set to min(4*MTU, max (2*MTU, 4380 - // bytes)). - // TODO: Consider whether this should use `clamp` - #[allow(clippy::manual_clamp)] - { - a.cwnd = std::cmp::min(4 * a.mtu, std::cmp::max(2 * a.mtu, 4380)); - } - log::trace!( - "[{}] updated cwnd={} ssthresh={} inflight={} (INI)", - a.name, - a.cwnd, - a.ssthresh, - a.inflight_queue.get_num_bytes() - ); - - a - } - - /// caller must hold self.lock - pub(crate) fn send_init(&mut self) -> Result<()> { - if let Some(stored_init) = self.stored_init.clone() { - log::debug!("[{}] sending INIT", self.name); - - self.source_port = 5000; // Spec?? - self.destination_port = 5000; // Spec?? - - let outbound = Packet { - source_port: self.source_port, - destination_port: self.destination_port, - verification_tag: 0, - chunks: vec![Box::new(stored_init)], - }; - - self.control_queue.push_back(outbound); - self.awake_write_loop(); - - Ok(()) - } else { - Err(Error::ErrInitNotStoredToSend) - } - } - - /// caller must hold self.lock - fn send_cookie_echo(&mut self) -> Result<()> { - if let Some(stored_cookie_echo) = &self.stored_cookie_echo { - log::debug!("[{}] sending COOKIE-ECHO", self.name); - - let outbound = Packet { - source_port: self.source_port, - destination_port: self.destination_port, - verification_tag: self.peer_verification_tag, - chunks: vec![Box::new(stored_cookie_echo.clone())], - }; - - self.control_queue.push_back(outbound); - self.awake_write_loop(); - Ok(()) - } else { - Err(Error::ErrCookieEchoNotStoredToSend) - } - } - - pub(crate) async fn close(&mut self) -> Result<()> { - if self.get_state() != AssociationState::Closed { - self.set_state(AssociationState::Closed); - - log::debug!("[{}] closing association..", self.name); - - self.close_all_timers().await; - - // awake read/write_loop to exit - self.close_loop_ch_tx.take(); - - for si in self.streams.keys().cloned().collect::>() { - self.unregister_stream(si); - } - - // Wait for read_loop to end - //if let Some(read_loop_close_ch) = &mut self.read_loop_close_ch { - // let _ = read_loop_close_ch.recv().await; - //} - - log::debug!("[{}] association closed", self.name); - log::debug!( - "[{}] stats nDATAs (in) : {}", - self.name, - self.stats.get_num_datas() - ); - log::debug!( - "[{}] stats nSACKs (in) : {}", - self.name, - self.stats.get_num_sacks() - ); - log::debug!( - "[{}] stats nT3Timeouts : {}", - self.name, - self.stats.get_num_t3timeouts() - ); - log::debug!( - "[{}] stats nAckTimeouts: {}", - self.name, - self.stats.get_num_ack_timeouts() - ); - log::debug!( - "[{}] stats nFastRetrans: {}", - self.name, - self.stats.get_num_fast_retrans() - ); - } - - Ok(()) - } - - async fn close_all_timers(&mut self) { - // Close all retransmission & ack timers - if let Some(t1init) = &self.t1init { - t1init.stop().await; - } - if let Some(t1cookie) = &self.t1cookie { - t1cookie.stop().await; - } - if let Some(t2shutdown) = &self.t2shutdown { - t2shutdown.stop().await; - } - if let Some(t3rtx) = &self.t3rtx { - t3rtx.stop().await; - } - if let Some(treconfig) = &self.treconfig { - treconfig.stop().await; - } - if let Some(ack_timer) = &mut self.ack_timer { - ack_timer.stop(); - } - } - - fn awake_write_loop(&self) { - //log::debug!("[{}] awake_write_loop_ch.notify_one", self.name); - if let Some(awake_write_loop_ch) = &self.awake_write_loop_ch { - let _ = awake_write_loop_ch.try_send(()); - } - } - - /// unregister_stream un-registers a stream from the association - /// The caller should hold the association write lock. - fn unregister_stream(&mut self, stream_identifier: u16) { - let s = self.streams.remove(&stream_identifier); - if let Some(s) = s { - // NOTE: shutdown is not used here because it resets the stream. - if !s.read_shutdown.swap(true, Ordering::SeqCst) { - s.read_notifier.notify_waiters(); - } - s.write_shutdown.store(true, Ordering::SeqCst); - } - } - - /// handle_inbound parses incoming raw packets - pub(crate) async fn handle_inbound(&mut self, raw: &Bytes) -> Result<()> { - let p = match Packet::unmarshal(raw) { - Ok(p) => p, - Err(err) => { - log::warn!("[{}] unable to parse SCTP packet {}", self.name, err); - return Ok(()); - } - }; - - if let Err(err) = p.check_packet() { - log::warn!("[{}] failed validating packet {}", self.name, err); - return Ok(()); - } - - self.handle_chunk_start(); - - for c in &p.chunks { - self.handle_chunk(&p, c).await?; - } - - self.handle_chunk_end(); - Ok(()) - } - - fn gather_data_packets_to_retransmit(&mut self, mut raw_packets: Vec) -> Vec { - for p in self.get_data_packets_to_retransmit() { - raw_packets.push(p); - } - - raw_packets - } - - async fn gather_outbound_data_and_reconfig_packets( - &mut self, - mut raw_packets: Vec, - ) -> Vec { - // Pop unsent data chunks from the pending queue to send as much as - // cwnd and rwnd allow. - let (chunks, sis_to_reset) = self.pop_pending_data_chunks_to_send().await; - if !chunks.is_empty() { - // Start timer. (noop if already started) - log::trace!("[{}] T3-rtx timer start (pt1)", self.name); - if let Some(t3rtx) = &self.t3rtx { - t3rtx.start(self.rto_mgr.get_rto()).await; - } - for p in self.bundle_data_chunks_into_packets(chunks) { - raw_packets.push(p); - } - } - - if !sis_to_reset.is_empty() || self.will_retransmit_reconfig { - if self.will_retransmit_reconfig { - self.will_retransmit_reconfig = false; - log::debug!( - "[{}] retransmit {} RECONFIG chunk(s)", - self.name, - self.reconfigs.len() - ); - for c in self.reconfigs.values() { - let p = self.create_packet(vec![Box::new(c.clone())]); - raw_packets.push(p); - } - } - - if !sis_to_reset.is_empty() { - let rsn = self.generate_next_rsn(); - let tsn = self.my_next_tsn - 1; - log::debug!( - "[{}] sending RECONFIG: rsn={} tsn={} streams={:?}", - self.name, - rsn, - self.my_next_tsn - 1, - sis_to_reset - ); - - let c = ChunkReconfig { - param_a: Some(Box::new(ParamOutgoingResetRequest { - reconfig_request_sequence_number: rsn, - sender_last_tsn: tsn, - stream_identifiers: sis_to_reset, - ..Default::default() - })), - ..Default::default() - }; - self.reconfigs.insert(rsn, c.clone()); // store in the map for retransmission - - let p = self.create_packet(vec![Box::new(c)]); - raw_packets.push(p); - } - - if !self.reconfigs.is_empty() { - if let Some(treconfig) = &self.treconfig { - treconfig.start(self.rto_mgr.get_rto()).await; - } - } - } - - raw_packets - } - - fn gather_outbound_fast_retransmission_packets( - &mut self, - mut raw_packets: Vec, - ) -> Vec { - if self.will_retransmit_fast { - self.will_retransmit_fast = false; - - let mut to_fast_retrans: Vec> = vec![]; - let mut fast_retrans_size = COMMON_HEADER_SIZE; - - let mut i = 0; - loop { - let tsn = self.cumulative_tsn_ack_point + i + 1; - if let Some(c) = self.inflight_queue.get_mut(tsn) { - if c.acked || c.abandoned() || c.nsent > 1 || c.miss_indicator < 3 { - i += 1; - continue; - } - - // RFC 4960 Sec 7.2.4 Fast Retransmit on Gap Reports - // 3) Determine how many of the earliest (i.e., lowest TSN) DATA chunks - // marked for retransmission will fit into a single packet, subject - // to constraint of the path MTU of the destination transport - // address to which the packet is being sent. Call this value K. - // Retransmit those K DATA chunks in a single packet. When a Fast - // Retransmit is being performed, the sender SHOULD ignore the value - // of cwnd and SHOULD NOT delay retransmission for this single - // packet. - - let data_chunk_size = DATA_CHUNK_HEADER_SIZE + c.user_data.len() as u32; - if self.mtu < fast_retrans_size + data_chunk_size { - break; - } - - fast_retrans_size += data_chunk_size; - self.stats.inc_fast_retrans(); - c.nsent += 1; - } else { - break; // end of pending data - } - - if let Some(c) = self.inflight_queue.get(tsn) { - self.check_partial_reliability_status(c); - to_fast_retrans.push(Box::new(c.clone())); - log::trace!( - "[{}] fast-retransmit: tsn={} sent={} htna={}", - self.name, - c.tsn, - c.nsent, - self.fast_recover_exit_point - ); - } - i += 1; - } - - if !to_fast_retrans.is_empty() { - let p = self.create_packet(to_fast_retrans); - raw_packets.push(p); - } - } - - raw_packets - } - - async fn gather_outbound_sack_packets(&mut self, mut raw_packets: Vec) -> Vec { - if self.ack_state == AckState::Immediate { - self.ack_state = AckState::Idle; - let sack = self.create_selective_ack_chunk().await; - log::debug!("[{}] sending SACK: {}", self.name, sack); - let p = self.create_packet(vec![Box::new(sack)]); - raw_packets.push(p); - } - - raw_packets - } - - fn gather_outbound_forward_tsn_packets(&mut self, mut raw_packets: Vec) -> Vec { - /*log::debug!( - "[{}] gatherOutboundForwardTSNPackets {}", - self.name, - self.will_send_forward_tsn - );*/ - if self.will_send_forward_tsn { - self.will_send_forward_tsn = false; - if sna32gt( - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point, - ) { - let fwd_tsn = self.create_forward_tsn(); - let p = self.create_packet(vec![Box::new(fwd_tsn)]); - raw_packets.push(p); - } - } - - raw_packets - } - - async fn gather_outbound_shutdown_packets( - &mut self, - mut raw_packets: Vec, - ) -> (Vec, bool) { - let mut ok = true; - - if self.will_send_shutdown.load(Ordering::SeqCst) { - self.will_send_shutdown.store(false, Ordering::SeqCst); - - let shutdown = ChunkShutdown { - cumulative_tsn_ack: self.cumulative_tsn_ack_point, - }; - - let p = self.create_packet(vec![Box::new(shutdown)]); - if let Some(t2shutdown) = &self.t2shutdown { - t2shutdown.start(self.rto_mgr.get_rto()).await; - } - raw_packets.push(p); - } else if self.will_send_shutdown_ack { - self.will_send_shutdown_ack = false; - - let shutdown_ack = ChunkShutdownAck {}; - - let p = self.create_packet(vec![Box::new(shutdown_ack)]); - if let Some(t2shutdown) = &self.t2shutdown { - t2shutdown.start(self.rto_mgr.get_rto()).await; - } - raw_packets.push(p); - } else if self.will_send_shutdown_complete { - self.will_send_shutdown_complete = false; - - let shutdown_complete = ChunkShutdownComplete {}; - ok = false; - let p = self.create_packet(vec![Box::new(shutdown_complete)]); - - raw_packets.push(p); - } - - (raw_packets, ok) - } - - /// gather_outbound gathers outgoing packets. The returned bool value set to - /// false means the association should be closed down after the final send. - pub(crate) async fn gather_outbound(&mut self) -> (Vec, bool) { - let mut raw_packets = Vec::with_capacity(16); - - if !self.control_queue.is_empty() { - for p in self.control_queue.drain(..) { - raw_packets.push(p); - } - } - - let state = self.get_state(); - match state { - AssociationState::Established => { - raw_packets = self.gather_data_packets_to_retransmit(raw_packets); - raw_packets = self - .gather_outbound_data_and_reconfig_packets(raw_packets) - .await; - raw_packets = self.gather_outbound_fast_retransmission_packets(raw_packets); - raw_packets = self.gather_outbound_sack_packets(raw_packets).await; - raw_packets = self.gather_outbound_forward_tsn_packets(raw_packets); - (raw_packets, true) - } - AssociationState::ShutdownPending - | AssociationState::ShutdownSent - | AssociationState::ShutdownReceived => { - raw_packets = self.gather_data_packets_to_retransmit(raw_packets); - raw_packets = self.gather_outbound_fast_retransmission_packets(raw_packets); - raw_packets = self.gather_outbound_sack_packets(raw_packets).await; - self.gather_outbound_shutdown_packets(raw_packets).await - } - AssociationState::ShutdownAckSent => { - self.gather_outbound_shutdown_packets(raw_packets).await - } - _ => (raw_packets, true), - } - } - - /// set_state atomically sets the state of the Association. - pub(crate) fn set_state(&self, new_state: AssociationState) { - let old_state = AssociationState::from(self.state.swap(new_state as u8, Ordering::SeqCst)); - if new_state != old_state { - log::debug!( - "[{}] state change: '{}' => '{}'", - self.name, - old_state, - new_state, - ); - } - } - - /// get_state atomically returns the state of the Association. - fn get_state(&self) -> AssociationState { - self.state.load(Ordering::SeqCst).into() - } - - async fn handle_init(&mut self, p: &Packet, i: &ChunkInit) -> Result> { - let state = self.get_state(); - log::debug!("[{}] chunkInit received in state '{}'", self.name, state); - - // https://tools.ietf.org/html/rfc4960#section-5.2.1 - // Upon receipt of an INIT in the COOKIE-WAIT state, an endpoint MUST - // respond with an INIT ACK using the same parameters it sent in its - // original INIT chunk (including its Initiate Tag, unchanged). When - // responding, the endpoint MUST send the INIT ACK back to the same - // address that the original INIT (sent by this endpoint) was sent. - - if state != AssociationState::Closed - && state != AssociationState::CookieWait - && state != AssociationState::CookieEchoed - { - // 5.2.2. Unexpected INIT in States Other than CLOSED, COOKIE-ECHOED, - // COOKIE-WAIT, and SHUTDOWN-ACK-SENT - return Err(Error::ErrHandleInitState); - } - - // Should we be setting any of these permanently until we've ACKed further? - self.my_max_num_inbound_streams = - std::cmp::min(i.num_inbound_streams, self.my_max_num_inbound_streams); - self.my_max_num_outbound_streams = - std::cmp::min(i.num_outbound_streams, self.my_max_num_outbound_streams); - self.peer_verification_tag = i.initiate_tag; - self.source_port = p.destination_port; - self.destination_port = p.source_port; - - // 13.2 This is the last TSN received in sequence. This value - // is set initially by taking the peer's initial TSN, - // received in the INIT or INIT ACK chunk, and - // subtracting one from it. - self.peer_last_tsn = if i.initial_tsn == 0 { - u32::MAX - } else { - i.initial_tsn - 1 - }; - - for param in &i.params { - if let Some(v) = param.as_any().downcast_ref::() { - for t in &v.chunk_types { - if *t == CT_FORWARD_TSN { - log::debug!("[{}] use ForwardTSN (on init)", self.name); - self.use_forward_tsn = true; - } - } - } - } - if !self.use_forward_tsn { - log::warn!("[{}] not using ForwardTSN (on init)", self.name); - } - - let mut outbound = Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - ..Default::default() - }; - - // According to RFC https://datatracker.ietf.org/doc/html/rfc4960#section-3.2.2 - // We report unknown parameters with a paramtype with bit 14 set as unrecognized - let unrecognized_params_from_init = i - .params - .iter() - .filter_map(|param| { - if let ParamType::Unknown { param_type } = param.header().typ { - let needs_to_be_reported = ((param_type >> 14) & 0x01) == 1; - if needs_to_be_reported { - let wrapped: Box = - Box::new(ParamUnrecognized::wrap(param.clone())); - Some(wrapped) - } else { - None - } - } else { - None - } - }) - .collect(); - - let mut init_ack = ChunkInit { - is_ack: true, - initial_tsn: self.my_next_tsn, - num_outbound_streams: self.my_max_num_outbound_streams, - num_inbound_streams: self.my_max_num_inbound_streams, - initiate_tag: self.my_verification_tag, - advertised_receiver_window_credit: self.max_receive_buffer_size, - params: unrecognized_params_from_init, - }; - - if self.my_cookie.is_none() { - self.my_cookie = Some(ParamStateCookie::new()); - } - - if let Some(my_cookie) = &self.my_cookie { - init_ack.params = vec![Box::new(my_cookie.clone())]; - } - - init_ack.set_supported_extensions(); - - outbound.chunks = vec![Box::new(init_ack)]; - - Ok(vec![outbound]) - } - - async fn handle_init_ack(&mut self, p: &Packet, i: &ChunkInit) -> Result> { - let state = self.get_state(); - log::debug!("[{}] chunkInitAck received in state '{}'", self.name, state); - if state != AssociationState::CookieWait { - // RFC 4960 - // 5.2.3. Unexpected INIT ACK - // If an INIT ACK is received by an endpoint in any state other than the - // COOKIE-WAIT state, the endpoint should discard the INIT ACK chunk. - // An unexpected INIT ACK usually indicates the processing of an old or - // duplicated INIT chunk. - return Ok(vec![]); - } - - self.my_max_num_inbound_streams = - std::cmp::min(i.num_inbound_streams, self.my_max_num_inbound_streams); - self.my_max_num_outbound_streams = - std::cmp::min(i.num_outbound_streams, self.my_max_num_outbound_streams); - self.peer_verification_tag = i.initiate_tag; - self.peer_last_tsn = if i.initial_tsn == 0 { - u32::MAX - } else { - i.initial_tsn - 1 - }; - if self.source_port != p.destination_port || self.destination_port != p.source_port { - log::warn!("[{}] handle_init_ack: port mismatch", self.name); - return Ok(vec![]); - } - - self.rwnd = i.advertised_receiver_window_credit; - log::debug!("[{}] initial rwnd={}", self.name, self.rwnd); - - // RFC 4690 Sec 7.2.1 - // o The initial value of ssthresh MAY be arbitrarily high (for - // example, implementations MAY use the size of the receiver - // advertised window). - self.ssthresh = self.rwnd; - log::trace!( - "[{}] updated cwnd={} ssthresh={} inflight={} (INI)", - self.name, - self.cwnd, - self.ssthresh, - self.inflight_queue.get_num_bytes() - ); - - if let Some(t1init) = &self.t1init { - t1init.stop().await; - } - self.stored_init = None; - - let mut cookie_param = None; - for param in &i.params { - if let Some(v) = param.as_any().downcast_ref::() { - cookie_param = Some(v); - } else if let Some(v) = param.as_any().downcast_ref::() { - for t in &v.chunk_types { - if *t == CT_FORWARD_TSN { - log::debug!("[{}] use ForwardTSN (on initAck)", self.name); - self.use_forward_tsn = true; - } - } - } else if param - .as_any() - .downcast_ref::() - .is_some() - { - self.use_forward_tsn = true; - } - } - if !self.use_forward_tsn { - log::warn!("[{}] not using ForwardTSN (on initAck)", self.name); - } - - if let Some(v) = cookie_param { - self.stored_cookie_echo = Some(ChunkCookieEcho { - cookie: v.cookie.clone(), - }); - - self.send_cookie_echo()?; - - if let Some(t1cookie) = &self.t1cookie { - t1cookie.start(self.rto_mgr.get_rto()).await; - } - - self.set_state(AssociationState::CookieEchoed); - - Ok(vec![]) - } else { - Err(Error::ErrInitAckNoCookie) - } - } - - async fn handle_heartbeat(&self, c: &ChunkHeartbeat) -> Result> { - log::trace!("[{}] chunkHeartbeat", self.name); - if let Some(p) = c.params.first() { - if let Some(hbi) = p.as_any().downcast_ref::() { - return Ok(vec![Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks: vec![Box::new(ChunkHeartbeatAck { - params: vec![Box::new(ParamHeartbeatInfo { - heartbeat_information: hbi.heartbeat_information.clone(), - })], - })], - }]); - } else { - log::warn!( - "[{}] failed to handle Heartbeat, no ParamHeartbeatInfo", - self.name, - ); - } - } - - Ok(vec![]) - } - - async fn handle_cookie_echo(&mut self, c: &ChunkCookieEcho) -> Result> { - let state = self.get_state(); - log::debug!("[{}] COOKIE-ECHO received in state '{}'", self.name, state); - - if let Some(my_cookie) = &self.my_cookie { - match state { - AssociationState::Established => { - if my_cookie.cookie != c.cookie { - return Ok(vec![]); - } - } - AssociationState::Closed - | AssociationState::CookieWait - | AssociationState::CookieEchoed => { - if my_cookie.cookie != c.cookie { - return Ok(vec![]); - } - - if let Some(t1init) = &self.t1init { - t1init.stop().await; - } - self.stored_init = None; - - if let Some(t1cookie) = &self.t1cookie { - t1cookie.stop().await; - } - self.stored_cookie_echo = None; - - self.set_state(AssociationState::Established); - if let Some(handshake_completed_ch) = &self.handshake_completed_ch_tx { - let _ = handshake_completed_ch.send(None).await; - } - } - _ => return Ok(vec![]), - }; - } else { - log::debug!("[{}] COOKIE-ECHO received before initialization", self.name); - return Ok(vec![]); - } - - Ok(vec![Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks: vec![Box::new(ChunkCookieAck {})], - }]) - } - - async fn handle_cookie_ack(&mut self) -> Result> { - let state = self.get_state(); - log::debug!("[{}] COOKIE-ACK received in state '{}'", self.name, state); - if state != AssociationState::CookieEchoed { - // RFC 4960 - // 5.2.5. Handle Duplicate COOKIE-ACK. - // At any state other than COOKIE-ECHOED, an endpoint should silently - // discard a received COOKIE ACK chunk. - return Ok(vec![]); - } - - if let Some(t1cookie) = &self.t1cookie { - t1cookie.stop().await; - } - self.stored_cookie_echo = None; - - self.set_state(AssociationState::Established); - if let Some(handshake_completed_ch) = &self.handshake_completed_ch_tx { - let _ = handshake_completed_ch.send(None).await; - } - - Ok(vec![]) - } - - async fn handle_data(&mut self, d: &ChunkPayloadData) -> Result> { - log::trace!( - "[{}] DATA: tsn={} immediateSack={} len={}", - self.name, - d.tsn, - d.immediate_sack, - d.user_data.len() - ); - self.stats.inc_datas(); - - let can_push = self.payload_queue.can_push(d, self.peer_last_tsn); - let mut stream_handle_data = false; - if can_push { - if let Some(_s) = self.get_or_create_stream(d.stream_identifier) { - if self.get_my_receiver_window_credit().await > 0 { - // Pass the new chunk to stream level as soon as it arrives - self.payload_queue.push(d.clone(), self.peer_last_tsn); - stream_handle_data = true; - } else { - // Receive buffer is full - if let Some(last_tsn) = self.payload_queue.get_last_tsn_received() { - if sna32lt(d.tsn, *last_tsn) { - log::debug!("[{}] receive buffer full, but accepted as this is a missing chunk with tsn={} ssn={}", self.name, d.tsn, d.stream_sequence_number); - self.payload_queue.push(d.clone(), self.peer_last_tsn); - stream_handle_data = true; //s.handle_data(d.clone()); - } - } else { - log::debug!( - "[{}] receive buffer full. dropping DATA with tsn={} ssn={}", - self.name, - d.tsn, - d.stream_sequence_number - ); - } - } - } else { - // silently discard the data. (sender will retry on T3-rtx timeout) - // see pion/sctp#30 - log::debug!("discard {}", d.stream_sequence_number); - return Ok(vec![]); - } - } - - let immediate_sack = d.immediate_sack; - - if stream_handle_data { - if let Some(s) = self.streams.get_mut(&d.stream_identifier) { - s.handle_data(d.clone()).await; - } - } - - self.handle_peer_last_tsn_and_acknowledgement(immediate_sack) - } - - /// A common routine for handle_data and handle_forward_tsn routines - fn handle_peer_last_tsn_and_acknowledgement( - &mut self, - sack_immediately: bool, - ) -> Result> { - let mut reply = vec![]; - - // Try to advance peer_last_tsn - - // From RFC 3758 Sec 3.6: - // .. and then MUST further advance its cumulative TSN point locally - // if possible - // Meaning, if peer_last_tsn+1 points to a chunk that is received, - // advance peer_last_tsn until peer_last_tsn+1 points to unreceived chunk. - log::debug!("[{}] peer_last_tsn = {}", self.name, self.peer_last_tsn); - while self.payload_queue.pop(self.peer_last_tsn + 1).is_some() { - self.peer_last_tsn += 1; - log::debug!("[{}] peer_last_tsn = {}", self.name, self.peer_last_tsn); - - let rst_reqs: Vec = - self.reconfig_requests.values().cloned().collect(); - for rst_req in rst_reqs { - self.reset_streams_if_any(&rst_req, false, &mut reply)?; - } - } - - let has_packet_loss = !self.payload_queue.is_empty(); - if has_packet_loss { - log::trace!( - "[{}] packetloss: {}", - self.name, - self.payload_queue - .get_gap_ack_blocks_string(self.peer_last_tsn) - ); - } - - if (self.ack_state != AckState::Immediate - && !sack_immediately - && !has_packet_loss - && self.ack_mode == AckMode::Normal) - || self.ack_mode == AckMode::AlwaysDelay - { - if self.ack_state == AckState::Idle { - self.delayed_ack_triggered = true; - } else { - self.immediate_ack_triggered = true; - } - } else { - self.immediate_ack_triggered = true; - } - - Ok(reply) - } - - pub(crate) async fn get_my_receiver_window_credit(&self) -> u32 { - let mut bytes_queued = 0; - for s in self.streams.values() { - bytes_queued += s.get_num_bytes_in_reassembly_queue().await as u32; - } - - if bytes_queued >= self.max_receive_buffer_size { - 0 - } else { - self.max_receive_buffer_size - bytes_queued - } - } - - pub(crate) fn open_stream( - &mut self, - stream_identifier: u16, - default_payload_type: PayloadProtocolIdentifier, - ) -> Result> { - if self.streams.contains_key(&stream_identifier) { - return Err(Error::ErrStreamAlreadyExist); - } - - if let Some(s) = self.create_stream(stream_identifier, false) { - s.set_default_payload_type(default_payload_type); - Ok(Arc::clone(&s)) - } else { - Err(Error::ErrStreamCreateFailed) - } - } - - /// create_stream creates a stream. The caller should hold the lock and check no stream exists for this id. - fn create_stream(&mut self, stream_identifier: u16, accept: bool) -> Option> { - let s = Arc::new(Stream::new( - format!("{}:{}", stream_identifier, self.name), - stream_identifier, - self.max_payload_size, - Arc::clone(&self.max_message_size), - Arc::clone(&self.state), - self.awake_write_loop_ch.clone(), - Arc::clone(&self.pending_queue), - )); - - if accept { - if let Some(accept_ch) = &self.accept_ch_tx { - if accept_ch.try_send(Arc::clone(&s)).is_ok() { - log::debug!( - "[{}] accepted a new stream (streamIdentifier: {})", - self.name, - stream_identifier - ); - } else { - log::debug!("[{}] dropped a new stream due to accept_ch full", self.name); - return None; - } - } else { - log::debug!( - "[{}] dropped a new stream due to accept_ch_tx is None", - self.name - ); - return None; - } - } - self.streams.insert(stream_identifier, Arc::clone(&s)); - Some(s) - } - - /// get_or_create_stream gets or creates a stream. The caller should hold the lock. - fn get_or_create_stream(&mut self, stream_identifier: u16) -> Option> { - if self.streams.contains_key(&stream_identifier) { - self.streams.get(&stream_identifier).cloned() - } else { - self.create_stream(stream_identifier, true) - } - } - - async fn process_selective_ack( - &mut self, - d: &ChunkSelectiveAck, - ) -> Result<(HashMap, u32)> { - let mut bytes_acked_per_stream = HashMap::new(); - - // New ack point, so pop all ACKed packets from inflight_queue - // We add 1 because the "currentAckPoint" has already been popped from the inflight queue - // For the first SACK we take care of this by setting the ackpoint to cumAck - 1 - let mut i = self.cumulative_tsn_ack_point + 1; - //log::debug!("[{}] i={} d={}", self.name, i, d.cumulative_tsn_ack); - while sna32lte(i, d.cumulative_tsn_ack) { - if let Some(c) = self.inflight_queue.pop(i) { - if !c.acked { - // RFC 4096 sec 6.3.2. Retransmission Timer Rules - // R3) Whenever a SACK is received that acknowledges the DATA chunk - // with the earliest outstanding TSN for that address, restart the - // T3-rtx timer for that address with its current RTO (if there is - // still outstanding data on that address). - if i == self.cumulative_tsn_ack_point + 1 { - // T3 timer needs to be reset. Stop it for now. - if let Some(t3rtx) = &self.t3rtx { - t3rtx.stop().await; - } - } - - let n_bytes_acked = c.user_data.len() as i64; - - // Sum the number of bytes acknowledged per stream - if let Some(amount) = bytes_acked_per_stream.get_mut(&c.stream_identifier) { - *amount += n_bytes_acked; - } else { - bytes_acked_per_stream.insert(c.stream_identifier, n_bytes_acked); - } - - // RFC 4960 sec 6.3.1. RTO Calculation - // C4) When data is in flight and when allowed by rule C5 below, a new - // RTT measurement MUST be made each round trip. Furthermore, new - // RTT measurements SHOULD be made no more than once per round trip - // for a given destination transport address. - // C5) Karn's algorithm: RTT measurements MUST NOT be made using - // packets that were retransmitted (and thus for which it is - // ambiguous whether the reply was for the first instance of the - // chunk or for a later instance) - if c.nsent == 1 && sna32gte(c.tsn, self.min_tsn2measure_rtt) { - self.min_tsn2measure_rtt = self.my_next_tsn; - let rtt = match SystemTime::now().duration_since(c.since) { - Ok(rtt) => rtt, - Err(_) => return Err(Error::ErrInvalidSystemTime), - }; - let srtt = self.rto_mgr.set_new_rtt(rtt.as_millis() as u64); - log::trace!( - "[{}] SACK: measured-rtt={} srtt={} new-rto={}", - self.name, - rtt.as_millis(), - srtt, - self.rto_mgr.get_rto() - ); - } - } - - if self.in_fast_recovery && c.tsn == self.fast_recover_exit_point { - log::debug!("[{}] exit fast-recovery", self.name); - self.in_fast_recovery = false; - } - } else { - return Err(Error::ErrInflightQueueTsnPop); - } - - i += 1; - } - - let mut htna = d.cumulative_tsn_ack; - - // Mark selectively acknowledged chunks as "acked" - for g in &d.gap_ack_blocks { - for i in g.start..=g.end { - let tsn = d.cumulative_tsn_ack + i as u32; - - let (is_existed, is_acked) = if let Some(c) = self.inflight_queue.get(tsn) { - (true, c.acked) - } else { - (false, false) - }; - let n_bytes_acked = if is_existed && !is_acked { - self.inflight_queue.mark_as_acked(tsn) as i64 - } else { - 0 - }; - - if let Some(c) = self.inflight_queue.get(tsn) { - if !is_acked { - // Sum the number of bytes acknowledged per stream - if let Some(amount) = bytes_acked_per_stream.get_mut(&c.stream_identifier) { - *amount += n_bytes_acked; - } else { - bytes_acked_per_stream.insert(c.stream_identifier, n_bytes_acked); - } - - log::trace!("[{}] tsn={} has been sacked", self.name, c.tsn); - - if c.nsent == 1 { - self.min_tsn2measure_rtt = self.my_next_tsn; - let rtt = match SystemTime::now().duration_since(c.since) { - Ok(rtt) => rtt, - Err(_) => return Err(Error::ErrInvalidSystemTime), - }; - let srtt = self.rto_mgr.set_new_rtt(rtt.as_millis() as u64); - log::trace!( - "[{}] SACK: measured-rtt={} srtt={} new-rto={}", - self.name, - rtt.as_millis(), - srtt, - self.rto_mgr.get_rto() - ); - } - - if sna32lt(htna, tsn) { - htna = tsn; - } - } - } else { - return Err(Error::ErrTsnRequestNotExist); - } - } - } - - Ok((bytes_acked_per_stream, htna)) - } - - async fn on_cumulative_tsn_ack_point_advanced(&mut self, total_bytes_acked: i64) { - // RFC 4096, sec 6.3.2. Retransmission Timer Rules - // R2) Whenever all outstanding data sent to an address have been - // acknowledged, turn off the T3-rtx timer of that address. - if self.inflight_queue.is_empty() { - log::trace!( - "[{}] SACK: no more packet in-flight (pending={})", - self.name, - self.pending_queue.len() - ); - if let Some(t3rtx) = &self.t3rtx { - t3rtx.stop().await; - } - } else { - log::trace!("[{}] T3-rtx timer start (pt2)", self.name); - if let Some(t3rtx) = &self.t3rtx { - t3rtx.start(self.rto_mgr.get_rto()).await; - } - } - - // Update congestion control parameters - if self.cwnd <= self.ssthresh { - // RFC 4096, sec 7.2.1. Slow-Start - // o When cwnd is less than or equal to ssthresh, an SCTP endpoint MUST - // use the slow-start algorithm to increase cwnd only if the current - // congestion window is being fully utilized, an incoming SACK - // advances the Cumulative TSN Ack Point, and the data sender is not - // in Fast Recovery. Only when these three conditions are met can - // the cwnd be increased; otherwise, the cwnd MUST not be increased. - // If these conditions are met, then cwnd MUST be increased by, at - // most, the lesser of 1) the total size of the previously - // outstanding DATA chunk(s) acknowledged, and 2) the destination's - // path MTU. - if !self.in_fast_recovery && self.pending_queue.len() > 0 { - self.cwnd += std::cmp::min(total_bytes_acked as u32, self.cwnd); // TCP way - // self.cwnd += min32(uint32(total_bytes_acked), self.mtu) // SCTP way (slow) - log::trace!( - "[{}] updated cwnd={} ssthresh={} acked={} (SS)", - self.name, - self.cwnd, - self.ssthresh, - total_bytes_acked - ); - } else { - log::trace!( - "[{}] cwnd did not grow: cwnd={} ssthresh={} acked={} FR={} pending={}", - self.name, - self.cwnd, - self.ssthresh, - total_bytes_acked, - self.in_fast_recovery, - self.pending_queue.len() - ); - } - } else { - // RFC 4096, sec 7.2.2. Congestion Avoidance - // o Whenever cwnd is greater than ssthresh, upon each SACK arrival - // that advances the Cumulative TSN Ack Point, increase - // partial_bytes_acked by the total number of bytes of all new chunks - // acknowledged in that SACK including chunks acknowledged by the new - // Cumulative TSN Ack and by Gap Ack Blocks. - self.partial_bytes_acked += total_bytes_acked as u32; - - // o When partial_bytes_acked is equal to or greater than cwnd and - // before the arrival of the SACK the sender had cwnd or more bytes - // of data outstanding (i.e., before arrival of the SACK, flight size - // was greater than or equal to cwnd), increase cwnd by MTU, and - // reset partial_bytes_acked to (partial_bytes_acked - cwnd). - if self.partial_bytes_acked >= self.cwnd && self.pending_queue.len() > 0 { - self.partial_bytes_acked -= self.cwnd; - self.cwnd += self.mtu; - log::trace!( - "[{}] updated cwnd={} ssthresh={} acked={} (CA)", - self.name, - self.cwnd, - self.ssthresh, - total_bytes_acked - ); - } - } - } - - fn process_fast_retransmission( - &mut self, - cum_tsn_ack_point: u32, - htna: u32, - cum_tsn_ack_point_advanced: bool, - ) -> Result<()> { - // HTNA algorithm - RFC 4960 Sec 7.2.4 - // Increment missIndicator of each chunks that the SACK reported missing - // when either of the following is met: - // a) Not in fast-recovery - // miss indications are incremented only for missing TSNs prior to the - // highest TSN newly acknowledged in the SACK. - // b) In fast-recovery AND the Cumulative TSN Ack Point advanced - // the miss indications are incremented for all TSNs reported missing - // in the SACK. - if !self.in_fast_recovery || cum_tsn_ack_point_advanced { - let max_tsn = if !self.in_fast_recovery { - // a) increment only for missing TSNs prior to the HTNA - htna - } else { - // b) increment for all TSNs reported missing - cum_tsn_ack_point + (self.inflight_queue.len() as u32) + 1 - }; - - let mut tsn = cum_tsn_ack_point + 1; - while sna32lt(tsn, max_tsn) { - if let Some(c) = self.inflight_queue.get_mut(tsn) { - if !c.acked && !c.abandoned() && c.miss_indicator < 3 { - c.miss_indicator += 1; - if c.miss_indicator == 3 && !self.in_fast_recovery { - // 2) If not in Fast Recovery, adjust the ssthresh and cwnd of the - // destination address(es) to which the missing DATA chunks were - // last sent, according to the formula described in Section 7.2.3. - self.in_fast_recovery = true; - self.fast_recover_exit_point = htna; - self.ssthresh = std::cmp::max(self.cwnd / 2, 4 * self.mtu); - self.cwnd = self.ssthresh; - self.partial_bytes_acked = 0; - self.will_retransmit_fast = true; - - log::trace!( - "[{}] updated cwnd={} ssthresh={} inflight={} (FR)", - self.name, - self.cwnd, - self.ssthresh, - self.inflight_queue.get_num_bytes() - ); - } - } - } else { - return Err(Error::ErrTsnRequestNotExist); - } - - tsn += 1; - } - } - - if self.in_fast_recovery && cum_tsn_ack_point_advanced { - self.will_retransmit_fast = true; - } - - Ok(()) - } - - async fn handle_sack(&mut self, d: &ChunkSelectiveAck) -> Result> { - log::trace!( - "[{}] {}, SACK: cumTSN={} a_rwnd={}", - self.name, - self.cumulative_tsn_ack_point, - d.cumulative_tsn_ack, - d.advertised_receiver_window_credit - ); - let state = self.get_state(); - if state != AssociationState::Established - && state != AssociationState::ShutdownPending - && state != AssociationState::ShutdownReceived - { - return Ok(vec![]); - } - - self.stats.inc_sacks(); - - if sna32gt(self.cumulative_tsn_ack_point, d.cumulative_tsn_ack) { - // RFC 4960 sec 6.2.1. Processing a Received SACK - // D) - // i) If Cumulative TSN Ack is less than the Cumulative TSN Ack - // Point, then drop the SACK. Since Cumulative TSN Ack is - // monotonically increasing, a SACK whose Cumulative TSN Ack is - // less than the Cumulative TSN Ack Point indicates an out-of- - // order SACK. - - log::debug!( - "[{}] SACK Cumulative ACK {} is older than ACK point {}", - self.name, - d.cumulative_tsn_ack, - self.cumulative_tsn_ack_point - ); - - return Ok(vec![]); - } - - // Process selective ack - let (bytes_acked_per_stream, htna) = self.process_selective_ack(d).await?; - - let mut total_bytes_acked = 0; - for n_bytes_acked in bytes_acked_per_stream.values() { - total_bytes_acked += *n_bytes_acked; - } - - let mut cum_tsn_ack_point_advanced = false; - if sna32lt(self.cumulative_tsn_ack_point, d.cumulative_tsn_ack) { - log::trace!( - "[{}] SACK: cumTSN advanced: {} -> {}", - self.name, - self.cumulative_tsn_ack_point, - d.cumulative_tsn_ack - ); - - self.cumulative_tsn_ack_point = d.cumulative_tsn_ack; - cum_tsn_ack_point_advanced = true; - self.on_cumulative_tsn_ack_point_advanced(total_bytes_acked) - .await; - } - - for (si, n_bytes_acked) in &bytes_acked_per_stream { - if let Some(s) = self.streams.get_mut(si) { - s.on_buffer_released(*n_bytes_acked).await; - } - } - - // New rwnd value - // RFC 4960 sec 6.2.1. Processing a Received SACK - // D) - // ii) Set rwnd equal to the newly received a_rwnd minus the number - // of bytes still outstanding after processing the Cumulative - // TSN Ack and the Gap Ack Blocks. - - // bytes acked were already subtracted by markAsAcked() method - let bytes_outstanding = self.inflight_queue.get_num_bytes() as u32; - if bytes_outstanding >= d.advertised_receiver_window_credit { - self.rwnd = 0; - } else { - self.rwnd = d.advertised_receiver_window_credit - bytes_outstanding; - } - - self.process_fast_retransmission(d.cumulative_tsn_ack, htna, cum_tsn_ack_point_advanced)?; - - if self.use_forward_tsn { - // RFC 3758 Sec 3.5 C1 - if sna32lt( - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point, - ) { - self.advanced_peer_tsn_ack_point = self.cumulative_tsn_ack_point - } - - // RFC 3758 Sec 3.5 C2 - let mut i = self.advanced_peer_tsn_ack_point + 1; - while let Some(c) = self.inflight_queue.get(i) { - if !c.abandoned() { - break; - } - self.advanced_peer_tsn_ack_point = i; - i += 1; - } - - // RFC 3758 Sec 3.5 C3 - if sna32gt( - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point, - ) { - self.will_send_forward_tsn = true; - log::debug!( - "[{}] handleSack {}: sna32GT({}, {})", - self.name, - self.will_send_forward_tsn, - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point - ); - } - self.awake_write_loop(); - } - - self.postprocess_sack(state, cum_tsn_ack_point_advanced) - .await; - - Ok(vec![]) - } - - /// The caller must hold the lock. This method was only added because the - /// linter was complaining about the "cognitive complexity" of handle_sack. - async fn postprocess_sack( - &mut self, - state: AssociationState, - mut should_awake_write_loop: bool, - ) { - if !self.inflight_queue.is_empty() { - // Start timer. (noop if already started) - log::trace!("[{}] T3-rtx timer start (pt3)", self.name); - if let Some(t3rtx) = &self.t3rtx { - t3rtx.start(self.rto_mgr.get_rto()).await; - } - } else if state == AssociationState::ShutdownPending { - // No more outstanding, send shutdown. - should_awake_write_loop = true; - self.will_send_shutdown.store(true, Ordering::SeqCst); - self.set_state(AssociationState::ShutdownSent); - } else if state == AssociationState::ShutdownReceived { - // No more outstanding, send shutdown ack. - should_awake_write_loop = true; - self.will_send_shutdown_ack = true; - self.set_state(AssociationState::ShutdownAckSent); - } - - if should_awake_write_loop { - self.awake_write_loop(); - } - } - - async fn handle_shutdown(&mut self, _: &ChunkShutdown) -> Result> { - let state = self.get_state(); - - if state == AssociationState::Established { - if !self.inflight_queue.is_empty() { - self.set_state(AssociationState::ShutdownReceived); - } else { - // No more outstanding, send shutdown ack. - self.will_send_shutdown_ack = true; - self.set_state(AssociationState::ShutdownAckSent); - - self.awake_write_loop(); - } - } else if state == AssociationState::ShutdownSent { - // self.cumulative_tsn_ack_point = c.cumulative_tsn_ack - - self.will_send_shutdown_ack = true; - self.set_state(AssociationState::ShutdownAckSent); - - self.awake_write_loop(); - } - - Ok(vec![]) - } - - async fn handle_shutdown_ack(&mut self, _: &ChunkShutdownAck) -> Result> { - let state = self.get_state(); - if state == AssociationState::ShutdownSent || state == AssociationState::ShutdownAckSent { - if let Some(t2shutdown) = &self.t2shutdown { - t2shutdown.stop().await; - } - self.will_send_shutdown_complete = true; - - self.awake_write_loop(); - } - - Ok(vec![]) - } - - async fn handle_shutdown_complete(&mut self, _: &ChunkShutdownComplete) -> Result> { - let state = self.get_state(); - if state == AssociationState::ShutdownAckSent { - if let Some(t2shutdown) = &self.t2shutdown { - t2shutdown.stop().await; - } - self.close().await?; - } - - Ok(vec![]) - } - - /// create_forward_tsn generates ForwardTSN chunk. - /// This method will be be called if use_forward_tsn is set to false. - fn create_forward_tsn(&self) -> ChunkForwardTsn { - // RFC 3758 Sec 3.5 C4 - let mut stream_map: HashMap = HashMap::new(); // to report only once per SI - let mut i = self.cumulative_tsn_ack_point + 1; - while sna32lte(i, self.advanced_peer_tsn_ack_point) { - if let Some(c) = self.inflight_queue.get(i) { - if let Some(ssn) = stream_map.get(&c.stream_identifier) { - if sna16lt(*ssn, c.stream_sequence_number) { - // to report only once with greatest SSN - stream_map.insert(c.stream_identifier, c.stream_sequence_number); - } - } else { - stream_map.insert(c.stream_identifier, c.stream_sequence_number); - } - } else { - break; - } - - i += 1; - } - - let mut fwd_tsn = ChunkForwardTsn { - new_cumulative_tsn: self.advanced_peer_tsn_ack_point, - streams: vec![], - }; - - let mut stream_str = String::new(); - for (si, ssn) in &stream_map { - stream_str += format!("(si={si} ssn={ssn})").as_str(); - fwd_tsn.streams.push(ChunkForwardTsnStream { - identifier: *si, - sequence: *ssn, - }); - } - log::trace!( - "[{}] building fwd_tsn: newCumulativeTSN={} cumTSN={} - {}", - self.name, - fwd_tsn.new_cumulative_tsn, - self.cumulative_tsn_ack_point, - stream_str - ); - - fwd_tsn - } - - /// create_packet wraps chunks in a packet. - /// The caller should hold the read lock. - pub(crate) fn create_packet(&self, chunks: Vec>) -> Packet { - Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks, - } - } - - async fn handle_reconfig(&mut self, c: &ChunkReconfig) -> Result> { - log::trace!("[{}] handle_reconfig", self.name); - - let mut pp = vec![]; - - if let Some(param_a) = &c.param_a { - self.handle_reconfig_param(param_a, &mut pp).await?; - } - - if let Some(param_b) = &c.param_b { - self.handle_reconfig_param(param_b, &mut pp).await?; - } - - Ok(pp) - } - - async fn handle_forward_tsn(&mut self, c: &ChunkForwardTsn) -> Result> { - log::trace!("[{}] FwdTSN: {}", self.name, c.to_string()); - - if !self.use_forward_tsn { - log::warn!("[{}] received FwdTSN but not enabled", self.name); - // Return an error chunk - let cerr = ChunkError { - error_causes: vec![ErrorCauseUnrecognizedChunkType::default()], - }; - - let outbound = Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks: vec![Box::new(cerr)], - }; - return Ok(vec![outbound]); - } - - // From RFC 3758 Sec 3.6: - // Note, if the "New Cumulative TSN" value carried in the arrived - // FORWARD TSN chunk is found to be behind or at the current cumulative - // TSN point, the data receiver MUST treat this FORWARD TSN as out-of- - // date and MUST NOT update its Cumulative TSN. The receiver SHOULD - // send a SACK to its peer (the sender of the FORWARD TSN) since such a - // duplicate may indicate the previous SACK was lost in the network. - - log::trace!( - "[{}] should send ack? newCumTSN={} peer_last_tsn={}", - self.name, - c.new_cumulative_tsn, - self.peer_last_tsn - ); - if sna32lte(c.new_cumulative_tsn, self.peer_last_tsn) { - log::trace!("[{}] sending ack on Forward TSN", self.name); - self.ack_state = AckState::Immediate; - if let Some(ack_timer) = &mut self.ack_timer { - ack_timer.stop(); - } - self.awake_write_loop(); - return Ok(vec![]); - } - - // From RFC 3758 Sec 3.6: - // the receiver MUST perform the same TSN handling, including duplicate - // detection, gap detection, SACK generation, cumulative TSN - // advancement, etc. as defined in RFC 2960 [2]---with the following - // exceptions and additions. - - // When a FORWARD TSN chunk arrives, the data receiver MUST first update - // its cumulative TSN point to the value carried in the FORWARD TSN - // chunk, - - // Advance peer_last_tsn - while sna32lt(self.peer_last_tsn, c.new_cumulative_tsn) { - self.payload_queue.pop(self.peer_last_tsn + 1); // may not exist - self.peer_last_tsn += 1; - } - - // Report new peer_last_tsn value and abandoned largest SSN value to - // corresponding streams so that the abandoned chunks can be removed - // from the reassemblyQueue. - for forwarded in &c.streams { - if let Some(s) = self.streams.get_mut(&forwarded.identifier) { - s.handle_forward_tsn_for_ordered(forwarded.sequence).await; - } - } - - // TSN may be forewared for unordered chunks. ForwardTSN chunk does not - // report which stream identifier it skipped for unordered chunks. - // Therefore, we need to broadcast this event to all existing streams for - // unordered chunks. - // See https://github.com/pion/sctp/issues/106 - for s in self.streams.values_mut() { - s.handle_forward_tsn_for_unordered(c.new_cumulative_tsn) - .await; - } - - self.handle_peer_last_tsn_and_acknowledgement(false) - } - - async fn send_reset_request(&mut self, stream_identifier: u16) -> Result<()> { - let state = self.get_state(); - if state != AssociationState::Established { - return Err(Error::ErrResetPacketInStateNotExist); - } - - // Create DATA chunk which only contains valid stream identifier with - // nil userData and use it as a EOS from the stream. - let c = ChunkPayloadData { - stream_identifier, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::new(), - ..Default::default() - }; - - self.pending_queue.push(c).await; - self.awake_write_loop(); - - Ok(()) - } - - #[allow(clippy::borrowed_box)] - async fn handle_reconfig_param( - &mut self, - raw: &Box, - reply: &mut Vec, - ) -> Result<()> { - if let Some(p) = raw.as_any().downcast_ref::() { - self.reconfig_requests - .insert(p.reconfig_request_sequence_number, p.clone()); - self.reset_streams_if_any(p, true, reply)?; - Ok(()) - } else if let Some(p) = raw.as_any().downcast_ref::() { - self.reconfigs.remove(&p.reconfig_response_sequence_number); - if self.reconfigs.is_empty() { - if let Some(treconfig) = &self.treconfig { - treconfig.stop().await; - } - } - Ok(()) - } else { - Err(Error::ErrParameterType) - } - } - - fn reset_streams_if_any( - &mut self, - p: &ParamOutgoingResetRequest, - respond: bool, - reply: &mut Vec, - ) -> Result<()> { - let mut result = ReconfigResult::SuccessPerformed; - let mut sis_to_reset = vec![]; - - if sna32lte(p.sender_last_tsn, self.peer_last_tsn) { - log::debug!( - "[{}] resetStream(): senderLastTSN={} <= peer_last_tsn={}", - self.name, - p.sender_last_tsn, - self.peer_last_tsn - ); - for id in &p.stream_identifiers { - if let Some(s) = self.streams.get(id) { - let stream_identifier = s.stream_identifier; - if respond { - sis_to_reset.push(*id); - } - self.unregister_stream(stream_identifier); - } - } - self.reconfig_requests - .remove(&p.reconfig_request_sequence_number); - } else { - log::debug!( - "[{}] resetStream(): senderLastTSN={} > peer_last_tsn={}", - self.name, - p.sender_last_tsn, - self.peer_last_tsn - ); - result = ReconfigResult::InProgress; - } - - // Answer incoming reset requests with the same reset request, but with - // reconfig_response_sequence_number. - if !sis_to_reset.is_empty() { - let rsn = self.generate_next_rsn(); - let tsn = self.my_next_tsn - 1; - - let c = ChunkReconfig { - param_a: Some(Box::new(ParamOutgoingResetRequest { - reconfig_request_sequence_number: rsn, - reconfig_response_sequence_number: p.reconfig_request_sequence_number, - sender_last_tsn: tsn, - stream_identifiers: sis_to_reset, - })), - ..Default::default() - }; - - self.reconfigs.insert(rsn, c.clone()); // store in the map for retransmission - - let p = self.create_packet(vec![Box::new(c)]); - reply.push(p); - } - - let packet = self.create_packet(vec![Box::new(ChunkReconfig { - param_a: Some(Box::new(ParamReconfigResponse { - reconfig_response_sequence_number: p.reconfig_request_sequence_number, - result, - })), - param_b: None, - })]); - - log::debug!("[{}] RESET RESPONSE: {}", self.name, packet); - - reply.push(packet); - - Ok(()) - } - - /// Move the chunk peeked with self.pending_queue.peek() to the inflight_queue. - async fn move_pending_data_chunk_to_inflight_queue( - &mut self, - beginning_fragment: bool, - unordered: bool, - ) -> Option { - if let Some(mut c) = self.pending_queue.pop(beginning_fragment, unordered) { - // Mark all fragments are in-flight now - if c.ending_fragment { - c.set_all_inflight(); - } - - // Assign TSN - c.tsn = self.generate_next_tsn(); - - c.since = SystemTime::now(); // use to calculate RTT and also for maxPacketLifeTime - c.nsent = 1; // being sent for the first time - - self.check_partial_reliability_status(&c); - - log::trace!( - "[{}] sending ppi={} tsn={} ssn={} sent={} len={} ({},{})", - self.name, - c.payload_type as u32, - c.tsn, - c.stream_sequence_number, - c.nsent, - c.user_data.len(), - c.beginning_fragment, - c.ending_fragment - ); - - self.inflight_queue.push_no_check(c.clone()); - - Some(c) - } else { - log::error!("[{}] failed to pop from pending queue", self.name); - None - } - } - - /// pop_pending_data_chunks_to_send pops chunks from the pending queues as many as - /// the cwnd and rwnd allows to send. - async fn pop_pending_data_chunks_to_send(&mut self) -> (Vec, Vec) { - let mut chunks = vec![]; - let mut sis_to_reset = vec![]; // stream identifiers to reset - - if self.pending_queue.len() == 0 { - return (chunks, sis_to_reset); - } - - // RFC 4960 sec 6.1. Transmission of DATA Chunks - // A) At any given time, the data sender MUST NOT transmit new data to - // any destination transport address if its peer's rwnd indicates - // that the peer has no buffer space (i.e., rwnd is 0; see Section - // 6.2.1). However, regardless of the value of rwnd (including if it - // is 0), the data sender can always have one DATA chunk in flight to - // the receiver if allowed by cwnd (see rule B, below). - while let Some(c) = self.pending_queue.peek() { - let (beginning_fragment, unordered, data_len, stream_identifier) = ( - c.beginning_fragment, - c.unordered, - c.user_data.len(), - c.stream_identifier, - ); - - if data_len == 0 { - sis_to_reset.push(stream_identifier); - if self - .pending_queue - .pop(beginning_fragment, unordered) - .is_none() - { - log::error!("failed to pop from pending queue"); - } - continue; - } - - if self.inflight_queue.get_num_bytes() + data_len > self.cwnd as usize { - break; // would exceed cwnd - } - - if data_len > self.rwnd as usize { - break; // no more rwnd - } - - self.rwnd -= data_len as u32; - - if let Some(chunk) = self - .move_pending_data_chunk_to_inflight_queue(beginning_fragment, unordered) - .await - { - chunks.push(chunk); - } - } - - // the data sender can always have one DATA chunk in flight to the receiver - if chunks.is_empty() && self.inflight_queue.is_empty() { - // Send zero window probe - if let Some(c) = self.pending_queue.peek() { - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - - if let Some(chunk) = self - .move_pending_data_chunk_to_inflight_queue(beginning_fragment, unordered) - .await - { - chunks.push(chunk); - } - } - } - - (chunks, sis_to_reset) - } - - /// bundle_data_chunks_into_packets packs DATA chunks into packets. It tries to bundle - /// DATA chunks into a packet so long as the resulting packet size does not exceed - /// the path MTU. - fn bundle_data_chunks_into_packets(&self, chunks: Vec) -> Vec { - let mut packets = vec![]; - let mut chunks_to_send = vec![]; - let mut bytes_in_packet = COMMON_HEADER_SIZE; - - for c in chunks { - // RFC 4960 sec 6.1. Transmission of DATA Chunks - // Multiple DATA chunks committed for transmission MAY be bundled in a - // single packet. Furthermore, DATA chunks being retransmitted MAY be - // bundled with new DATA chunks, as long as the resulting packet size - // does not exceed the path MTU. - if bytes_in_packet + c.user_data.len() as u32 > self.mtu { - packets.push(self.create_packet(chunks_to_send)); - chunks_to_send = vec![]; - bytes_in_packet = COMMON_HEADER_SIZE; - } - - bytes_in_packet += DATA_CHUNK_HEADER_SIZE + c.user_data.len() as u32; - chunks_to_send.push(Box::new(c)); - } - - if !chunks_to_send.is_empty() { - packets.push(self.create_packet(chunks_to_send)); - } - - packets - } - - fn check_partial_reliability_status(&self, c: &ChunkPayloadData) { - if !self.use_forward_tsn { - return; - } - - // draft-ietf-rtcweb-data-protocol-09.txt section 6 - // 6. Procedures - // All Data Channel Establishment Protocol messages MUST be sent using - // ordered delivery and reliable transmission. - // - if c.payload_type == PayloadProtocolIdentifier::Dcep { - return; - } - - // PR-SCTP - if let Some(s) = self.streams.get(&c.stream_identifier) { - let reliability_type: ReliabilityType = - s.reliability_type.load(Ordering::SeqCst).into(); - let reliability_value = s.reliability_value.load(Ordering::SeqCst); - - if reliability_type == ReliabilityType::Rexmit { - if c.nsent >= reliability_value { - c.set_abandoned(true); - log::trace!( - "[{}] marked as abandoned: tsn={} ppi={} (remix: {})", - self.name, - c.tsn, - c.payload_type, - c.nsent - ); - } - } else if reliability_type == ReliabilityType::Timed { - if let Ok(elapsed) = SystemTime::now().duration_since(c.since) { - if elapsed.as_millis() as u32 >= reliability_value { - c.set_abandoned(true); - log::trace!( - "[{}] marked as abandoned: tsn={} ppi={} (timed: {:?})", - self.name, - c.tsn, - c.payload_type, - elapsed - ); - } - } - } - } else { - log::error!("[{}] stream {} not found)", self.name, c.stream_identifier); - } - } - - /// get_data_packets_to_retransmit is called when T3-rtx is timed out and retransmit outstanding data chunks - /// that are not acked or abandoned yet. - fn get_data_packets_to_retransmit(&mut self) -> Vec { - let awnd = std::cmp::min(self.cwnd, self.rwnd); - let mut chunks = vec![]; - let mut bytes_to_send = 0; - let mut done = false; - let mut i = 0; - while !done { - let tsn = self.cumulative_tsn_ack_point + i + 1; - if let Some(c) = self.inflight_queue.get_mut(tsn) { - if !c.retransmit { - i += 1; - continue; - } - - if i == 0 && self.rwnd < c.user_data.len() as u32 { - // Send it as a zero window probe - done = true; - } else if bytes_to_send + c.user_data.len() > awnd as usize { - break; - } - - // reset the retransmit flag not to retransmit again before the next - // t3-rtx timer fires - c.retransmit = false; - bytes_to_send += c.user_data.len(); - - c.nsent += 1; - } else { - break; // end of pending data - } - - if let Some(c) = self.inflight_queue.get(tsn) { - self.check_partial_reliability_status(c); - - log::trace!( - "[{}] retransmitting tsn={} ssn={} sent={}", - self.name, - c.tsn, - c.stream_sequence_number, - c.nsent - ); - - chunks.push(c.clone()); - } - i += 1; - } - - self.bundle_data_chunks_into_packets(chunks) - } - - /// generate_next_tsn returns the my_next_tsn and increases it. The caller should hold the lock. - fn generate_next_tsn(&mut self) -> u32 { - let tsn = self.my_next_tsn; - self.my_next_tsn += 1; - tsn - } - - /// generate_next_rsn returns the my_next_rsn and increases it. The caller should hold the lock. - fn generate_next_rsn(&mut self) -> u32 { - let rsn = self.my_next_rsn; - self.my_next_rsn += 1; - rsn - } - - async fn create_selective_ack_chunk(&mut self) -> ChunkSelectiveAck { - ChunkSelectiveAck { - cumulative_tsn_ack: self.peer_last_tsn, - advertised_receiver_window_credit: self.get_my_receiver_window_credit().await, - gap_ack_blocks: self.payload_queue.get_gap_ack_blocks(self.peer_last_tsn), - duplicate_tsn: self.payload_queue.pop_duplicates(), - } - } - - fn pack(p: Packet) -> Vec { - vec![p] - } - - fn handle_chunk_start(&mut self) { - self.delayed_ack_triggered = false; - self.immediate_ack_triggered = false; - } - - fn handle_chunk_end(&mut self) { - if self.immediate_ack_triggered { - self.ack_state = AckState::Immediate; - if let Some(ack_timer) = &mut self.ack_timer { - ack_timer.stop(); - } - self.awake_write_loop(); - } else if self.delayed_ack_triggered { - // Will send delayed ack in the next ack timeout - self.ack_state = AckState::Delay; - if let Some(ack_timer) = &mut self.ack_timer { - ack_timer.start(); - } - } - } - - #[allow(clippy::borrowed_box)] - async fn handle_chunk( - &mut self, - p: &Packet, - chunk: &Box, - ) -> Result<()> { - chunk.check()?; - let chunk_any = chunk.as_any(); - let packets = if let Some(c) = chunk_any.downcast_ref::() { - if c.is_ack { - self.handle_init_ack(p, c).await? - } else { - self.handle_init(p, c).await? - } - } else if chunk_any.downcast_ref::().is_some() - || chunk_any.downcast_ref::().is_some() - { - return Err(Error::ErrChunk); - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_heartbeat(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_cookie_echo(c).await? - } else if chunk_any.downcast_ref::().is_some() { - self.handle_cookie_ack().await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_data(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_sack(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_reconfig(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_forward_tsn(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_shutdown(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_shutdown_ack(c).await? - } else if let Some(c) = chunk_any.downcast_ref::() { - self.handle_shutdown_complete(c).await? - } else { - /* - https://datatracker.ietf.org/doc/html/rfc4960#section-3 - - 00 - Stop processing this SCTP packet and discard it, do not - process any further chunks within it. - - 01 - Stop processing this SCTP packet and discard it, do not - process any further chunks within it, and report the - unrecognized chunk in an 'Unrecognized Chunk Type'. - - 10 - Skip this chunk and continue processing. - - 11 - Skip this chunk and continue processing, but report in an - ERROR chunk using the 'Unrecognized Chunk Type' cause of - error. - */ - let handle_code = chunk.header().typ.0 >> 6; - match handle_code { - 0b00 => { - // Stop processing this packet - return Err(Error::ErrChunkTypeUnhandled); - } - 0b01 => { - // stop processing but report the chunk as unrecognized - let err_chunk = ChunkError { - error_causes: vec![ErrorCause { - code: UNRECOGNIZED_CHUNK_TYPE, - raw: chunk.marshal()?, - }], - }; - let packet = Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks: vec![Box::new(err_chunk)], - }; - self.control_queue.push_back(packet); - self.awake_write_loop(); - return Err(Error::ErrChunkTypeUnhandled); - } - 0b10 => { - // just ignore - vec![] - } - 0b11 => { - // keep processing but report the chunk as unrecognized - let err_chunk = ChunkError { - error_causes: vec![ErrorCause { - code: UNRECOGNIZED_CHUNK_TYPE, - raw: chunk.marshal()?, - }], - }; - let packet = Packet { - verification_tag: self.peer_verification_tag, - source_port: self.source_port, - destination_port: self.destination_port, - chunks: vec![Box::new(err_chunk)], - }; - vec![packet] - } - _ => unreachable!("This can only have 4 values."), - } - }; - - if !packets.is_empty() { - let mut buf: VecDeque<_> = packets.into_iter().collect(); - self.control_queue.append(&mut buf); - self.awake_write_loop(); - } - - Ok(()) - } - - /// buffered_amount returns total amount (in bytes) of currently buffered user data. - /// This is used only by testing. - pub(crate) fn buffered_amount(&self) -> usize { - self.pending_queue.get_num_bytes() + self.inflight_queue.get_num_bytes() - } -} - -#[async_trait] -impl AckTimerObserver for AssociationInternal { - async fn on_ack_timeout(&mut self) { - log::trace!( - "[{}] ack timed out (ack_state: {})", - self.name, - self.ack_state - ); - self.stats.inc_ack_timeouts(); - self.ack_state = AckState::Immediate; - self.awake_write_loop(); - } -} - -#[async_trait] -impl RtxTimerObserver for AssociationInternal { - async fn on_retransmission_timeout(&mut self, id: RtxTimerId, n_rtos: usize) { - match id { - RtxTimerId::T1Init => { - if let Err(err) = self.send_init() { - log::debug!( - "[{}] failed to retransmit init (n_rtos={}): {:?}", - self.name, - n_rtos, - err - ); - } - } - - RtxTimerId::T1Cookie => { - if let Err(err) = self.send_cookie_echo() { - log::debug!( - "[{}] failed to retransmit cookie-echo (n_rtos={}): {:?}", - self.name, - n_rtos, - err - ); - } - } - - RtxTimerId::T2Shutdown => { - log::debug!( - "[{}] retransmission of shutdown timeout (n_rtos={})", - self.name, - n_rtos - ); - let state = self.get_state(); - match state { - AssociationState::ShutdownSent => { - self.will_send_shutdown.store(true, Ordering::SeqCst); - self.awake_write_loop(); - } - AssociationState::ShutdownAckSent => { - self.will_send_shutdown_ack = true; - self.awake_write_loop(); - } - _ => {} - } - } - - RtxTimerId::T3RTX => { - self.stats.inc_t3timeouts(); - - // RFC 4960 sec 6.3.3 - // E1) For the destination address for which the timer expires, adjust - // its ssthresh with rules defined in Section 7.2.3 and set the - // cwnd <- MTU. - // RFC 4960 sec 7.2.3 - // When the T3-rtx timer expires on an address, SCTP should perform slow - // start by: - // ssthresh = max(cwnd/2, 4*MTU) - // cwnd = 1*MTU - - self.ssthresh = std::cmp::max(self.cwnd / 2, 4 * self.mtu); - self.cwnd = self.mtu; - log::trace!( - "[{}] updated cwnd={} ssthresh={} inflight={} (RTO)", - self.name, - self.cwnd, - self.ssthresh, - self.inflight_queue.get_num_bytes() - ); - - // RFC 3758 sec 3.5 - // A5) Any time the T3-rtx timer expires, on any destination, the sender - // SHOULD try to advance the "Advanced.Peer.Ack.Point" by following - // the procedures outlined in C2 - C5. - if self.use_forward_tsn { - // RFC 3758 Sec 3.5 C2 - let mut i = self.advanced_peer_tsn_ack_point + 1; - while let Some(c) = self.inflight_queue.get(i) { - if !c.abandoned() { - break; - } - self.advanced_peer_tsn_ack_point = i; - i += 1; - } - - // RFC 3758 Sec 3.5 C3 - if sna32gt( - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point, - ) { - self.will_send_forward_tsn = true; - log::debug!( - "[{}] on_retransmission_timeout {}: sna32GT({}, {})", - self.name, - self.will_send_forward_tsn, - self.advanced_peer_tsn_ack_point, - self.cumulative_tsn_ack_point - ); - } - } - - log::debug!( - "[{}] T3-rtx timed out: n_rtos={} cwnd={} ssthresh={}", - self.name, - n_rtos, - self.cwnd, - self.ssthresh - ); - - self.inflight_queue.mark_all_to_retrasmit(); - self.awake_write_loop(); - } - - RtxTimerId::Reconfig => { - self.will_retransmit_reconfig = true; - self.awake_write_loop(); - } - } - } - - async fn on_retransmission_failure(&mut self, id: RtxTimerId) { - match id { - RtxTimerId::T1Init => { - log::error!("[{}] retransmission failure: T1-init", self.name); - if let Some(handshake_completed_ch) = &self.handshake_completed_ch_tx { - let _ = handshake_completed_ch - .send(Some(Error::ErrHandshakeInitAck)) - .await; - } - } - RtxTimerId::T1Cookie => { - log::error!("[{}] retransmission failure: T1-cookie", self.name); - if let Some(handshake_completed_ch) = &self.handshake_completed_ch_tx { - let _ = handshake_completed_ch - .send(Some(Error::ErrHandshakeCookieEcho)) - .await; - } - } - - RtxTimerId::T2Shutdown => { - log::error!("[{}] retransmission failure: T2-shutdown", self.name); - } - - RtxTimerId::T3RTX => { - // T3-rtx timer will not fail by design - // Justifications: - // * ICE would fail if the connectivity is lost - // * WebRTC spec is not clear how this incident should be reported to ULP - log::error!("[{}] retransmission failure: T3-rtx (DATA)", self.name); - } - _ => {} - } - } -} diff --git a/sctp/src/association/association_internal/association_internal_test.rs b/sctp/src/association/association_internal/association_internal_test.rs deleted file mode 100644 index 2a830aef9..000000000 --- a/sctp/src/association/association_internal/association_internal_test.rs +++ /dev/null @@ -1,544 +0,0 @@ -use std::io; -use std::net::SocketAddr; - -use super::*; - -type Result = std::result::Result; - -impl From for util::Error { - fn from(e: Error) -> Self { - util::Error::from_std(e) - } -} - -struct DumbConn; - -#[async_trait] -impl Conn for DumbConn { - async fn connect(&self, _addr: SocketAddr) -> Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, _b: &mut [u8]) -> Result { - Ok(0) - } - - async fn recv_from(&self, _buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn send(&self, _b: &[u8]) -> Result { - Ok(0) - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> Result { - Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "Addr Not Available").into()) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -fn create_association_internal(config: Config) -> AssociationInternal { - let (close_loop_ch_tx, _close_loop_ch_rx) = broadcast::channel(1); - let (accept_ch_tx, _accept_ch_rx) = mpsc::channel(1); - let (handshake_completed_ch_tx, _handshake_completed_ch_rx) = mpsc::channel(1); - let (awake_write_loop_ch_tx, _awake_write_loop_ch_rx) = mpsc::channel(1); - AssociationInternal::new( - config, - close_loop_ch_tx, - accept_ch_tx, - handshake_completed_ch_tx, - Arc::new(awake_write_loop_ch_tx), - ) -} - -#[test] -fn test_create_forward_tsn_forward_one_abandoned() -> Result<()> { - let mut a = AssociationInternal { - cumulative_tsn_ack_point: 9, - ..Default::default() - }; - - a.advanced_peer_tsn_ack_point = 10; - a.inflight_queue.push_no_check(ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: 10, - stream_identifier: 1, - stream_sequence_number: 2, - user_data: Bytes::from_static(b"ABC"), - nsent: 1, - abandoned: Arc::new(AtomicBool::new(true)), - ..Default::default() - }); - - let fwdtsn = a.create_forward_tsn(); - - assert_eq!(fwdtsn.new_cumulative_tsn, 10, "should be able to serialize"); - assert_eq!(fwdtsn.streams.len(), 1, "there should be one stream"); - assert_eq!(fwdtsn.streams[0].identifier, 1, "si should be 1"); - assert_eq!(fwdtsn.streams[0].sequence, 2, "ssn should be 2"); - - Ok(()) -} - -#[test] -fn test_create_forward_tsn_forward_two_abandoned_with_the_same_si() -> Result<()> { - let mut a = AssociationInternal { - cumulative_tsn_ack_point: 9, - ..Default::default() - }; - - a.advanced_peer_tsn_ack_point = 12; - a.inflight_queue.push_no_check(ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: 10, - stream_identifier: 1, - stream_sequence_number: 2, - user_data: Bytes::from_static(b"ABC"), - nsent: 1, - abandoned: Arc::new(AtomicBool::new(true)), - ..Default::default() - }); - a.inflight_queue.push_no_check(ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: 11, - stream_identifier: 1, - stream_sequence_number: 3, - user_data: Bytes::from_static(b"DEF"), - nsent: 1, - abandoned: Arc::new(AtomicBool::new(true)), - ..Default::default() - }); - a.inflight_queue.push_no_check(ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: 12, - stream_identifier: 2, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"123"), - nsent: 1, - abandoned: Arc::new(AtomicBool::new(true)), - ..Default::default() - }); - - let fwdtsn = a.create_forward_tsn(); - - assert_eq!(fwdtsn.new_cumulative_tsn, 12, "should be able to serialize"); - assert_eq!(fwdtsn.streams.len(), 2, "there should be two stream"); - - let mut si1ok = false; - let mut si2ok = false; - for s in &fwdtsn.streams { - match s.identifier { - 1 => { - assert_eq!(3, s.sequence, "ssn should be 3"); - si1ok = true; - } - 2 => { - assert_eq!(1, s.sequence, "ssn should be 1"); - si2ok = true; - } - _ => panic!("unexpected stream identifier"), - } - } - assert!(si1ok, "si=1 should be present"); - assert!(si2ok, "si=2 should be present"); - - Ok(()) -} - -#[tokio::test] -async fn test_handle_forward_tsn_forward_3unreceived_chunks() -> Result<()> { - let mut a = AssociationInternal { - use_forward_tsn: true, - ..Default::default() - }; - - let prev_tsn = a.peer_last_tsn; - - let fwdtsn = ChunkForwardTsn { - new_cumulative_tsn: a.peer_last_tsn + 3, - streams: vec![ChunkForwardTsnStream { - identifier: 0, - sequence: 0, - }], - }; - - let p = a.handle_forward_tsn(&fwdtsn).await?; - - let delayed_ack_triggered = a.delayed_ack_triggered; - let immediate_ack_triggered = a.immediate_ack_triggered; - assert_eq!( - a.peer_last_tsn, - prev_tsn + 3, - "peerLastTSN should advance by 3 " - ); - assert!(delayed_ack_triggered, "delayed sack should be triggered"); - assert!( - !immediate_ack_triggered, - "immediate sack should NOT be triggered" - ); - assert!(p.is_empty(), "should return empty"); - - Ok(()) -} - -#[tokio::test] -async fn test_handle_forward_tsn_forward_1for1_missing() -> Result<()> { - let mut a = AssociationInternal { - use_forward_tsn: true, - ..Default::default() - }; - - let prev_tsn = a.peer_last_tsn; - - // this chunk is blocked by the missing chunk at tsn=1 - a.payload_queue.push( - ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: a.peer_last_tsn + 2, - stream_identifier: 0, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }, - a.peer_last_tsn, - ); - - let fwdtsn = ChunkForwardTsn { - new_cumulative_tsn: a.peer_last_tsn + 1, - streams: vec![ChunkForwardTsnStream { - identifier: 0, - sequence: 1, - }], - }; - - let p = a.handle_forward_tsn(&fwdtsn).await?; - - let delayed_ack_triggered = a.delayed_ack_triggered; - let immediate_ack_triggered = a.immediate_ack_triggered; - assert_eq!( - a.peer_last_tsn, - prev_tsn + 2, - "peerLastTSN should advance by 2" - ); - assert!(delayed_ack_triggered, "delayed sack should be triggered"); - assert!( - !immediate_ack_triggered, - "immediate sack should NOT be triggered" - ); - assert!(p.is_empty(), "should return empty"); - - Ok(()) -} - -#[tokio::test] -async fn test_handle_forward_tsn_forward_1for2_missing() -> Result<()> { - let mut a = AssociationInternal { - use_forward_tsn: true, - ..Default::default() - }; - - let prev_tsn = a.peer_last_tsn; - - // this chunk is blocked by the missing chunk at tsn=1 - a.payload_queue.push( - ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: a.peer_last_tsn + 3, - stream_identifier: 0, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }, - a.peer_last_tsn, - ); - - let fwdtsn = ChunkForwardTsn { - new_cumulative_tsn: a.peer_last_tsn + 1, - streams: vec![ChunkForwardTsnStream { - identifier: 0, - sequence: 1, - }], - }; - - let p = a.handle_forward_tsn(&fwdtsn).await?; - - let immediate_ack_triggered = a.immediate_ack_triggered; - assert_eq!( - a.peer_last_tsn, - prev_tsn + 1, - "peerLastTSN should advance by 1" - ); - assert!( - immediate_ack_triggered, - "immediate sack should be triggered" - ); - assert!(p.is_empty(), "should return empty"); - - Ok(()) -} - -#[tokio::test] -async fn test_handle_forward_tsn_dup_forward_tsn_chunk_should_generate_sack() -> Result<()> { - let mut a = AssociationInternal { - use_forward_tsn: true, - ..Default::default() - }; - - let prev_tsn = a.peer_last_tsn; - - let fwdtsn = ChunkForwardTsn { - new_cumulative_tsn: a.peer_last_tsn, - streams: vec![ChunkForwardTsnStream { - identifier: 0, - sequence: 1, - }], - }; - - let p = a.handle_forward_tsn(&fwdtsn).await?; - - assert_eq!(a.peer_last_tsn, prev_tsn, "peerLastTSN should not advance"); - assert_eq!(a.ack_state, AckState::Immediate, "sack should be requested"); - assert!(p.is_empty(), "should return empty"); - - Ok(()) -} - -#[tokio::test] -async fn test_assoc_create_new_stream() -> Result<()> { - let (accept_ch_tx, _accept_ch_rx) = mpsc::channel(ACCEPT_CH_SIZE); - let mut a = AssociationInternal { - accept_ch_tx: Some(accept_ch_tx), - ..Default::default() - }; - - for i in 0..ACCEPT_CH_SIZE { - let s = a.create_stream(i as u16, true); - if let Some(s) = s { - let result = a.streams.get(&s.stream_identifier); - assert!(result.is_some(), "should be in a.streams map"); - } else { - panic!("{i} should success"); - } - } - - let new_si = ACCEPT_CH_SIZE as u16; - let s = a.create_stream(new_si, true); - assert!(s.is_none(), "should be none"); - let result = a.streams.get(&new_si); - assert!(result.is_none(), "should NOT be in a.streams map"); - - let to_be_ignored = ChunkPayloadData { - beginning_fragment: true, - ending_fragment: true, - tsn: a.peer_last_tsn + 1, - stream_identifier: new_si, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let p = a.handle_data(&to_be_ignored).await?; - assert!(p.is_empty(), "should return empty"); - - Ok(()) -} - -async fn handle_init_test(name: &str, initial_state: AssociationState, expect_err: bool) { - let mut a = create_association_internal(Config { - net_conn: Arc::new(DumbConn {}), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }); - a.set_state(initial_state); - let pkt = Packet { - source_port: 5001, - destination_port: 5002, - ..Default::default() - }; - let mut init = ChunkInit { - initial_tsn: 1234, - num_outbound_streams: 1001, - num_inbound_streams: 1002, - initiate_tag: 5678, - advertised_receiver_window_credit: 512 * 1024, - ..Default::default() - }; - init.set_supported_extensions(); - - let result = a.handle_init(&pkt, &init).await; - if expect_err { - assert!(result.is_err(), "{name} should fail"); - return; - } else { - assert!(result.is_ok(), "{name} should be ok"); - } - assert_eq!( - a.peer_last_tsn, - if init.initial_tsn == 0 { - u32::MAX - } else { - init.initial_tsn - 1 - }, - "{name} should match" - ); - assert_eq!(a.my_max_num_outbound_streams, 1001, "{name} should match"); - assert_eq!(a.my_max_num_inbound_streams, 1002, "{name} should match"); - assert_eq!(a.peer_verification_tag, 5678, "{name} should match"); - assert_eq!(a.destination_port, pkt.source_port, "{name} should match"); - assert_eq!(a.source_port, pkt.destination_port, "{name} should match"); - assert!(a.use_forward_tsn, "{name} should be set to true"); -} - -#[tokio::test] -async fn test_assoc_handle_init() -> Result<()> { - handle_init_test("normal", AssociationState::Closed, false).await; - - handle_init_test( - "unexpected state established", - AssociationState::Established, - true, - ) - .await; - - handle_init_test( - "unexpected state shutdownAckSent", - AssociationState::ShutdownAckSent, - true, - ) - .await; - - handle_init_test( - "unexpected state shutdownPending", - AssociationState::ShutdownPending, - true, - ) - .await; - - handle_init_test( - "unexpected state shutdownReceived", - AssociationState::ShutdownReceived, - true, - ) - .await; - - handle_init_test( - "unexpected state shutdownSent", - AssociationState::ShutdownSent, - true, - ) - .await; - - Ok(()) -} - -#[tokio::test] -async fn test_assoc_max_message_size_default() -> Result<()> { - let mut a = create_association_internal(Config { - net_conn: Arc::new(DumbConn {}), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }); - assert_eq!( - a.max_message_size.load(Ordering::SeqCst), - 65536, - "should match" - ); - - let stream = a.create_stream(1, false); - assert!(stream.is_some(), "should succeed"); - - if let Some(s) = stream { - let p = Bytes::from(vec![0u8; 65537]); - let ppi = PayloadProtocolIdentifier::from(s.default_payload_type.load(Ordering::SeqCst)); - - if let Err(err) = s.write_sctp(&p.slice(..65536), ppi).await { - assert_ne!( - err, - Error::ErrOutboundPacketTooLarge, - "should be not Error::ErrOutboundPacketTooLarge" - ); - } else { - panic!("should be error"); - } - - if let Err(err) = s.write_sctp(&p.slice(..65537), ppi).await { - assert_eq!( - err, - Error::ErrOutboundPacketTooLarge, - "should be Error::ErrOutboundPacketTooLarge" - ); - } else { - panic!("should be error"); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_assoc_max_message_size_explicit() -> Result<()> { - let mut a = create_association_internal(Config { - net_conn: Arc::new(DumbConn {}), - max_receive_buffer_size: 0, - max_message_size: 30000, - name: "client".to_owned(), - }); - - assert_eq!( - a.max_message_size.load(Ordering::SeqCst), - 30000, - "should match" - ); - - let stream = a.create_stream(1, false); - assert!(stream.is_some(), "should succeed"); - - if let Some(s) = stream { - let p = Bytes::from(vec![0u8; 30001]); - let ppi = PayloadProtocolIdentifier::from(s.default_payload_type.load(Ordering::SeqCst)); - - if let Err(err) = s.write_sctp(&p.slice(..30000), ppi).await { - assert_ne!( - err, - Error::ErrOutboundPacketTooLarge, - "should be not Error::ErrOutboundPacketTooLarge" - ); - } else { - panic!("should be error"); - } - - if let Err(err) = s.write_sctp(&p.slice(..30001), ppi).await { - assert_eq!( - err, - Error::ErrOutboundPacketTooLarge, - "should be Error::ErrOutboundPacketTooLarge" - ); - } else { - panic!("should be error"); - } - } - - Ok(()) -} diff --git a/sctp/src/association/association_stats.rs b/sctp/src/association/association_stats.rs deleted file mode 100644 index 0fe390c0c..000000000 --- a/sctp/src/association/association_stats.rs +++ /dev/null @@ -1,61 +0,0 @@ -use portable_atomic::AtomicU64; -use std::sync::atomic::Ordering; - -#[derive(Default, Debug)] -pub(crate) struct AssociationStats { - n_datas: AtomicU64, - n_sacks: AtomicU64, - n_t3timeouts: AtomicU64, - n_ack_timeouts: AtomicU64, - n_fast_retrans: AtomicU64, -} - -impl AssociationStats { - pub(crate) fn inc_datas(&self) { - self.n_datas.fetch_add(1, Ordering::SeqCst); - } - - pub(crate) fn get_num_datas(&self) -> u64 { - self.n_datas.load(Ordering::SeqCst) - } - - pub(crate) fn inc_sacks(&self) { - self.n_sacks.fetch_add(1, Ordering::SeqCst); - } - - pub(crate) fn get_num_sacks(&self) -> u64 { - self.n_sacks.load(Ordering::SeqCst) - } - - pub(crate) fn inc_t3timeouts(&self) { - self.n_t3timeouts.fetch_add(1, Ordering::SeqCst); - } - - pub(crate) fn get_num_t3timeouts(&self) -> u64 { - self.n_t3timeouts.load(Ordering::SeqCst) - } - - pub(crate) fn inc_ack_timeouts(&self) { - self.n_ack_timeouts.fetch_add(1, Ordering::SeqCst); - } - - pub(crate) fn get_num_ack_timeouts(&self) -> u64 { - self.n_ack_timeouts.load(Ordering::SeqCst) - } - - pub(crate) fn inc_fast_retrans(&self) { - self.n_fast_retrans.fetch_add(1, Ordering::SeqCst); - } - - pub(crate) fn get_num_fast_retrans(&self) -> u64 { - self.n_fast_retrans.load(Ordering::SeqCst) - } - - pub(crate) fn reset(&self) { - self.n_datas.store(0, Ordering::SeqCst); - self.n_sacks.store(0, Ordering::SeqCst); - self.n_t3timeouts.store(0, Ordering::SeqCst); - self.n_ack_timeouts.store(0, Ordering::SeqCst); - self.n_fast_retrans.store(0, Ordering::SeqCst); - } -} diff --git a/sctp/src/association/association_test.rs b/sctp/src/association/association_test.rs deleted file mode 100644 index 069d35773..000000000 --- a/sctp/src/association/association_test.rs +++ /dev/null @@ -1,2616 +0,0 @@ -// Silence warning on `for i in 0..vec.len() { … }`: -#![allow(clippy::needless_range_loop)] - -use std::io; -use std::net::{Shutdown, SocketAddr}; -use std::str::FromStr; -use std::time::Duration; - -use async_trait::async_trait; -use tokio::net::UdpSocket; -use util::conn::conn_bridge::*; -use util::conn::conn_pipe::pipe; -use util::conn::*; - -use super::*; -use crate::chunk::chunk_selective_ack::GapAckBlock; -use crate::stream::*; - -async fn create_new_association_pair( - br: &Arc, - ca: Arc, - cb: Arc, - ack_mode: AckMode, - recv_buf_size: u32, -) -> Result<(Association, Association)> { - let (handshake0ch_tx, mut handshake0ch_rx) = mpsc::channel(1); - let (handshake1ch_tx, mut handshake1ch_rx) = mpsc::channel(1); - let (closed_tx, mut closed_rx0) = broadcast::channel::<()>(1); - let mut closed_rx1 = closed_tx.subscribe(); - - // Setup client - tokio::spawn(async move { - let client = Association::client(Config { - net_conn: ca, - max_receive_buffer_size: recv_buf_size, - max_message_size: 0, - name: "client".to_owned(), - }) - .await; - - let _ = handshake0ch_tx.send(client).await; - let _ = closed_rx0.recv().await; - - Result::<()>::Ok(()) - }); - - // Setup server - tokio::spawn(async move { - let server = Association::server(Config { - net_conn: cb, - max_receive_buffer_size: recv_buf_size, - max_message_size: 0, - name: "server".to_owned(), - }) - .await; - - let _ = handshake1ch_tx.send(server).await; - let _ = closed_rx1.recv().await; - - Result::<()>::Ok(()) - }); - - let mut client = None; - let mut server = None; - let mut a0handshake_done = false; - let mut a1handshake_done = false; - let mut i = 0; - while (!a0handshake_done || !a1handshake_done) && i < 100 { - br.tick().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - r0 = handshake0ch_rx.recv() => { - if let Ok(c) = r0.unwrap() { - client = Some(c); - } - a0handshake_done = true; - }, - r1 = handshake1ch_rx.recv() => { - if let Ok(s) = r1.unwrap() { - server = Some(s); - } - a1handshake_done = true; - }, - }; - i += 1; - } - - if !a0handshake_done || !a1handshake_done { - return Err(Error::Other("handshake failed".to_owned())); - } - - drop(closed_tx); - - let (client, server) = (client.unwrap(), server.unwrap()); - { - let mut ai = client.association_internal.lock().await; - ai.ack_mode = ack_mode; - } - { - let mut ai = server.association_internal.lock().await; - ai.ack_mode = ack_mode; - } - - Ok((client, server)) -} - -async fn close_association_pair(br: &Arc, client: Association, server: Association) { - let (handshake0ch_tx, mut handshake0ch_rx) = mpsc::channel(1); - let (handshake1ch_tx, mut handshake1ch_rx) = mpsc::channel(1); - let (closed_tx, mut closed_rx0) = broadcast::channel::<()>(1); - let mut closed_rx1 = closed_tx.subscribe(); - - // Close client - tokio::spawn(async move { - client.close().await?; - let _ = handshake0ch_tx.send(()).await; - let _ = closed_rx0.recv().await; - - Result::<()>::Ok(()) - }); - - // Close server - tokio::spawn(async move { - server.close().await?; - let _ = handshake1ch_tx.send(()).await; - let _ = closed_rx1.recv().await; - - Result::<()>::Ok(()) - }); - - let mut a0handshake_done = false; - let mut a1handshake_done = false; - let mut i = 0; - while (!a0handshake_done || !a1handshake_done) && i < 100 { - br.tick().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - _ = handshake0ch_rx.recv() => { - a0handshake_done = true; - }, - _ = handshake1ch_rx.recv() => { - a1handshake_done = true; - }, - }; - i += 1; - } - - drop(closed_tx); -} - -async fn flush_buffers(br: &Arc, client: &Association, server: &Association) { - loop { - loop { - let n = br.tick().await; - if n == 0 { - break; - } - } - - { - let (a0, a1) = ( - client.association_internal.lock().await, - server.association_internal.lock().await, - ); - if a0.buffered_amount() == 0 && a1.buffered_amount() == 0 { - break; - } - } - tokio::time::sleep(Duration::from_millis(10)).await; - } -} - -async fn establish_session_pair( - br: &Arc, - client: &Association, - server: &mut Association, - si: u16, -) -> Result<(Arc, Arc)> { - let hello_msg = Bytes::from_static(b"Hello"); - let s0 = client - .open_stream(si, PayloadProtocolIdentifier::Binary) - .await?; - let _ = s0 - .write_sctp(&hello_msg, PayloadProtocolIdentifier::Dcep) - .await?; - - flush_buffers(br, client, server).await; - - let s1 = server.accept_stream().await.unwrap(); - if s0.stream_identifier != s1.stream_identifier { - return Err(Error::Other("SI should match".to_owned())); - } - - br.process().await; - - let mut buf = vec![0u8; 1024]; - let (n, ppi) = s1.read_sctp(&mut buf).await?; - - if n != hello_msg.len() { - return Err(Error::Other("received data must by 3 bytes".to_owned())); - } - - if ppi != PayloadProtocolIdentifier::Dcep { - return Err(Error::Other("unexpected ppi".to_owned())); - } - - if buf[..n] != hello_msg { - return Err(Error::Other("received data mismatch".to_owned())); - } - - flush_buffers(br, client, server).await; - - Ok((s0, s1)) -} - -//use std::io::Write; - -#[cfg(not(target_os = "windows"))] // this times out in CI on windows. -#[tokio::test] -async fn test_assoc_reliable_simple() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - static MSG: Bytes = Bytes::from_static(b"ABC"); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), 0, "incorrect bufferedAmount"); - } - - let n = s0 - .write_sctp(&MSG, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG.len(), "unexpected length of received data"); - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), MSG.len(), "incorrect bufferedAmount"); - } - - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 32]; - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, MSG.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), 0, "incorrect bufferedAmount"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -// NB: This is ignored on Windows due to flakiness with timing/IO interactions. -// TODO: Refactor this and other tests that are disabled for similar reason to not have such issues -#[cfg(not(target_os = "windows"))] -#[tokio::test] -async fn test_assoc_reliable_ordered_reordered() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 2; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - let mut sbufl = vec![0u8; 2000]; - for i in 0..sbufl.len() { - sbufl[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), 0, "incorrect bufferedAmount"); - } - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - tokio::time::sleep(Duration::from_millis(10)).await; - br.reorder(0).await; - br.process().await; - - let mut buf = vec![0u8; 2000]; - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 0, - "unexpected received data" - ); - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reliable_ordered_fragmented_then_defragmented() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 3; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - let mut sbufl = vec![0u8; 2000]; - for i in 0..sbufl.len() { - sbufl[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - s0.set_reliability_params(false, ReliabilityType::Reliable, 0); - s1.set_reliability_params(false, ReliabilityType::Reliable, 0); - - let n = s0 - .write_sctp( - &Bytes::from(sbufl.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbufl.len(), "unexpected length of received data"); - - flush_buffers(&br, &a0, &a1).await; - - let mut rbuf = vec![0u8; 2000]; - let (n, ppi) = s1.read_sctp(&mut rbuf).await?; - assert_eq!(n, sbufl.len(), "unexpected length of received data"); - assert_eq!(&rbuf[..n], &sbufl, "unexpected received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reliable_unordered_fragmented_then_defragmented() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 4; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - let mut sbufl = vec![0u8; 2000]; - for i in 0..sbufl.len() { - sbufl[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - s0.set_reliability_params(true, ReliabilityType::Reliable, 0); - s1.set_reliability_params(true, ReliabilityType::Reliable, 0); - - let n = s0 - .write_sctp( - &Bytes::from(sbufl.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbufl.len(), "unexpected length of received data"); - - flush_buffers(&br, &a0, &a1).await; - - let mut rbuf = vec![0u8; 2000]; - let (n, ppi) = s1.read_sctp(&mut rbuf).await?; - assert_eq!(n, sbufl.len(), "unexpected length of received data"); - assert_eq!(&rbuf[..n], &sbufl, "unexpected received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reliable_unordered_ordered() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 5; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - let mut sbufl = vec![0u8; 2000]; - for i in 0..sbufl.len() { - sbufl[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - s0.set_reliability_params(true, ReliabilityType::Reliable, 0); - s1.set_reliability_params(true, ReliabilityType::Reliable, 0); - - br.reorder_next_nwrites(0, 2); - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 0, - "unexpected received data" - ); - - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -// NB: This is ignored on Windows due to flakiness with timing/IO interactions. -// TODO: Refactor this and other tests that are disabled for similar reason to not have such issues -#[cfg(not(target_os = "windows"))] -#[tokio::test] -async fn test_assoc_reliable_retransmission() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 6; - static MSG1: Bytes = Bytes::from_static(b"ABC"); - static MSG2: Bytes = Bytes::from_static(b"DEFG"); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - { - let mut a = a0.association_internal.lock().await; - a.rto_mgr.set_rto(100, true); - } - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - let n = s0 - .write_sctp(&MSG1, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG1.len(), "unexpected length of received data"); - - let n = s0 - .write_sctp(&MSG2, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG2.len(), "unexpected length of received data"); - - tokio::time::sleep(Duration::from_millis(10)).await; - log::debug!("dropping packet"); - br.drop_offset(0, 0, 1).await; // drop the first packet (second one should be sacked) - - // process packets for 200 msec - for _ in 0..20 { - br.tick().await; - tokio::time::sleep(Duration::from_millis(10)).await; - } - - let mut buf = vec![0u8; 32]; - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, MSG1.len(), "unexpected length of received data"); - assert_eq!(&buf[..n], &MSG1, "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, MSG2.len(), "unexpected length of received data"); - assert_eq!(&buf[..n], &MSG2, "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reliable_short_buffer() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - static MSG: Bytes = Bytes::from_static(b"Hello"); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), 0, "incorrect bufferedAmount"); - } - - let n = s0 - .write_sctp(&MSG, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG.len(), "unexpected length of received data"); - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), MSG.len(), "incorrect bufferedAmount"); - } - - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 3]; - let result = s1.read_sctp(&mut buf).await; - assert!(result.is_err(), "expected error to be ErrShortBuffer"); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrShortBuffer { size: 3 }, - "expected error to be ErrShortBuffer" - ); - } - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), 0, "incorrect bufferedAmount"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_unreliable_rexmit_ordered_no_fragment() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(false, ReliabilityType::Rexmit, 0); - s1.set_reliability_params(false, ReliabilityType::Rexmit, 0); // doesn't matter - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - log::debug!("flush_buffers"); - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_unreliable_rexmit_ordered_fragment() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - let mut sbuf = vec![0u8; 2000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - // lock RTO value at 100 [msec] - let mut a = a0.association_internal.lock().await; - a.rto_mgr.set_rto(100, true); - } - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(false, ReliabilityType::Rexmit, 0); - s1.set_reliability_params(false, ReliabilityType::Rexmit, 0); // doesn't matter - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - //log::debug!("flush_buffers"); - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - //log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - //log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_unreliable_rexmit_unordered_no_fragment() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 2; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(true, ReliabilityType::Rexmit, 0); - s1.set_reliability_params(true, ReliabilityType::Rexmit, 0); // doesn't matter - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - //log::debug!("flush_buffers"); - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - //log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - //log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -// NB: This is ignored on Windows and macOS due to flakiness with timing/IO interactions. -// TODO: Refactor this and other tests that are disabled for similar reason to not have such issues -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -#[tokio::test] -async fn test_assoc_unreliable_rexmit_unordered_fragment() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - let mut sbuf = vec![0u8; 2000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(true, ReliabilityType::Rexmit, 0); - s1.set_reliability_params(true, ReliabilityType::Rexmit, 0); // doesn't matter - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - //log::debug!("flush_buffers"); - tokio::time::sleep(Duration::from_millis(10)).await; - br.drop_offset(0, 0, 2).await; // drop the second fragment of the first chunk (second chunk should be sacked) - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - //log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - //log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - assert_eq!( - q.unordered.len(), - 0, - "should be nothing in the unordered queue" - ); - assert_eq!( - q.unordered_chunks.len(), - 0, - "should be nothing in the unorderedChunks list" - ); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_unreliable_rexmit_timed_ordered() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 3; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(false, ReliabilityType::Timed, 0); - s1.set_reliability_params(false, ReliabilityType::Timed, 0); // doesn't matter - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - //log::debug!("flush_buffers"); - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - //log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - //log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_unreliable_rexmit_timed_unordered() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 3; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // When we set the reliability value to 0 [times], then it will cause - // the chunk to be abandoned immediately after the first transmission. - s0.set_reliability_params(true, ReliabilityType::Timed, 0); - s1.set_reliability_params(true, ReliabilityType::Timed, 0); // doesn't matter - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - sbuf[0..4].copy_from_slice(&0u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - sbuf[0..4].copy_from_slice(&1u32.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - //log::debug!("flush_buffers"); - flush_buffers(&br, &a0, &a1).await; - - let mut buf = vec![0u8; 2000]; - - //log::debug!("read_sctp"); - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - 1, - "unexpected received data" - ); - - //log::debug!("process"); - br.process().await; - - { - let q = s0.reassembly_queue.lock().await; - assert!(!q.is_readable(), "should no longer be readable"); - assert_eq!( - q.unordered.len(), - 0, - "should be nothing in the unordered queue" - ); - assert_eq!( - q.unordered_chunks.len(), - 0, - "should be nothing in the unorderedChunks list" - ); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//TODO: TestAssocT1InitTimer -//TODO: TestAssocT1CookieTimer -//TODO: TestAssocT3RtxTimer - -//use std::io::Write; - -// 1) Send 4 packets. drop the first one. -// 2) Last 3 packet will be received, which triggers fast-retransmission -// 3) The first one is retransmitted, which makes s1 readable -// Above should be done before RTO occurs (fast recovery) -#[tokio::test] -async fn test_assoc_congestion_control_fast_retransmission() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 6; - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::Normal, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - br.drop_next_nwrites(0, 1); // drop the first packet (second one should be sacked) - - for i in 0..4u32 { - sbuf[0..4].copy_from_slice(&i.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - } - - // process packets for 500 msec, assuming that the fast retrans/recover - // should complete within 500 msec. - for _ in 0..50 { - br.tick().await; - tokio::time::sleep(Duration::from_millis(10)).await; - } - - let mut buf = vec![0u8; 3000]; - - // Try to read all 4 packets - for i in 0..4 { - { - let q = s1.reassembly_queue.lock().await; - assert!(q.is_readable(), "should be readable"); - } - - let (n, ppi) = s1.read_sctp(&mut buf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!( - u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), - i, - "unexpected received data" - ); - } - - //br.process().await; - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - assert!(!a.in_fast_recovery, "should not be in fast-recovery"); - - log::debug!("nDATAs : {}", b.stats.get_num_datas()); - log::debug!("nSACKs : {}", a.stats.get_num_sacks()); - log::debug!("nAckTimeouts: {}", b.stats.get_num_ack_timeouts()); - log::debug!("nFastRetrans: {}", a.stats.get_num_fast_retrans()); - - assert_eq!(a.stats.get_num_fast_retrans(), 1, "should be 1"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_congestion_control_congestion_avoidance() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const MAX_RECEIVE_BUFFER_SIZE: u32 = 64 * 1024; - const SI: u16 = 6; - const N_PACKETS_TO_SEND: u32 = 2000; - - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = create_new_association_pair( - &br, - Arc::new(ca), - Arc::new(cb), - AckMode::Normal, - MAX_RECEIVE_BUFFER_SIZE, - ) - .await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - a.stats.reset(); - b.stats.reset(); - } - - for i in 0..N_PACKETS_TO_SEND { - sbuf[0..4].copy_from_slice(&i.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - } - - let mut rbuf = vec![0u8; 3000]; - - // Repeat calling br.Tick() until the buffered amount becomes 0 - let mut n_packets_received = 0u32; - while s0.buffered_amount() > 0 && n_packets_received < N_PACKETS_TO_SEND { - loop { - let n = br.tick().await; - if n == 0 { - break; - } - } - - loop { - let readable = { - let q = s1.reassembly_queue.lock().await; - q.is_readable() - }; - if !readable { - break; - } - let (n, ppi) = s1.read_sctp(&mut rbuf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!( - n_packets_received, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "unexpected length of received data" - ); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - n_packets_received += 1; - } - } - - br.process().await; - - assert_eq!( - n_packets_received, N_PACKETS_TO_SEND, - "unexpected num of packets received" - ); - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - - assert!(!a.in_fast_recovery, "should not be in fast-recovery"); - assert!( - a.cwnd > a.ssthresh, - "should be in congestion avoidance mode" - ); - assert!( - a.ssthresh >= MAX_RECEIVE_BUFFER_SIZE, - "{} should not be less than the initial size of 128KB {}", - a.ssthresh, - MAX_RECEIVE_BUFFER_SIZE - ); - - assert_eq!( - 0, - s1.get_num_bytes_in_reassembly_queue().await, - "reassembly queue should be empty" - ); - - log::debug!("nDATAs : {}", b.stats.get_num_datas()); - log::debug!("nSACKs : {}", a.stats.get_num_sacks()); - log::debug!("nT3Timeouts: {}", a.stats.get_num_t3timeouts()); - - assert_eq!( - b.stats.get_num_datas(), - N_PACKETS_TO_SEND as u64, - "packet count mismatch" - ); - assert!( - a.stats.get_num_sacks() <= N_PACKETS_TO_SEND as u64 / 2, - "too many sacks" - ); - assert_eq!(a.stats.get_num_t3timeouts(), 0, "should be no retransmit"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_congestion_control_slow_reader() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const MAX_RECEIVE_BUFFER_SIZE: u32 = 64 * 1024; - const SI: u16 = 6; - const N_PACKETS_TO_SEND: u32 = 130; - - let mut sbuf = vec![0u8; 1000]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = create_new_association_pair( - &br, - Arc::new(ca), - Arc::new(cb), - AckMode::Normal, - MAX_RECEIVE_BUFFER_SIZE, - ) - .await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - for i in 0..N_PACKETS_TO_SEND { - sbuf[0..4].copy_from_slice(&i.to_be_bytes()); - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - ) - .await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - } - - let mut rbuf = vec![0u8; 3000]; - - // 1. First forward packets to receiver until rwnd becomes 0 - // 2. Wait until the sender's cwnd becomes 1*MTU (RTO occurred) - // 3. Stat reading a1's data - let mut n_packets_received = 0u32; - let mut has_rtoed = false; - while s0.buffered_amount() > 0 && n_packets_received < N_PACKETS_TO_SEND { - loop { - let n = br.tick().await; - if n == 0 { - break; - } - } - - if !has_rtoed { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - - let rwnd = b.get_my_receiver_window_credit().await; - let cwnd = a.cwnd; - if cwnd > a.mtu || rwnd > 0 { - // Do not read until a1.getMyReceiverWindowCredit() becomes zero - continue; - } - - has_rtoed = true; - } - - loop { - let readable = { - let q = s1.reassembly_queue.lock().await; - q.is_readable() - }; - if !readable { - break; - } - let (n, ppi) = s1.read_sctp(&mut rbuf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!( - n_packets_received, - u32::from_be_bytes([rbuf[0], rbuf[1], rbuf[2], rbuf[3]]), - "unexpected length of received data" - ); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - n_packets_received += 1; - } - - tokio::time::sleep(Duration::from_millis(4)).await; - } - - br.process().await; - - assert_eq!( - n_packets_received, N_PACKETS_TO_SEND, - "unexpected num of packets received" - ); - assert_eq!( - s1.get_num_bytes_in_reassembly_queue().await, - 0, - "reassembly queue should be empty" - ); - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - - log::debug!("nDATAs : {}", b.stats.get_num_datas()); - log::debug!("nSACKs : {}", a.stats.get_num_sacks()); - log::debug!("nAckTimeouts: {}", b.stats.get_num_ack_timeouts()); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -/*FIXME -use std::io::Write; - -#[tokio::test] -async fn test_assoc_delayed_ack() -> Result<()> { - env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init(); - - const SI: u16 = 6; - let mut sbuf = vec![0u8; 1000]; - let mut rbuf = vec![0u8; 1500]; - for i in 0..sbuf.len() { - sbuf[i] = (i & 0xff) as u8; - } - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::AlwaysDelay, 0) - .await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - a.stats.reset(); - b.stats.reset(); - } - - let n = s0 - .write_sctp( - &Bytes::from(sbuf.clone()), - PayloadProtocolIdentifier::Binary, - )?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - - // Repeat calling br.Tick() until the buffered amount becomes 0 - let since = SystemTime::now(); - let mut n_packets_received = 0; - while s0.buffered_amount() > 0 { - loop { - let n = br.tick().await; - if n == 0 { - break; - } - } - - loop { - let readable = { - let q = s1.reassembly_queue.lock().await; - q.is_readable() - }; - if !readable { - break; - } - let (n, ppi) = s1.read_sctp(&mut rbuf).await?; - assert_eq!(n, sbuf.len(), "unexpected length of received data"); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - - n_packets_received += 1; - } - } - let delay = (SystemTime::now().duration_since(since).unwrap().as_millis() as f64) / 1000.0; - log::debug!("received in {} seconds", delay); - assert!(delay >= 0.2, "should be >= 200msec"); - - br.process().await; - - assert_eq!(n_packets_received, 1, "unexpected num of packets received"); - assert_eq!( - s1.get_num_bytes_in_reassembly_queue().await, - 0, - "reassembly queue should be empty" - ); - - { - let a = a0.association_internal.lock().await; - let b = a1.association_internal.lock().await; - - log::debug!("nDATAs : {}", b.stats.get_num_datas()); - log::debug!("nSACKs : {}", a.stats.get_num_sacks()); - log::debug!("nAckTimeouts: {}", b.stats.get_num_ack_timeouts()); - - assert_eq!(b.stats.get_num_datas(), 1, "DATA chunk count mismatch"); - assert_eq!( - a.stats.get_num_sacks(), - b.stats.get_num_datas(), - "sack count should be equal to the number of data chunks" - ); - assert_eq!( - b.stats.get_num_ack_timeouts(), - 1, - "ackTimeout count mismatch" - ); - assert_eq!(a.stats.get_num_t3timeouts(), 0, "should be no retransmit"); - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} -*/ - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reset_close_one_way() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - static MSG: Bytes = Bytes::from_static(b"ABC"); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - assert_eq!(0, a.buffered_amount(), "incorrect bufferedAmount"); - } - - let n = s0 - .write_sctp(&MSG, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG.len(), "unexpected length of received data"); - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), MSG.len(), "incorrect bufferedAmount"); - } - - log::debug!("s0.shutdown"); - s0.shutdown(Shutdown::Both).await?; // send reset - - let (done_ch_tx, mut done_ch_rx) = mpsc::channel(1); - let mut buf = vec![0u8; 32]; - - tokio::spawn(async move { - loop { - log::debug!("s1.read_sctp begin"); - match s1.read_sctp(&mut buf).await { - Ok((0, PayloadProtocolIdentifier::Unknown)) => { - log::debug!("s1.read_sctp EOF"); - let _ = done_ch_tx.send(Some(Error::ErrEof)).await; - break; - } - Ok((n, ppi)) => { - log::debug!("s1.read_sctp done with {:?}", &buf[..n]); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!(n, MSG.len(), "unexpected length of received data"); - let _ = done_ch_tx.send(None).await; - } - Err(err) => { - log::debug!("s1.read_sctp err {:?}", err); - let _ = done_ch_tx.send(Some(err)).await; - break; - } - } - } - }); - - loop { - br.process().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - result = done_ch_rx.recv() => { - log::debug!("s1. {:?}", result); - if let Some(err_opt) = result { - if err_opt.is_some() { - break; - } - } else { - break; - } - } - } - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_reset_close_both_ways() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - static MSG: Bytes = Bytes::from_static(b"ABC"); - - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let (s0, s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - { - let a = a0.association_internal.lock().await; - assert_eq!(0, a.buffered_amount(), "incorrect bufferedAmount"); - } - - let n = s0 - .write_sctp(&MSG, PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, MSG.len(), "unexpected length of received data"); - { - let a = a0.association_internal.lock().await; - assert_eq!(a.buffered_amount(), MSG.len(), "incorrect bufferedAmount"); - } - - log::debug!("s0.shutdown"); - s0.shutdown(Shutdown::Both).await?; // send reset - - let (done_ch_tx, mut done_ch_rx) = mpsc::channel(1); - let done_ch_tx = Arc::new(done_ch_tx); - - let done_ch_tx1 = Arc::clone(&done_ch_tx); - let ss1 = Arc::clone(&s1); - tokio::spawn(async move { - let mut buf = vec![0u8; 32]; - loop { - log::debug!("s1.read_sctp begin"); - match ss1.read_sctp(&mut buf).await { - Ok((0, PayloadProtocolIdentifier::Unknown)) => { - log::debug!("s1.read_sctp EOF"); - let _ = done_ch_tx1.send(Some(Error::ErrEof)).await; - break; - } - Ok((n, ppi)) => { - log::debug!("s1.read_sctp done with {:?}", &buf[..n]); - assert_eq!(ppi, PayloadProtocolIdentifier::Binary, "unexpected ppi"); - assert_eq!(n, MSG.len(), "unexpected length of received data"); - let _ = done_ch_tx1.send(None).await; - } - Err(err) => { - log::debug!("s1.read_sctp err {:?}", err); - let _ = done_ch_tx1.send(Some(err)).await; - break; - } - } - } - }); - - loop { - br.process().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - result = done_ch_rx.recv() => { - log::debug!("s1. {:?}", result); - if let Some(err_opt) = result { - if err_opt.is_some() { - break; - } - } else { - break; - } - } - } - } - - log::debug!("s1.shutdown"); - s1.shutdown(Shutdown::Both).await?; // send reset - - let done_ch_tx0 = Arc::clone(&done_ch_tx); - tokio::spawn(async move { - let mut buf = vec![0u8; 32]; - - log::debug!("s.read_sctp begin"); - match s0.read_sctp(&mut buf).await { - Ok((0, PayloadProtocolIdentifier::Unknown)) => { - log::debug!("s0.read_sctp EOF"); - let _ = done_ch_tx0.send(Some(Error::ErrEof)).await; - } - Ok(_) => { - panic!("must be error"); - } - Err(err) => { - log::debug!("s0.read_sctp err {:?}", err); - let _ = done_ch_tx0.send(Some(err)).await; - } - } - }); - - loop { - br.process().await; - - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() =>{}, - result = done_ch_rx.recv() => { - log::debug!("s0. {:?}", result); - if let Some(err_opt) = result { - if err_opt.is_some() { - break; - } else { - panic!("must be error"); - } - } else { - break; - } - } - } - } - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_assoc_abort() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - const SI: u16 = 1; - let (br, ca, cb) = Bridge::new(0, None, None); - - let (a0, mut a1) = - create_new_association_pair(&br, Arc::new(ca), Arc::new(cb), AckMode::NoDelay, 0).await?; - - let abort = ChunkAbort { - error_causes: vec![ErrorCauseProtocolViolation { - code: PROTOCOL_VIOLATION, - ..Default::default() - }], - }; - - let packet = { - let a = a0.association_internal.lock().await; - a.create_packet(vec![Box::new(abort)]).marshal()? - }; - - let (_s0, _s1) = establish_session_pair(&br, &a0, &mut a1, SI).await?; - - // Both associations are established - assert_eq!(a0.get_state(), AssociationState::Established); - assert_eq!(a1.get_state(), AssociationState::Established); - - let result = a0.net_conn.send(&packet).await; - assert!(result.is_ok(), "must be ok"); - - flush_buffers(&br, &a0, &a1).await; - - // There is a little delay before changing the state to closed - tokio::time::sleep(Duration::from_millis(10)).await; - - // The receiving association should be closed because it got an ABORT - assert_eq!(a0.get_state(), AssociationState::Established); - assert_eq!(a1.get_state(), AssociationState::Closed); - - close_association_pair(&br, a0, a1).await; - - Ok(()) -} - -struct FakeEchoConn { - wr_tx: Mutex>>, - rd_rx: Mutex>>, - bytes_sent: AtomicUsize, - bytes_received: AtomicUsize, -} - -impl FakeEchoConn { - fn type_erased() -> impl Conn { - Self::default() - } -} - -impl Default for FakeEchoConn { - fn default() -> Self { - let (wr_tx, rd_rx) = mpsc::channel(1); - FakeEchoConn { - wr_tx: Mutex::new(wr_tx), - rd_rx: Mutex::new(rd_rx), - bytes_sent: AtomicUsize::new(0), - bytes_received: AtomicUsize::new(0), - } - } -} - -type UResult = std::result::Result; - -#[async_trait] -impl Conn for FakeEchoConn { - async fn connect(&self, _addr: SocketAddr) -> UResult<()> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, b: &mut [u8]) -> UResult { - let mut rd_rx = self.rd_rx.lock().await; - let v = match rd_rx.recv().await { - Some(v) => v, - None => { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "Unexpected EOF").into()) - } - }; - let l = std::cmp::min(v.len(), b.len()); - b[..l].copy_from_slice(&v[..l]); - self.bytes_received.fetch_add(l, Ordering::SeqCst); - Ok(l) - } - - async fn recv_from(&self, _buf: &mut [u8]) -> UResult<(usize, SocketAddr)> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn send(&self, b: &[u8]) -> UResult { - let wr_tx = self.wr_tx.lock().await; - match wr_tx.send(b.to_vec()).await { - Ok(_) => {} - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err.to_string()).into()), - }; - self.bytes_sent.fetch_add(b.len(), Ordering::SeqCst); - Ok(b.len()) - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> UResult { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> UResult { - Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "Addr Not Available").into()) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> UResult<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -//use std::io::Write; - -#[tokio::test] -async fn test_stats() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let conn = Arc::new(FakeEchoConn::type_erased()); - let a = Association::client(Config { - net_conn: Arc::clone(&conn) as Arc, - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }) - .await?; - - if let Some(conn) = conn.as_any().downcast_ref::() { - assert_eq!( - conn.bytes_received.load(Ordering::SeqCst), - a.bytes_received() - ); - assert_eq!(conn.bytes_sent.load(Ordering::SeqCst), a.bytes_sent()); - } else { - panic!("must be FakeEchoConn"); - } - - Ok(()) -} - -async fn create_assocs() -> Result<(Association, Association)> { - let addr1 = SocketAddr::from_str("0.0.0.0:0").unwrap(); - let addr2 = SocketAddr::from_str("0.0.0.0:0").unwrap(); - - let udp1 = UdpSocket::bind(addr1).await.unwrap(); - let udp2 = UdpSocket::bind(addr2).await.unwrap(); - - udp1.connect(udp2.local_addr().unwrap()).await.unwrap(); - udp2.connect(udp1.local_addr().unwrap()).await.unwrap(); - - let (a1chan_tx, mut a1chan_rx) = mpsc::channel(1); - let (a2chan_tx, mut a2chan_rx) = mpsc::channel(1); - - tokio::spawn(async move { - let a = Association::client(Config { - net_conn: Arc::new(udp1), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "client".to_owned(), - }) - .await?; - - let _ = a1chan_tx.send(a).await; - - Result::<()>::Ok(()) - }); - - tokio::spawn(async move { - let a = Association::server(Config { - net_conn: Arc::new(udp2), - max_receive_buffer_size: 0, - max_message_size: 0, - name: "server".to_owned(), - }) - .await?; - - let _ = a2chan_tx.send(a).await; - - Result::<()>::Ok(()) - }); - - let timer1 = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(timer1); - let a1 = tokio::select! { - _ = timer1.as_mut() =>{ - panic!("timed out waiting for a1"); - }, - a1 = a1chan_rx.recv() => { - a1.unwrap() - } - }; - - let timer2 = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(timer2); - let a2 = tokio::select! { - _ = timer2.as_mut() =>{ - panic!("timed out waiting for a2"); - }, - a2 = a2chan_rx.recv() => { - a2.unwrap() - } - }; - - Ok((a1, a2)) -} - -//use std::io::Write; -//TODO: remove this conditional test -#[cfg(not(target_os = "windows"))] -#[tokio::test] -async fn test_association_shutdown() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let (a1, a2) = create_assocs().await?; - - let s11 = a1.open_stream(1, PayloadProtocolIdentifier::String).await?; - let s21 = a2.open_stream(1, PayloadProtocolIdentifier::String).await?; - - let test_data = Bytes::from_static(b"test"); - - let n = s11.write(&test_data).await?; - assert_eq!(n, test_data.len()); - - let mut buf = vec![0u8; test_data.len()]; - let n = s21.read(&mut buf).await?; - assert_eq!(n, test_data.len()); - assert_eq!(&buf[0..n], &test_data); - - if let Ok(result) = tokio::time::timeout(Duration::from_secs(1), a1.shutdown()).await { - assert!(result.is_ok(), "shutdown should be ok"); - } else { - panic!("shutdown timeout"); - } - - { - let mut close_loop_ch_rx = a2.close_loop_ch_rx.lock().await; - - // Wait for close read loop channels to prevent flaky tests. - let timer2 = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(timer2); - tokio::select! { - _ = timer2.as_mut() =>{ - panic!("timed out waiting for a2 read loop to close"); - }, - _ = close_loop_ch_rx.recv() => { - log::debug!("recv a2.close_loop_ch_rx"); - } - }; - } - Ok(()) -} - -//use std::io::Write; -//TODO: remove this conditional test -#[cfg(not(target_os = "windows"))] -#[tokio::test] -async fn test_association_shutdown_during_write() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let (a1, a2) = create_assocs().await?; - - let s11 = a1.open_stream(1, PayloadProtocolIdentifier::String).await?; - let s21 = a2.open_stream(1, PayloadProtocolIdentifier::String).await?; - - let (writing_done_tx, mut writing_done_rx) = mpsc::channel::<()>(1); - let ss21 = Arc::clone(&s21); - tokio::spawn(async move { - let mut i = 0; - while ss21.write(&Bytes::from(vec![i])).await.is_ok() { - if i == 255 { - i = 0; - } else { - i += 1; - } - - if i % 100 == 0 { - tokio::time::sleep(Duration::from_millis(20)).await; - } - } - - drop(writing_done_tx); - }); - - let test_data = Bytes::from_static(b"test"); - - let n = s11.write(&test_data).await?; - assert_eq!(n, test_data.len()); - - let mut buf = vec![0u8; test_data.len()]; - let n = s21.read(&mut buf).await?; - assert_eq!(n, test_data.len()); - assert_eq!(&buf[0..n], &test_data); - - { - let mut close_loop_ch_rx = a1.close_loop_ch_rx.lock().await; - tokio::select! { - res = tokio::time::timeout(Duration::from_secs(1), a1.shutdown()) => { - if let Ok(result) = res { - assert!(result.is_ok(), "shutdown should be ok"); - } else { - panic!("shutdown timeout"); - } - } - _ = writing_done_rx.recv() => { - log::debug!("writing_done_rx"); - let result = close_loop_ch_rx.recv().await; - log::debug!("a1.close_loop_ch_rx.recv: {:?}", result); - }, - }; - } - - { - let mut close_loop_ch_rx = a2.close_loop_ch_rx.lock().await; - // Wait for close read loop channels to prevent flaky tests. - let timer2 = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(timer2); - tokio::select! { - _ = timer2.as_mut() =>{ - panic!("timed out waiting for a2 read loop to close"); - }, - _ = close_loop_ch_rx.recv() => { - log::debug!("recv a2.close_loop_ch_rx"); - } - }; - } - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_association_handle_packet_before_init() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let tests = vec![ - ( - "InitAck", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::new(ChunkInit { - is_ack: true, - initiate_tag: 1, - num_inbound_streams: 1, - num_outbound_streams: 1, - advertised_receiver_window_credit: 1500, - ..Default::default() - })], - }, - ), - ( - "Abort", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "CoockeEcho", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "HeartBeat", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "PayloadData", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "Sack", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::new(ChunkSelectiveAck { - cumulative_tsn_ack: 1000, - advertised_receiver_window_credit: 1500, - gap_ack_blocks: vec![GapAckBlock { - start: 100, - end: 200, - }], - ..Default::default() - })], - }, - ), - ( - "Reconfig", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::new(ChunkReconfig { - param_a: Some(Box::::default()), - param_b: Some(Box::::default()), - })], - }, - ), - ( - "ForwardTSN", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::new(ChunkForwardTsn { - new_cumulative_tsn: 100, - ..Default::default() - })], - }, - ), - ( - "Error", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "Shutdown", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "ShutdownAck", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ( - "ShutdownComplete", - Packet { - source_port: 1, - destination_port: 1, - verification_tag: 0, - chunks: vec![Box::::default()], - }, - ), - ]; - - for (name, packet) in tests { - log::debug!("testing {}", name); - - let (a_conn, charlie_conn) = pipe(); - - let (a, _) = Association::new( - Config { - net_conn: Arc::new(a_conn), - max_message_size: 0, - max_receive_buffer_size: 0, - name: "client".to_owned(), - }, - true, - ) - .await - .unwrap(); - - let packet = packet.marshal()?; - let result = charlie_conn.send(&packet).await; - assert!(result.is_ok(), "{name} charlie_conn.send should be ok"); - - // Should not panic. - tokio::time::sleep(Duration::from_millis(100)).await; - - a.close().await.unwrap(); - } - - Ok(()) -} diff --git a/sctp/src/association/mod.rs b/sctp/src/association/mod.rs deleted file mode 100644 index cec94accb..000000000 --- a/sctp/src/association/mod.rs +++ /dev/null @@ -1,626 +0,0 @@ -#[cfg(test)] -mod association_test; - -mod association_internal; -mod association_stats; - -use std::collections::{HashMap, VecDeque}; -use std::fmt; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use association_internal::*; -use association_stats::*; -use bytes::{Bytes, BytesMut}; -use portable_atomic::{AtomicBool, AtomicU32, AtomicU8, AtomicUsize}; -use rand::random; -use tokio::sync::{broadcast, mpsc, Mutex}; -use util::Conn; - -use crate::chunk::chunk_abort::ChunkAbort; -use crate::chunk::chunk_cookie_ack::ChunkCookieAck; -use crate::chunk::chunk_cookie_echo::ChunkCookieEcho; -use crate::chunk::chunk_error::ChunkError; -use crate::chunk::chunk_forward_tsn::{ChunkForwardTsn, ChunkForwardTsnStream}; -use crate::chunk::chunk_heartbeat::ChunkHeartbeat; -use crate::chunk::chunk_heartbeat_ack::ChunkHeartbeatAck; -use crate::chunk::chunk_init::ChunkInit; -use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; -use crate::chunk::chunk_reconfig::ChunkReconfig; -use crate::chunk::chunk_selective_ack::ChunkSelectiveAck; -use crate::chunk::chunk_shutdown::ChunkShutdown; -use crate::chunk::chunk_shutdown_ack::ChunkShutdownAck; -use crate::chunk::chunk_shutdown_complete::ChunkShutdownComplete; -use crate::chunk::chunk_type::*; -use crate::chunk::Chunk; -use crate::error::{Error, Result}; -use crate::error_cause::*; -use crate::packet::Packet; -use crate::param::param_heartbeat_info::ParamHeartbeatInfo; -use crate::param::param_outgoing_reset_request::ParamOutgoingResetRequest; -use crate::param::param_reconfig_response::{ParamReconfigResponse, ReconfigResult}; -use crate::param::param_state_cookie::ParamStateCookie; -use crate::param::param_supported_extensions::ParamSupportedExtensions; -use crate::param::Param; -use crate::queue::control_queue::ControlQueue; -use crate::queue::payload_queue::PayloadQueue; -use crate::queue::pending_queue::PendingQueue; -use crate::stream::*; -use crate::timer::ack_timer::*; -use crate::timer::rtx_timer::*; -use crate::util::*; - -pub(crate) const RECEIVE_MTU: usize = 8192; -/// MTU for inbound packet (from DTLS) -pub(crate) const INITIAL_MTU: u32 = 1228; -/// initial MTU for outgoing packets (to DTLS) -pub(crate) const INITIAL_RECV_BUF_SIZE: u32 = 1024 * 1024; -pub(crate) const COMMON_HEADER_SIZE: u32 = 12; -pub(crate) const DATA_CHUNK_HEADER_SIZE: u32 = 16; -pub(crate) const DEFAULT_MAX_MESSAGE_SIZE: u32 = 65536; - -/// other constants -pub(crate) const ACCEPT_CH_SIZE: usize = 16; - -/// association state enums -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum AssociationState { - Closed = 0, - CookieWait = 1, - CookieEchoed = 2, - Established = 3, - ShutdownAckSent = 4, - ShutdownPending = 5, - ShutdownReceived = 6, - ShutdownSent = 7, -} - -impl From for AssociationState { - fn from(v: u8) -> AssociationState { - match v { - 1 => AssociationState::CookieWait, - 2 => AssociationState::CookieEchoed, - 3 => AssociationState::Established, - 4 => AssociationState::ShutdownAckSent, - 5 => AssociationState::ShutdownPending, - 6 => AssociationState::ShutdownReceived, - 7 => AssociationState::ShutdownSent, - _ => AssociationState::Closed, - } - } -} - -impl fmt::Display for AssociationState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - AssociationState::Closed => "Closed", - AssociationState::CookieWait => "CookieWait", - AssociationState::CookieEchoed => "CookieEchoed", - AssociationState::Established => "Established", - AssociationState::ShutdownPending => "ShutdownPending", - AssociationState::ShutdownSent => "ShutdownSent", - AssociationState::ShutdownReceived => "ShutdownReceived", - AssociationState::ShutdownAckSent => "ShutdownAckSent", - }; - write!(f, "{s}") - } -} - -/// retransmission timer IDs -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum RtxTimerId { - #[default] - T1Init, - T1Cookie, - T2Shutdown, - T3RTX, - Reconfig, -} - -impl fmt::Display for RtxTimerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RtxTimerId::T1Init => "T1Init", - RtxTimerId::T1Cookie => "T1Cookie", - RtxTimerId::T2Shutdown => "T2Shutdown", - RtxTimerId::T3RTX => "T3RTX", - RtxTimerId::Reconfig => "Reconfig", - }; - write!(f, "{s}") - } -} - -/// ack mode (for testing) -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum AckMode { - #[default] - Normal, - NoDelay, - AlwaysDelay, -} - -impl fmt::Display for AckMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - AckMode::Normal => "Normal", - AckMode::NoDelay => "NoDelay", - AckMode::AlwaysDelay => "AlwaysDelay", - }; - write!(f, "{s}") - } -} - -/// ack transmission state -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum AckState { - #[default] - Idle, // ack timer is off - Immediate, // will send ack immediately - Delay, // ack timer is on (ack is being delayed) -} - -impl fmt::Display for AckState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - AckState::Idle => "Idle", - AckState::Immediate => "Immediate", - AckState::Delay => "Delay", - }; - write!(f, "{s}") - } -} - -/// Config collects the arguments to create_association construction into -/// a single structure -pub struct Config { - pub net_conn: Arc, - pub max_receive_buffer_size: u32, - pub max_message_size: u32, - pub name: String, -} - -///Association represents an SCTP association -///13.2. Parameters Necessary per Association (i.e., the TCB) -///Peer : Tag value to be sent in every packet and is received -///Verification: in the INIT or INIT ACK chunk. -///Tag : -/// -///My : Tag expected in every inbound packet and sent in the -///Verification: INIT or INIT ACK chunk. -/// -///Tag : -///State : A state variable indicating what state the association -/// : is in, i.e., COOKIE-WAIT, COOKIE-ECHOED, ESTABLISHED, -/// : SHUTDOWN-PENDING, SHUTDOWN-SENT, SHUTDOWN-RECEIVED, -/// : SHUTDOWN-ACK-SENT. -/// -/// No Closed state is illustrated since if a -/// association is Closed its TCB SHOULD be removed. -pub struct Association { - name: String, - state: Arc, - max_message_size: Arc, - inflight_queue_length: Arc, - will_send_shutdown: Arc, - awake_write_loop_ch: Arc>, - close_loop_ch_rx: Mutex>, - accept_ch_rx: Mutex>>, - net_conn: Arc, - bytes_received: Arc, - bytes_sent: Arc, - - pub(crate) association_internal: Arc>, -} - -impl Association { - /// server accepts a SCTP stream over a conn - pub async fn server(config: Config) -> Result { - let (a, mut handshake_completed_ch_rx) = Association::new(config, false).await?; - - if let Some(err_opt) = handshake_completed_ch_rx.recv().await { - if let Some(err) = err_opt { - Err(err) - } else { - Ok(a) - } - } else { - Err(Error::ErrAssociationHandshakeClosed) - } - } - - /// Client opens a SCTP stream over a conn - pub async fn client(config: Config) -> Result { - let (a, mut handshake_completed_ch_rx) = Association::new(config, true).await?; - - if let Some(err_opt) = handshake_completed_ch_rx.recv().await { - if let Some(err) = err_opt { - Err(err) - } else { - Ok(a) - } - } else { - Err(Error::ErrAssociationHandshakeClosed) - } - } - - /// Shutdown initiates the shutdown sequence. The method blocks until the - /// shutdown sequence is completed and the connection is closed, or until the - /// passed context is done, in which case the context's error is returned. - pub async fn shutdown(&self) -> Result<()> { - log::debug!("[{}] closing association..", self.name); - - let state = self.get_state(); - if state != AssociationState::Established { - return Err(Error::ErrShutdownNonEstablished); - } - - // Attempt a graceful shutdown. - self.set_state(AssociationState::ShutdownPending); - - if self.inflight_queue_length.load(Ordering::SeqCst) == 0 { - // No more outstanding, send shutdown. - self.will_send_shutdown.store(true, Ordering::SeqCst); - let _ = self.awake_write_loop_ch.try_send(()); - self.set_state(AssociationState::ShutdownSent); - } - - { - let mut close_loop_ch_rx = self.close_loop_ch_rx.lock().await; - let _ = close_loop_ch_rx.recv().await; - } - - Ok(()) - } - - /// Close ends the SCTP Association and cleans up any state - pub async fn close(&self) -> Result<()> { - log::debug!("[{}] closing association..", self.name); - - let _ = self.net_conn.close().await; - - let mut ai = self.association_internal.lock().await; - ai.close().await - } - - async fn new(config: Config, is_client: bool) -> Result<(Self, mpsc::Receiver>)> { - let net_conn = Arc::clone(&config.net_conn); - - let (awake_write_loop_ch_tx, awake_write_loop_ch_rx) = mpsc::channel(1); - let (accept_ch_tx, accept_ch_rx) = mpsc::channel(ACCEPT_CH_SIZE); - let (handshake_completed_ch_tx, handshake_completed_ch_rx) = mpsc::channel(1); - let (close_loop_ch_tx, close_loop_ch_rx) = broadcast::channel(1); - let (close_loop_ch_rx1, close_loop_ch_rx2) = - (close_loop_ch_tx.subscribe(), close_loop_ch_tx.subscribe()); - let awake_write_loop_ch = Arc::new(awake_write_loop_ch_tx); - - let ai = AssociationInternal::new( - config, - close_loop_ch_tx, - accept_ch_tx, - handshake_completed_ch_tx, - Arc::clone(&awake_write_loop_ch), - ); - - let bytes_received = Arc::new(AtomicUsize::new(0)); - let bytes_sent = Arc::new(AtomicUsize::new(0)); - let name = ai.name.clone(); - let state = Arc::clone(&ai.state); - let max_message_size = Arc::clone(&ai.max_message_size); - let inflight_queue_length = Arc::clone(&ai.inflight_queue_length); - let will_send_shutdown = Arc::clone(&ai.will_send_shutdown); - - let mut init = ChunkInit { - initial_tsn: ai.my_next_tsn, - num_outbound_streams: ai.my_max_num_outbound_streams, - num_inbound_streams: ai.my_max_num_inbound_streams, - initiate_tag: ai.my_verification_tag, - advertised_receiver_window_credit: ai.max_receive_buffer_size, - ..Default::default() - }; - init.set_supported_extensions(); - - let name1 = name.clone(); - let name2 = name.clone(); - - let bytes_received1 = Arc::clone(&bytes_received); - let bytes_sent2 = Arc::clone(&bytes_sent); - - let net_conn1 = Arc::clone(&net_conn); - let net_conn2 = Arc::clone(&net_conn); - - let association_internal = Arc::new(Mutex::new(ai)); - let association_internal1 = Arc::clone(&association_internal); - let association_internal2 = Arc::clone(&association_internal); - - { - let association_internal3 = Arc::clone(&association_internal); - - let mut ai = association_internal.lock().await; - ai.t1init = Some(RtxTimer::new( - Arc::downgrade(&association_internal3), - RtxTimerId::T1Init, - MAX_INIT_RETRANS, - )); - ai.t1cookie = Some(RtxTimer::new( - Arc::downgrade(&association_internal3), - RtxTimerId::T1Cookie, - MAX_INIT_RETRANS, - )); - ai.t2shutdown = Some(RtxTimer::new( - Arc::downgrade(&association_internal3), - RtxTimerId::T2Shutdown, - NO_MAX_RETRANS, - )); // retransmit forever - ai.t3rtx = Some(RtxTimer::new( - Arc::downgrade(&association_internal3), - RtxTimerId::T3RTX, - NO_MAX_RETRANS, - )); // retransmit forever - ai.treconfig = Some(RtxTimer::new( - Arc::downgrade(&association_internal3), - RtxTimerId::Reconfig, - NO_MAX_RETRANS, - )); // retransmit forever - ai.ack_timer = Some(AckTimer::new( - Arc::downgrade(&association_internal3), - ACK_INTERVAL, - )); - } - - tokio::spawn(async move { - Association::read_loop( - name1, - bytes_received1, - net_conn1, - close_loop_ch_rx1, - association_internal1, - ) - .await; - }); - - tokio::spawn(async move { - Association::write_loop( - name2, - bytes_sent2, - net_conn2, - close_loop_ch_rx2, - association_internal2, - awake_write_loop_ch_rx, - ) - .await; - }); - - if is_client { - let mut ai = association_internal.lock().await; - ai.set_state(AssociationState::CookieWait); - ai.stored_init = Some(init); - ai.send_init()?; - let rto = ai.rto_mgr.get_rto(); - if let Some(t1init) = &ai.t1init { - t1init.start(rto).await; - } - } - - Ok(( - Association { - name, - state, - max_message_size, - inflight_queue_length, - will_send_shutdown, - awake_write_loop_ch, - close_loop_ch_rx: Mutex::new(close_loop_ch_rx), - accept_ch_rx: Mutex::new(accept_ch_rx), - net_conn, - bytes_received, - bytes_sent, - association_internal, - }, - handshake_completed_ch_rx, - )) - } - - async fn read_loop( - name: String, - bytes_received: Arc, - net_conn: Arc, - mut close_loop_ch: broadcast::Receiver<()>, - association_internal: Arc>, - ) { - log::debug!("[{}] read_loop entered", name); - - let mut buffer = vec![0u8; RECEIVE_MTU]; - let mut done = false; - let mut n; - while !done { - tokio::select! { - _ = close_loop_ch.recv() => break, - result = net_conn.recv(&mut buffer) => { - match result { - Ok(m) => { - n=m; - } - Err(err) => { - log::warn!("[{}] failed to read packets on net_conn: {}", name, err); - break; - } - } - } - }; - - // Make a buffer sized to what we read, then copy the data we - // read from the underlying transport. We do this because the - // user data is passed to the reassembly queue without - // copying. - log::debug!("[{}] recving {} bytes", name, n); - let inbound = Bytes::from(buffer[..n].to_vec()); - bytes_received.fetch_add(n, Ordering::SeqCst); - - { - let mut ai = association_internal.lock().await; - if let Err(err) = ai.handle_inbound(&inbound).await { - log::warn!("[{}] failed to handle_inbound: {:?}", name, err); - done = true; - } - } - } - - { - let mut ai = association_internal.lock().await; - if let Err(err) = ai.close().await { - log::warn!("[{}] failed to close association: {:?}", name, err); - } - } - - log::debug!("[{}] read_loop exited", name); - } - - async fn write_loop( - name: String, - bytes_sent: Arc, - net_conn: Arc, - mut close_loop_ch: broadcast::Receiver<()>, - association_internal: Arc>, - mut awake_write_loop_ch: mpsc::Receiver<()>, - ) { - log::debug!("[{}] write_loop entered", name); - let done = Arc::new(AtomicBool::new(false)); - let name = Arc::new(name); - - 'outer: while !done.load(Ordering::Relaxed) { - //log::debug!("[{}] gather_outbound begin", name); - let (packets, continue_loop) = { - let mut ai = association_internal.lock().await; - ai.gather_outbound().await - }; - //log::debug!("[{}] gather_outbound done with {}", name, packets.len()); - - let net_conn = Arc::clone(&net_conn); - let bytes_sent = Arc::clone(&bytes_sent); - let name2 = Arc::clone(&name); - let done2 = Arc::clone(&done); - let mut buffer = None; - for raw in packets { - let mut buf = buffer - .take() - .unwrap_or_else(|| BytesMut::with_capacity(16 * 1024)); - - // We do the marshalling work in a blocking task here for a reason: - // If we don't tokio tends to run the write_loop and read_loop of one connection on the same OS thread - // This means that even though we release the lock above, the read_loop isn't able to take it, simply because it is not being scheduled by tokio - // Doing it this way, tokio schedules this work on a dedicated blocking thread, this future is suspended, and the read_loop can make progress - match tokio::task::spawn_blocking(move || raw.marshal_to(&mut buf).map(|_| buf)) - .await - { - Ok(Ok(mut buf)) => { - let raw = buf.as_ref(); - if let Err(err) = net_conn.send(raw.as_ref()).await { - log::warn!("[{}] failed to write packets on net_conn: {}", name2, err); - done2.store(true, Ordering::Relaxed) - } else { - bytes_sent.fetch_add(raw.len(), Ordering::SeqCst); - } - - // Reuse allocation. Have to use options, since spawn blocking can't borrow, has to take ownership. - buf.clear(); - buffer = Some(buf); - } - Ok(Err(err)) => { - log::warn!("[{}] failed to serialize a packet: {:?}", name2, err); - } - Err(err) => { - if err.is_cancelled() { - log::debug!( - "[{}] task cancelled while serializing a packet: {:?}", - name, - err - ); - break 'outer; - } else { - log::error!("[{}] panic while serializing a packet: {:?}", name, err); - } - } - } - //log::debug!("[{}] sending {} bytes done", name, raw.len()); - } - - if !continue_loop { - break; - } - - //log::debug!("[{}] wait awake_write_loop_ch", name); - tokio::select! { - _ = awake_write_loop_ch.recv() =>{} - _ = close_loop_ch.recv() => { - done.store(true, Ordering::Relaxed); - } - }; - //log::debug!("[{}] wait awake_write_loop_ch done", name); - } - - { - let mut ai = association_internal.lock().await; - if let Err(err) = ai.close().await { - log::warn!("[{}] failed to close association: {:?}", name, err); - } - } - - log::debug!("[{}] write_loop exited", name); - } - - /// bytes_sent returns the number of bytes sent - pub fn bytes_sent(&self) -> usize { - self.bytes_sent.load(Ordering::SeqCst) - } - - /// bytes_received returns the number of bytes received - pub fn bytes_received(&self) -> usize { - self.bytes_received.load(Ordering::SeqCst) - } - - /// open_stream opens a stream - pub async fn open_stream( - &self, - stream_identifier: u16, - default_payload_type: PayloadProtocolIdentifier, - ) -> Result> { - let mut ai = self.association_internal.lock().await; - ai.open_stream(stream_identifier, default_payload_type) - } - - /// accept_stream accepts a stream - pub async fn accept_stream(&self) -> Option> { - let mut accept_ch_rx = self.accept_ch_rx.lock().await; - accept_ch_rx.recv().await - } - - /// max_message_size returns the maximum message size you can send. - pub fn max_message_size(&self) -> u32 { - self.max_message_size.load(Ordering::SeqCst) - } - - /// set_max_message_size sets the maximum message size you can send. - pub fn set_max_message_size(&self, max_message_size: u32) { - self.max_message_size - .store(max_message_size, Ordering::SeqCst); - } - - /// set_state atomically sets the state of the Association. - fn set_state(&self, new_state: AssociationState) { - let old_state = AssociationState::from(self.state.swap(new_state as u8, Ordering::SeqCst)); - if new_state != old_state { - log::debug!( - "[{}] state change: '{}' => '{}'", - self.name, - old_state, - new_state, - ); - } - } - - /// get_state atomically returns the state of the Association. - fn get_state(&self) -> AssociationState { - self.state.load(Ordering::SeqCst).into() - } -} diff --git a/sctp/src/chunk/chunk_abort.rs b/sctp/src/chunk/chunk_abort.rs deleted file mode 100644 index 6fc131442..000000000 --- a/sctp/src/chunk/chunk_abort.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::error_cause::*; - -///Abort represents an SCTP Chunk of type ABORT -/// -///The ABORT chunk is sent to the peer of an association to close the -///association. The ABORT chunk may contain Cause Parameters to inform -///the receiver about the reason of the abort. DATA chunks MUST NOT be -///bundled with ABORT. Control chunks (except for INIT, INIT ACK, and -///SHUTDOWN COMPLETE) MAY be bundled with an ABORT, but they MUST be -///placed before the ABORT in the SCTP packet or they will be ignored by -///the receiver. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 6 |Reserved |T| Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| zero or more Error Causes | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkAbort { - pub(crate) error_causes: Vec, -} - -/// String makes chunkAbort printable -impl fmt::Display for ChunkAbort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = vec![self.header().to_string()]; - - for cause in &self.error_causes { - res.push(format!(" - {cause}")); - } - - write!(f, "{}", res.join("\n")) - } -} - -impl Chunk for ChunkAbort { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_ABORT, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_ABORT { - return Err(Error::ErrChunkTypeNotAbort); - } - - let mut error_causes = vec![]; - let mut offset = CHUNK_HEADER_SIZE; - while offset + 4 <= raw.len() { - let e = ErrorCause::unmarshal( - &raw.slice(offset..CHUNK_HEADER_SIZE + header.value_length()), - )?; - offset += e.length(); - error_causes.push(e); - } - - Ok(ChunkAbort { error_causes }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for ec in &self.error_causes { - buf.extend(ec.marshal()); - } - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.error_causes - .iter() - .fold(0, |length, ec| length + ec.length()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_cookie_ack.rs b/sctp/src/chunk/chunk_cookie_ack.rs deleted file mode 100644 index a3548b645..000000000 --- a/sctp/src/chunk/chunk_cookie_ack.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -/// chunkCookieAck represents an SCTP Chunk of type chunkCookieAck -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Type = 11 |Chunk Flags | Length = 4 | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Clone)] -pub(crate) struct ChunkCookieAck; - -/// makes ChunkCookieAck printable -impl fmt::Display for ChunkCookieAck { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkCookieAck { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_COOKIE_ACK, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_COOKIE_ACK { - return Err(Error::ErrChunkTypeNotCookieAck); - } - - Ok(ChunkCookieAck {}) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - 0 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_cookie_echo.rs b/sctp/src/chunk/chunk_cookie_echo.rs deleted file mode 100644 index c49c88a60..000000000 --- a/sctp/src/chunk/chunk_cookie_echo.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -/// CookieEcho represents an SCTP Chunk of type CookieEcho -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Type = 10 |Chunk Flags | Length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Cookie | -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkCookieEcho { - pub(crate) cookie: Bytes, -} - -/// makes ChunkCookieEcho printable -impl fmt::Display for ChunkCookieEcho { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkCookieEcho { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_COOKIE_ECHO, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_COOKIE_ECHO { - return Err(Error::ErrChunkTypeNotCookieEcho); - } - - let cookie = raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - Ok(ChunkCookieEcho { cookie }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.extend(self.cookie.clone()); - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.cookie.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_error.rs b/sctp/src/chunk/chunk_error.rs deleted file mode 100644 index 3ce538b3a..000000000 --- a/sctp/src/chunk/chunk_error.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::error_cause::*; - -///Operation Error (ERROR) (9) -/// -///An endpoint sends this chunk to its peer endpoint to notify it of -///certain error conditions. It contains one or more error causes. An -///Operation Error is not considered fatal in and of itself, but may be -///used with an ERROR chunk to report a fatal condition. It has the -///following parameters: -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 9 | Chunk Flags | Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| one or more Error Causes | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///Chunk Flags: 8 bits -/// Set to 0 on transmit and ignored on receipt. -///Length: 16 bits (unsigned integer) -/// Set to the size of the chunk in bytes, including the chunk header -/// and all the Error Cause fields present. -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkError { - pub(crate) error_causes: Vec, -} - -/// makes ChunkError printable -impl fmt::Display for ChunkError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = vec![self.header().to_string()]; - - for cause in &self.error_causes { - res.push(format!(" - {cause}")); - } - - write!(f, "{}", res.join("\n")) - } -} - -impl Chunk for ChunkError { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_ERROR, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_ERROR { - return Err(Error::ErrChunkTypeNotCtError); - } - - let mut error_causes = vec![]; - let mut offset = CHUNK_HEADER_SIZE; - while offset + 4 <= raw.len() { - let e = ErrorCause::unmarshal( - &raw.slice(offset..CHUNK_HEADER_SIZE + header.value_length()), - )?; - offset += e.length(); - error_causes.push(e); - } - - Ok(ChunkError { error_causes }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for ec in &self.error_causes { - buf.extend(ec.marshal()); - } - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.error_causes - .iter() - .fold(0, |length, ec| length + ec.length()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_forward_tsn.rs b/sctp/src/chunk/chunk_forward_tsn.rs deleted file mode 100644 index 5599460dc..000000000 --- a/sctp/src/chunk/chunk_forward_tsn.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -///This chunk shall be used by the data sender to inform the data -///receiver to adjust its cumulative received TSN point forward because -///some missing TSNs are associated with data chunks that SHOULD NOT be -///transmitted or retransmitted by the sender. -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 192 | Flags = 0x00 | Length = Variable | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| New Cumulative TSN | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Stream-1 | Stream Sequence-1 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Stream-N | Stream Sequence-N | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkForwardTsn { - /// This indicates the new cumulative TSN to the data receiver. Upon - /// the reception of this value, the data receiver MUST consider - /// any missing TSNs earlier than or equal to this value as received, - /// and stop reporting them as gaps in any subsequent SACKs. - pub(crate) new_cumulative_tsn: u32, - pub(crate) streams: Vec, -} - -pub(crate) const NEW_CUMULATIVE_TSN_LENGTH: usize = 4; -pub(crate) const FORWARD_TSN_STREAM_LENGTH: usize = 4; - -/// makes ChunkForwardTsn printable -impl fmt::Display for ChunkForwardTsn { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = vec![self.header().to_string()]; - res.push(format!("New Cumulative TSN: {}", self.new_cumulative_tsn)); - for s in &self.streams { - res.push(format!(" - si={}, ssn={}", s.identifier, s.sequence)); - } - - write!(f, "{}", res.join("\n")) - } -} - -impl Chunk for ChunkForwardTsn { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_FORWARD_TSN, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(buf: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(buf)?; - - if header.typ != CT_FORWARD_TSN { - return Err(Error::ErrChunkTypeNotForwardTsn); - } - - let mut offset = CHUNK_HEADER_SIZE + NEW_CUMULATIVE_TSN_LENGTH; - if buf.len() < offset { - return Err(Error::ErrChunkTooShort); - } - - let reader = &mut buf.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - let new_cumulative_tsn = reader.get_u32(); - - let mut streams = vec![]; - let mut remaining = buf.len() - offset; - while remaining > 0 { - let s = ChunkForwardTsnStream::unmarshal( - &buf.slice(offset..CHUNK_HEADER_SIZE + header.value_length()), - )?; - offset += s.value_length(); - remaining -= s.value_length(); - streams.push(s); - } - - Ok(ChunkForwardTsn { - new_cumulative_tsn, - streams, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - - writer.put_u32(self.new_cumulative_tsn); - - for s in &self.streams { - writer.extend(s.marshal()?); - } - - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - NEW_CUMULATIVE_TSN_LENGTH + FORWARD_TSN_STREAM_LENGTH * self.streams.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} - -#[derive(Debug, Clone)] -pub(crate) struct ChunkForwardTsnStream { - /// This field holds a stream number that was skipped by this - /// FWD-TSN. - pub(crate) identifier: u16, - - /// This field holds the sequence number associated with the stream - /// that was skipped. The stream sequence field holds the largest - /// stream sequence number in this stream being skipped. The receiver - /// of the FWD-TSN's can use the Stream-N and Stream Sequence-N fields - /// to enable delivery of any stranded TSN's that remain on the stream - /// re-ordering queues. This field MUST NOT report TSN's corresponding - /// to DATA chunks that are marked as unordered. For ordered DATA - /// chunks this field MUST be filled in. - pub(crate) sequence: u16, -} - -/// makes ChunkForwardTsnStream printable -impl fmt::Display for ChunkForwardTsnStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}, {}", self.identifier, self.sequence) - } -} - -impl Chunk for ChunkForwardTsnStream { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: ChunkType(0), - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(buf: &Bytes) -> Result { - if buf.len() < FORWARD_TSN_STREAM_LENGTH { - return Err(Error::ErrChunkTooShort); - } - - let reader = &mut buf.clone(); - let identifier = reader.get_u16(); - let sequence = reader.get_u16(); - - Ok(ChunkForwardTsnStream { - identifier, - sequence, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - writer.put_u16(self.identifier); - writer.put_u16(self.sequence); - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - FORWARD_TSN_STREAM_LENGTH - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_header.rs b/sctp/src/chunk/chunk_header.rs deleted file mode 100644 index 8c59f8bdc..000000000 --- a/sctp/src/chunk/chunk_header.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::chunk_type::*; -use super::*; - -///chunkHeader represents a SCTP Chunk header, defined in https://tools.ietf.org/html/rfc4960#section-3.2 -///The figure below illustrates the field format for the chunks to be -///transmitted in the SCTP packet. Each chunk is formatted with a Chunk -///Type field, a chunk-specific Flag field, a Chunk Length field, and a -///Value field. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Chunk Type | Chunk Flags | Chunk Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Chunk Value | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Clone)] -pub(crate) struct ChunkHeader { - pub(crate) typ: ChunkType, - pub(crate) flags: u8, - pub(crate) value_length: u16, -} - -pub(crate) const CHUNK_HEADER_SIZE: usize = 4; - -/// makes ChunkHeader printable -impl fmt::Display for ChunkHeader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.typ) - } -} - -impl Chunk for ChunkHeader { - fn header(&self) -> ChunkHeader { - self.clone() - } - - fn unmarshal(raw: &Bytes) -> Result { - if raw.len() < CHUNK_HEADER_SIZE { - return Err(Error::ErrChunkHeaderTooSmall); - } - - let reader = &mut raw.clone(); - - let typ = ChunkType(reader.get_u8()); - let flags = reader.get_u8(); - let length = reader.get_u16(); - - if length < CHUNK_HEADER_SIZE as u16 { - return Err(Error::ErrChunkHeaderInvalidLength); - } - if (length as usize) > raw.len() { - return Err(Error::ErrChunkHeaderInvalidLength); - } - - // Length includes Chunk header - let value_length = length as isize - CHUNK_HEADER_SIZE as isize; - - let length_after_value = raw.len() as isize - length as isize; - if length_after_value < 0 { - return Err(Error::ErrChunkHeaderNotEnoughSpace); - } else if length_after_value < 4 { - // https://tools.ietf.org/html/rfc4960#section-3.2 - // The Chunk Length field does not count any chunk PADDING. - // Chunks (including Type, Length, and Value fields) are padded out - // by the sender with all zero bytes to be a multiple of 4 bytes - // long. This PADDING MUST NOT be more than 3 bytes in total. The - // Chunk Length value does not include terminating PADDING of the - // chunk. However, it does include PADDING of any variable-length - // parameter except the last parameter in the chunk. The receiver - // MUST ignore the PADDING. - for i in (1..=length_after_value).rev() { - let padding_offset = CHUNK_HEADER_SIZE + (value_length + i - 1) as usize; - if raw[padding_offset] != 0 { - return Err(Error::ErrChunkHeaderPaddingNonZero); - } - } - } - - Ok(ChunkHeader { - typ, - flags, - value_length: length - CHUNK_HEADER_SIZE as u16, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - writer.put_u8(self.typ.0); - writer.put_u8(self.flags); - writer.put_u16(self.value_length + CHUNK_HEADER_SIZE as u16); - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.value_length as usize - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_heartbeat.rs b/sctp/src/chunk/chunk_heartbeat.rs deleted file mode 100644 index f2091a4c0..000000000 --- a/sctp/src/chunk/chunk_heartbeat.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::param::param_header::*; -use crate::param::param_type::*; -use crate::param::*; - -///chunkHeartbeat represents an SCTP Chunk of type HEARTBEAT -/// -///An endpoint should send this chunk to its peer endpoint to probe the -///reachability of a particular destination transport address defined in -///the present association. -/// -///The parameter field contains the Heartbeat Information, which is a -///variable-length opaque data structure understood only by the sender. -/// -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 4 | Chunk Flags | Heartbeat Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Heartbeat Information TLV (Variable-Length) | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -///Defined as a variable-length parameter using the format described -///in Section 3.2.1, i.e.: -/// -///Variable Parameters Status Type Value -///------------------------------------------------------------- -///heartbeat Info Mandatory 1 -#[derive(Default, Debug)] -pub(crate) struct ChunkHeartbeat { - pub(crate) params: Vec>, -} - -/// makes ChunkHeartbeat printable -impl fmt::Display for ChunkHeartbeat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkHeartbeat { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_HEARTBEAT, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_HEARTBEAT { - return Err(Error::ErrChunkTypeNotHeartbeat); - } - - if raw.len() <= CHUNK_HEADER_SIZE { - return Err(Error::ErrHeartbeatNotLongEnoughInfo); - } - - let p = - build_param(&raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()))?; - if p.header().typ != ParamType::HeartbeatInfo { - return Err(Error::ErrHeartbeatParam); - } - let params = vec![p]; - - Ok(ChunkHeartbeat { params }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for p in &self.params { - buf.extend(p.marshal()?); - } - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.params.iter().fold(0, |length, p| { - length + PARAM_HEADER_LENGTH + p.value_length() - }) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_heartbeat_ack.rs b/sctp/src/chunk/chunk_heartbeat_ack.rs deleted file mode 100644 index 0d058c261..000000000 --- a/sctp/src/chunk/chunk_heartbeat_ack.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::param::param_header::*; -use crate::param::param_type::ParamType; -use crate::param::*; -use crate::util::get_padding_size; - -///chunkHeartbeatAck represents an SCTP Chunk of type HEARTBEAT ACK -/// -///An endpoint should send this chunk to its peer endpoint as a response -///to a HEARTBEAT chunk (see Section 8.3). A HEARTBEAT ACK is always -///sent to the source IP address of the IP datagram containing the -///HEARTBEAT chunk to which this ack is responding. -/// -///The parameter field contains a variable-length opaque data structure. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 5 | Chunk Flags | Heartbeat Ack Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Heartbeat Information TLV (Variable-Length) | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// -///Defined as a variable-length parameter using the format described -///in Section 3.2.1, i.e.: -/// -///Variable Parameters Status Type Value -///------------------------------------------------------------- -///Heartbeat Info Mandatory 1 -#[derive(Default, Debug)] -pub(crate) struct ChunkHeartbeatAck { - pub(crate) params: Vec>, -} - -/// makes ChunkHeartbeatAck printable -impl fmt::Display for ChunkHeartbeatAck { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkHeartbeatAck { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_HEARTBEAT_ACK, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_HEARTBEAT_ACK { - return Err(Error::ErrChunkTypeNotHeartbeatAck); - } - - if raw.len() <= CHUNK_HEADER_SIZE { - return Err(Error::ErrHeartbeatNotLongEnoughInfo); - } - - let p = - build_param(&raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()))?; - if p.header().typ != ParamType::HeartbeatInfo { - return Err(Error::ErrHeartbeatParam); - } - let params = vec![p]; - - Ok(ChunkHeartbeatAck { params }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - if self.params.len() != 1 { - return Err(Error::ErrHeartbeatAckParams); - } - if self.params[0].header().typ != ParamType::HeartbeatInfo { - return Err(Error::ErrHeartbeatAckNotHeartbeatInfo); - } - - self.header().marshal_to(buf)?; - for (idx, p) in self.params.iter().enumerate() { - let pp = p.marshal()?; - let pp_len = pp.len(); - buf.extend(pp); - - // Chunks (including Type, Length, and Value fields) are padded out - // by the sender with all zero bytes to be a multiple of 4 bytes - // long. This PADDING MUST NOT be more than 3 bytes in total. The - // Chunk Length value does not include terminating PADDING of the - // chunk. *However, it does include PADDING of any variable-length - // parameter except the last parameter in the chunk.* The receiver - // MUST ignore the PADDING. - if idx != self.params.len() - 1 { - let cnt = get_padding_size(pp_len); - buf.extend(vec![0u8; cnt]); - } - } - Ok(buf.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - let mut l = 0; - for (idx, p) in self.params.iter().enumerate() { - let p_len = PARAM_HEADER_LENGTH + p.value_length(); - l += p_len; - if idx != self.params.len() - 1 { - l += get_padding_size(p_len); - } - } - l - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_init.rs b/sctp/src/chunk/chunk_init.rs deleted file mode 100644 index 141ed102d..000000000 --- a/sctp/src/chunk/chunk_init.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::param::param_header::*; -use crate::param::param_supported_extensions::ParamSupportedExtensions; -use crate::param::*; -use crate::util::get_padding_size; - -///chunkInitCommon represents an SCTP Chunk body of type INIT and INIT ACK -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 1 | Chunk Flags | Chunk Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Initiate Tag | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Advertised Receiver Window Credit (a_rwnd) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Number of Outbound Streams | Number of Inbound Streams | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Initial TSN | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Optional/Variable-Length Parameters | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -///The INIT chunk contains the following parameters. Unless otherwise -///noted, each parameter MUST only be included once in the INIT chunk. -/// -///Fixed Parameters Status -///---------------------------------------------- -///Initiate Tag Mandatory -///Advertised Receiver Window Credit Mandatory -///Number of Outbound Streams Mandatory -///Number of Inbound Streams Mandatory -///Initial TSN Mandatory -/// -///Init represents an SCTP Chunk of type INIT -/// -///See chunkInitCommon for the fixed headers -/// -///Variable Parameters Status Type Value -///------------------------------------------------------------- -///IPv4 IP (Note 1) Optional 5 -///IPv6 IP (Note 1) Optional 6 -///Cookie Preservative Optional 9 -///Reserved for ECN Capable (Note 2) Optional 32768 (0x8000) -///Host Name IP (Note 3) Optional 11 -///Supported IP Types (Note 4) Optional 12 -/// -/// -/// chunkInitAck represents an SCTP Chunk of type INIT ACK -/// -///See chunkInitCommon for the fixed headers -/// -///Variable Parameters Status Type Value -///------------------------------------------------------------- -///State Cookie Mandatory 7 -///IPv4 IP (Note 1) Optional 5 -///IPv6 IP (Note 1) Optional 6 -///Unrecognized Parameter Optional 8 -///Reserved for ECN Capable (Note 2) Optional 32768 (0x8000) -///Host Name IP (Note 3) Optional 11 -#[derive(Default, Debug)] -pub(crate) struct ChunkInit { - pub(crate) is_ack: bool, - pub(crate) initiate_tag: u32, - pub(crate) advertised_receiver_window_credit: u32, - pub(crate) num_outbound_streams: u16, - pub(crate) num_inbound_streams: u16, - pub(crate) initial_tsn: u32, - pub(crate) params: Vec>, -} - -impl Clone for ChunkInit { - fn clone(&self) -> Self { - ChunkInit { - is_ack: self.is_ack, - initiate_tag: self.initiate_tag, - advertised_receiver_window_credit: self.advertised_receiver_window_credit, - num_outbound_streams: self.num_outbound_streams, - num_inbound_streams: self.num_inbound_streams, - initial_tsn: self.initial_tsn, - params: self.params.to_vec(), - } - } -} - -pub(crate) const INIT_CHUNK_MIN_LENGTH: usize = 16; -pub(crate) const INIT_OPTIONAL_VAR_HEADER_LENGTH: usize = 4; - -/// makes chunkInitCommon printable -impl fmt::Display for ChunkInit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = format!( - "is_ack: {} - initiate_tag: {} - advertised_receiver_window_credit: {} - num_outbound_streams: {} - num_inbound_streams: {} - initial_tsn: {}", - self.is_ack, - self.initiate_tag, - self.advertised_receiver_window_credit, - self.num_outbound_streams, - self.num_inbound_streams, - self.initial_tsn, - ); - - for (i, param) in self.params.iter().enumerate() { - res += format!("Param {i}:\n {param}").as_str(); - } - write!(f, "{} {}", self.header(), res) - } -} - -impl Chunk for ChunkInit { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: if self.is_ack { CT_INIT_ACK } else { CT_INIT }, - flags: 0, - value_length: self.value_length() as u16, - } - } - - ///https://tools.ietf.org/html/rfc4960#section-3.2.1 - /// - ///Chunk values of SCTP control chunks consist of a chunk-type-specific - ///header of required fields, followed by zero or more parameters. The - ///optional and variable-length parameters contained in a chunk are - ///defined in a Type-Length-Value format as shown below. - /// - ///0 1 2 3 - ///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| Parameter Type | Parameter Length | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| | - ///| Parameter Value | - ///| | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if !(header.typ == CT_INIT || header.typ == CT_INIT_ACK) { - return Err(Error::ErrChunkTypeNotTypeInit); - } else if header.value_length() < INIT_CHUNK_MIN_LENGTH { - // validity of value_length is checked in ChunkHeader::unmarshal - return Err(Error::ErrChunkValueNotLongEnough); - } - - // The Chunk Flags field in INIT is reserved, and all bits in it should - // be set to 0 by the sender and ignored by the receiver. The sequence - // of parameters within an INIT can be processed in any order. - if header.flags != 0 { - return Err(Error::ErrChunkTypeInitFlagZero); - } - - let reader = &mut raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - - let initiate_tag = reader.get_u32(); - let advertised_receiver_window_credit = reader.get_u32(); - let num_outbound_streams = reader.get_u16(); - let num_inbound_streams = reader.get_u16(); - let initial_tsn = reader.get_u32(); - - let mut params = vec![]; - let mut offset = CHUNK_HEADER_SIZE + INIT_CHUNK_MIN_LENGTH; - let mut remaining = raw.len() as isize - offset as isize; - while remaining >= INIT_OPTIONAL_VAR_HEADER_LENGTH as isize { - let p = build_param(&raw.slice(offset..CHUNK_HEADER_SIZE + header.value_length()))?; - let p_len = PARAM_HEADER_LENGTH + p.value_length(); - let len_plus_padding = p_len + get_padding_size(p_len); - params.push(p); - offset += len_plus_padding; - remaining -= len_plus_padding as isize; - } - - Ok(ChunkInit { - is_ack: header.typ == CT_INIT_ACK, - initiate_tag, - advertised_receiver_window_credit, - num_outbound_streams, - num_inbound_streams, - initial_tsn, - params, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - - writer.put_u32(self.initiate_tag); - writer.put_u32(self.advertised_receiver_window_credit); - writer.put_u16(self.num_outbound_streams); - writer.put_u16(self.num_inbound_streams); - writer.put_u32(self.initial_tsn); - for (idx, p) in self.params.iter().enumerate() { - let pp = p.marshal()?; - let pp_len = pp.len(); - writer.extend(pp); - - // Chunks (including Type, Length, and Value fields) are padded out - // by the sender with all zero bytes to be a multiple of 4 bytes - // long. This padding MUST NOT be more than 3 bytes in total. The - // Chunk Length value does not include terminating padding of the - // chunk. *However, it does include padding of any variable-length - // parameter except the last parameter in the chunk.* The receiver - // MUST ignore the padding. - if idx != self.params.len() - 1 { - let cnt = get_padding_size(pp_len); - writer.extend(vec![0u8; cnt]); - } - } - - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - // The receiver of the INIT (the responding end) records the value of - // the Initiate Tag parameter. This value MUST be placed into the - // Verification Tag field of every SCTP packet that the receiver of - // the INIT transmits within this association. - // - // The Initiate Tag is allowed to have any value except 0. See - // Section 5.3.1 for more on the selection of the tag value. - // - // If the value of the Initiate Tag in a received INIT chunk is found - // to be 0, the receiver MUST treat it as an error and close the - // association by transmitting an ABORT. - if self.initiate_tag == 0 { - return Err(Error::ErrChunkTypeInitInitiateTagZero); - } - - // Defines the maximum number of streams the sender of this INIT - // chunk allows the peer end to create in this association. The - // value 0 MUST NOT be used. - // - // Note: There is no negotiation of the actual number of streams but - // instead the two endpoints will use the min(requested, offered). - // See Section 5.1.1 for details. - // - // Note: A receiver of an INIT with the MIS value of 0 SHOULD abort - // the association. - if self.num_inbound_streams == 0 { - return Err(Error::ErrInitInboundStreamRequestZero); - } - - // Defines the number of outbound streams the sender of this INIT - // chunk wishes to create in this association. The value of 0 MUST - // NOT be used. - // - // Note: A receiver of an INIT with the OS value set to 0 SHOULD - // abort the association. - - if self.num_outbound_streams == 0 { - return Err(Error::ErrInitOutboundStreamRequestZero); - } - - // An SCTP receiver MUST be able to receive a minimum of 1500 bytes in - // one SCTP packet. This means that an SCTP endpoint MUST NOT indicate - // less than 1500 bytes in its initial a_rwnd sent in the INIT or INIT - // ACK. - if self.advertised_receiver_window_credit < 1500 { - return Err(Error::ErrInitAdvertisedReceiver1500); - } - - Ok(()) - } - - fn value_length(&self) -> usize { - let mut l = 4 + 4 + 2 + 2 + 4; - for (idx, p) in self.params.iter().enumerate() { - let p_len = PARAM_HEADER_LENGTH + p.value_length(); - l += p_len; - if idx != self.params.len() - 1 { - l += get_padding_size(p_len); - } - } - l - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} - -impl ChunkInit { - pub(crate) fn set_supported_extensions(&mut self) { - // TODO RFC5061 https://tools.ietf.org/html/rfc6525#section-5.2 - // An implementation supporting this (Supported Extensions Parameter) - // extension MUST list the ASCONF, the ASCONF-ACK, and the AUTH chunks - // in its INIT and INIT-ACK parameters. - self.params.push(Box::new(ParamSupportedExtensions { - chunk_types: vec![CT_RECONFIG, CT_FORWARD_TSN], - })); - } -} diff --git a/sctp/src/chunk/chunk_payload_data.rs b/sctp/src/chunk/chunk_payload_data.rs deleted file mode 100644 index 2a2da26ad..000000000 --- a/sctp/src/chunk/chunk_payload_data.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::fmt; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use portable_atomic::AtomicBool; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -pub(crate) const PAYLOAD_DATA_ENDING_FRAGMENT_BITMASK: u8 = 1; -pub(crate) const PAYLOAD_DATA_BEGINNING_FRAGMENT_BITMASK: u8 = 2; -pub(crate) const PAYLOAD_DATA_UNORDERED_BITMASK: u8 = 4; -pub(crate) const PAYLOAD_DATA_IMMEDIATE_SACK: u8 = 8; -pub(crate) const PAYLOAD_DATA_HEADER_SIZE: usize = 12; - -/// PayloadProtocolIdentifier is an enum for DataChannel payload types -/// PayloadProtocolIdentifier enums -/// https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[repr(C)] -pub enum PayloadProtocolIdentifier { - Dcep = 50, - String = 51, - Binary = 53, - StringEmpty = 56, - BinaryEmpty = 57, - #[default] - Unknown, -} - -impl fmt::Display for PayloadProtocolIdentifier { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - PayloadProtocolIdentifier::Dcep => "WebRTC DCEP", - PayloadProtocolIdentifier::String => "WebRTC String", - PayloadProtocolIdentifier::Binary => "WebRTC Binary", - PayloadProtocolIdentifier::StringEmpty => "WebRTC String (Empty)", - PayloadProtocolIdentifier::BinaryEmpty => "WebRTC Binary (Empty)", - _ => "Unknown Payload Protocol Identifier", - }; - write!(f, "{s}") - } -} - -impl From for PayloadProtocolIdentifier { - fn from(v: u32) -> PayloadProtocolIdentifier { - match v { - 50 => PayloadProtocolIdentifier::Dcep, - 51 => PayloadProtocolIdentifier::String, - 53 => PayloadProtocolIdentifier::Binary, - 56 => PayloadProtocolIdentifier::StringEmpty, - 57 => PayloadProtocolIdentifier::BinaryEmpty, - _ => PayloadProtocolIdentifier::Unknown, - } - } -} - -///chunkPayloadData represents an SCTP Chunk of type DATA -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 0 | Reserved|U|B|E| Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| TSN | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Stream Identifier S | Stream Sequence Number n | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Payload Protocol Identifier | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| User Data (seq n of Stream S) | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// -///An unfragmented user message shall have both the B and E bits set to -///'1'. Setting both B and E bits to '0' indicates a middle fragment of -///a multi-fragment user message, as summarized in the following table: -/// B E Description -///============================================================ -///| 1 0 | First piece of a fragmented user message | -///+----------------------------------------------------------+ -///| 0 0 | Middle piece of a fragmented user message | -///+----------------------------------------------------------+ -///| 0 1 | Last piece of a fragmented user message | -///+----------------------------------------------------------+ -///| 1 1 | Unfragmented message | -///============================================================ -///| Table 1: Fragment Description Flags | -///============================================================ -#[derive(Debug, Clone)] -pub struct ChunkPayloadData { - pub(crate) unordered: bool, - pub(crate) beginning_fragment: bool, - pub(crate) ending_fragment: bool, - pub(crate) immediate_sack: bool, - - pub(crate) tsn: u32, - pub(crate) stream_identifier: u16, - pub(crate) stream_sequence_number: u16, - pub(crate) payload_type: PayloadProtocolIdentifier, - pub(crate) user_data: Bytes, - - /// Whether this data chunk was acknowledged (received by peer) - pub(crate) acked: bool, - pub(crate) miss_indicator: u32, - - /// Partial-reliability parameters used only by sender - pub(crate) since: SystemTime, - /// number of transmission made for this chunk - pub(crate) nsent: u32, - - /// valid only with the first fragment - pub(crate) abandoned: Arc, - /// valid only with the first fragment - pub(crate) all_inflight: Arc, - - /// Retransmission flag set when T1-RTX timeout occurred and this - /// chunk is still in the inflight queue - pub(crate) retransmit: bool, -} - -impl Default for ChunkPayloadData { - fn default() -> Self { - ChunkPayloadData { - unordered: false, - beginning_fragment: false, - ending_fragment: false, - immediate_sack: false, - tsn: 0, - stream_identifier: 0, - stream_sequence_number: 0, - payload_type: PayloadProtocolIdentifier::default(), - user_data: Bytes::new(), - acked: false, - miss_indicator: 0, - since: SystemTime::now(), - nsent: 0, - abandoned: Arc::new(AtomicBool::new(false)), - all_inflight: Arc::new(AtomicBool::new(false)), - retransmit: false, - } - } -} - -/// makes chunkPayloadData printable -impl fmt::Display for ChunkPayloadData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}\n{}", self.header(), self.tsn) - } -} - -impl Chunk for ChunkPayloadData { - fn header(&self) -> ChunkHeader { - let mut flags: u8 = 0; - if self.ending_fragment { - flags = 1; - } - if self.beginning_fragment { - flags |= 1 << 1; - } - if self.unordered { - flags |= 1 << 2; - } - if self.immediate_sack { - flags |= 1 << 3; - } - - ChunkHeader { - typ: CT_PAYLOAD_DATA, - flags, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_PAYLOAD_DATA { - return Err(Error::ErrChunkTypeNotPayloadData); - } - - let immediate_sack = (header.flags & PAYLOAD_DATA_IMMEDIATE_SACK) != 0; - let unordered = (header.flags & PAYLOAD_DATA_UNORDERED_BITMASK) != 0; - let beginning_fragment = (header.flags & PAYLOAD_DATA_BEGINNING_FRAGMENT_BITMASK) != 0; - let ending_fragment = (header.flags & PAYLOAD_DATA_ENDING_FRAGMENT_BITMASK) != 0; - - // validity of value_length is checked in ChunkHeader::unmarshal - if header.value_length() < PAYLOAD_DATA_HEADER_SIZE { - return Err(Error::ErrChunkPayloadSmall); - } - - let reader = &mut raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - - let tsn = reader.get_u32(); - let stream_identifier = reader.get_u16(); - let stream_sequence_number = reader.get_u16(); - let payload_type: PayloadProtocolIdentifier = reader.get_u32().into(); - let user_data = raw.slice( - CHUNK_HEADER_SIZE + PAYLOAD_DATA_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length(), - ); - - Ok(ChunkPayloadData { - unordered, - beginning_fragment, - ending_fragment, - immediate_sack, - - tsn, - stream_identifier, - stream_sequence_number, - payload_type, - user_data, - acked: false, - miss_indicator: 0, - since: SystemTime::now(), - nsent: 0, - abandoned: Arc::new(AtomicBool::new(false)), - all_inflight: Arc::new(AtomicBool::new(false)), - retransmit: false, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - - writer.put_u32(self.tsn); - writer.put_u16(self.stream_identifier); - writer.put_u16(self.stream_sequence_number); - writer.put_u32(self.payload_type as u32); - writer.extend_from_slice(&self.user_data); - - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - PAYLOAD_DATA_HEADER_SIZE + self.user_data.len() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} - -impl ChunkPayloadData { - pub(crate) fn abandoned(&self) -> bool { - let (abandoned, all_inflight) = ( - self.abandoned.load(Ordering::SeqCst), - self.all_inflight.load(Ordering::SeqCst), - ); - - abandoned && all_inflight - } - - pub(crate) fn set_abandoned(&self, abandoned: bool) { - self.abandoned.store(abandoned, Ordering::SeqCst); - } - - pub(crate) fn set_all_inflight(&mut self) { - if self.ending_fragment { - self.all_inflight.store(true, Ordering::SeqCst); - } - } -} diff --git a/sctp/src/chunk/chunk_reconfig.rs b/sctp/src/chunk/chunk_reconfig.rs deleted file mode 100644 index 2282857d2..000000000 --- a/sctp/src/chunk/chunk_reconfig.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; -use crate::param::param_header::*; -use crate::param::*; -use crate::util::get_padding_size; - -///https://tools.ietf.org/html/rfc6525#section-3.1 -///chunkReconfig represents an SCTP Chunk used to reconfigure streams. -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 130 | Chunk Flags | Chunk Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Re-configuration Parameter | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| Re-configuration Parameter (optional) | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug)] -pub(crate) struct ChunkReconfig { - pub(crate) param_a: Option>, - pub(crate) param_b: Option>, -} - -impl Clone for ChunkReconfig { - fn clone(&self) -> Self { - ChunkReconfig { - param_a: self.param_a.as_ref().cloned(), - param_b: self.param_b.as_ref().cloned(), - } - } -} - -/// makes chunkReconfig printable -impl fmt::Display for ChunkReconfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = String::new(); - if let Some(param_a) = &self.param_a { - res += format!("Param A:\n {param_a}").as_str(); - } - if let Some(param_b) = &self.param_b { - res += format!("Param B:\n {param_b}").as_str() - } - write!(f, "{res}") - } -} - -impl Chunk for ChunkReconfig { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_RECONFIG, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_RECONFIG { - return Err(Error::ErrChunkTypeNotReconfig); - } - - let param_a = - build_param(&raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()))?; - - let padding = get_padding_size(PARAM_HEADER_LENGTH + param_a.value_length()); - let offset = CHUNK_HEADER_SIZE + PARAM_HEADER_LENGTH + param_a.value_length() + padding; - let param_b = if CHUNK_HEADER_SIZE + header.value_length() > offset { - Some(build_param( - &raw.slice(offset..CHUNK_HEADER_SIZE + header.value_length()), - )?) - } else { - None - }; - - Ok(ChunkReconfig { - param_a: Some(param_a), - param_b, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - - let param_a_value_length = if let Some(param_a) = &self.param_a { - writer.extend(param_a.marshal()?); - param_a.value_length() - } else { - return Err(Error::ErrChunkReconfigInvalidParamA); - }; - - if let Some(param_b) = &self.param_b { - // Pad param A - let padding = get_padding_size(PARAM_HEADER_LENGTH + param_a_value_length); - writer.extend(vec![0u8; padding]); - writer.extend(param_b.marshal()?); - } - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - let mut l = PARAM_HEADER_LENGTH; - let param_a_value_length = if let Some(param_a) = &self.param_a { - l += param_a.value_length(); - param_a.value_length() - } else { - 0 - }; - if let Some(param_b) = &self.param_b { - let padding = get_padding_size(PARAM_HEADER_LENGTH + param_a_value_length); - l += PARAM_HEADER_LENGTH + param_b.value_length() + padding; - } - l - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_selective_ack.rs b/sctp/src/chunk/chunk_selective_ack.rs deleted file mode 100644 index 38e22e6c1..000000000 --- a/sctp/src/chunk/chunk_selective_ack.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -///chunkSelectiveAck represents an SCTP Chunk of type SACK -/// -///This chunk is sent to the peer endpoint to acknowledge received DATA -///chunks and to inform the peer endpoint of gaps in the received -///subsequences of DATA chunks as represented by their TSNs. -///0 1 2 3 -///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 3 |Chunk Flags | Chunk Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Cumulative TSN Ack | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Advertised Receiver Window Credit (a_rwnd) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Number of Gap Ack Blocks = N | Number of Duplicate TSNs = X | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Gap Ack Block #1 Start | Gap Ack Block #1 End | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| ... | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Gap Ack Block #N Start | Gap Ack Block #N End | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Duplicate TSN 1 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| | -///| ... | -///| | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Duplicate TSN X | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Debug, Default, Copy, Clone)] -pub(crate) struct GapAckBlock { - pub(crate) start: u16, - pub(crate) end: u16, -} - -/// makes gapAckBlock printable -impl fmt::Display for GapAckBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} - {}", self.start, self.end) - } -} - -#[derive(Default, Debug)] -pub(crate) struct ChunkSelectiveAck { - pub(crate) cumulative_tsn_ack: u32, - pub(crate) advertised_receiver_window_credit: u32, - pub(crate) gap_ack_blocks: Vec, - pub(crate) duplicate_tsn: Vec, -} - -/// makes chunkSelectiveAck printable -impl fmt::Display for ChunkSelectiveAck { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = format!( - "SACK cumTsnAck={} arwnd={} dupTsn={:?}", - self.cumulative_tsn_ack, self.advertised_receiver_window_credit, self.duplicate_tsn - ); - - for gap in &self.gap_ack_blocks { - res += format!("\n gap ack: {gap}").as_str(); - } - - write!(f, "{res}") - } -} - -pub(crate) const SELECTIVE_ACK_HEADER_SIZE: usize = 12; - -impl Chunk for ChunkSelectiveAck { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_SACK, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_SACK { - return Err(Error::ErrChunkTypeNotSack); - } - - // validity of value_length is checked in ChunkHeader::unmarshal - if header.value_length() < SELECTIVE_ACK_HEADER_SIZE { - return Err(Error::ErrSackSizeNotLargeEnoughInfo); - } - - let reader = &mut raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - - let cumulative_tsn_ack = reader.get_u32(); - let advertised_receiver_window_credit = reader.get_u32(); - let gap_ack_blocks_len = reader.get_u16() as usize; - let duplicate_tsn_len = reader.get_u16() as usize; - - // Here we must account for case where the buffer contains another chunk - // right after this one. Testing for equality would incorrectly fail the - // parsing of this chunk and incorrectly close the transport. - - // validity of value_length is checked in ChunkHeader::unmarshal - if header.value_length() - < SELECTIVE_ACK_HEADER_SIZE + (4 * gap_ack_blocks_len + 4 * duplicate_tsn_len) - { - return Err(Error::ErrSackSizeNotLargeEnoughInfo); - } - - let mut gap_ack_blocks = vec![]; - let mut duplicate_tsn = vec![]; - for _ in 0..gap_ack_blocks_len { - let start = reader.get_u16(); - let end = reader.get_u16(); - gap_ack_blocks.push(GapAckBlock { start, end }); - } - for _ in 0..duplicate_tsn_len { - duplicate_tsn.push(reader.get_u32()); - } - - Ok(ChunkSelectiveAck { - cumulative_tsn_ack, - advertised_receiver_window_credit, - gap_ack_blocks, - duplicate_tsn, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - - writer.put_u32(self.cumulative_tsn_ack); - writer.put_u32(self.advertised_receiver_window_credit); - writer.put_u16(self.gap_ack_blocks.len() as u16); - writer.put_u16(self.duplicate_tsn.len() as u16); - for g in &self.gap_ack_blocks { - writer.put_u16(g.start); - writer.put_u16(g.end); - } - for t in &self.duplicate_tsn { - writer.put_u32(*t); - } - - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - SELECTIVE_ACK_HEADER_SIZE + self.gap_ack_blocks.len() * 4 + self.duplicate_tsn.len() * 4 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_shutdown.rs b/sctp/src/chunk/chunk_shutdown.rs deleted file mode 100644 index 44a3e4547..000000000 --- a/sctp/src/chunk/chunk_shutdown.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -///chunkShutdown represents an SCTP Chunk of type chunkShutdown -/// -///0 1 2 3 -///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 7 | Chunk Flags | Length = 8 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Cumulative TSN Ack | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkShutdown { - pub(crate) cumulative_tsn_ack: u32, -} - -pub(crate) const CUMULATIVE_TSN_ACK_LENGTH: usize = 4; - -/// makes chunkShutdown printable -impl fmt::Display for ChunkShutdown { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkShutdown { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_SHUTDOWN, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_SHUTDOWN { - return Err(Error::ErrChunkTypeNotShutdown); - } - - if raw.len() != CHUNK_HEADER_SIZE + CUMULATIVE_TSN_ACK_LENGTH { - return Err(Error::ErrInvalidChunkSize); - } - - let reader = &mut raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + header.value_length()); - - let cumulative_tsn_ack = reader.get_u32(); - - Ok(ChunkShutdown { cumulative_tsn_ack }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - writer.put_u32(self.cumulative_tsn_ack); - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - CUMULATIVE_TSN_ACK_LENGTH - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_shutdown_ack.rs b/sctp/src/chunk/chunk_shutdown_ack.rs deleted file mode 100644 index d22c86739..000000000 --- a/sctp/src/chunk/chunk_shutdown_ack.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -///chunkShutdownAck represents an SCTP Chunk of type chunkShutdownAck -/// -///0 1 2 3 -///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 8 | Chunk Flags | Length = 4 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkShutdownAck; - -/// makes chunkShutdownAck printable -impl fmt::Display for ChunkShutdownAck { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkShutdownAck { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_SHUTDOWN_ACK, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_SHUTDOWN_ACK { - return Err(Error::ErrChunkTypeNotShutdownAck); - } - - Ok(ChunkShutdownAck {}) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - 0 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_shutdown_complete.rs b/sctp/src/chunk/chunk_shutdown_complete.rs deleted file mode 100644 index c84da65a6..000000000 --- a/sctp/src/chunk/chunk_shutdown_complete.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; - -use super::chunk_header::*; -use super::chunk_type::*; -use super::*; - -///chunkShutdownComplete represents an SCTP Chunk of type chunkShutdownComplete -/// -///0 1 2 3 -///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Type = 14 |Reserved |T| Length = 4 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone)] -pub(crate) struct ChunkShutdownComplete; - -/// makes chunkShutdownComplete printable -impl fmt::Display for ChunkShutdownComplete { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Chunk for ChunkShutdownComplete { - fn header(&self) -> ChunkHeader { - ChunkHeader { - typ: CT_SHUTDOWN_COMPLETE, - flags: 0, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ChunkHeader::unmarshal(raw)?; - - if header.typ != CT_SHUTDOWN_COMPLETE { - return Err(Error::ErrChunkTypeNotShutdownComplete); - } - - Ok(ChunkShutdownComplete {}) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - self.header().marshal_to(writer)?; - Ok(writer.len()) - } - - fn check(&self) -> Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - 0 - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/chunk/chunk_test.rs b/sctp/src/chunk/chunk_test.rs deleted file mode 100644 index f16169661..000000000 --- a/sctp/src/chunk/chunk_test.rs +++ /dev/null @@ -1,752 +0,0 @@ -/////////////////////////////////////////////////////////////////// -//chunk_type_test -/////////////////////////////////////////////////////////////////// -use super::chunk_type::*; -use super::*; - -#[test] -fn test_chunk_type_string() -> Result<()> { - let tests = vec![ - (CT_PAYLOAD_DATA, "DATA"), - (CT_INIT, "INIT"), - (CT_INIT_ACK, "INIT-ACK"), - (CT_SACK, "SACK"), - (CT_HEARTBEAT, "HEARTBEAT"), - (CT_HEARTBEAT_ACK, "HEARTBEAT-ACK"), - (CT_ABORT, "ABORT"), - (CT_SHUTDOWN, "SHUTDOWN"), - (CT_SHUTDOWN_ACK, "SHUTDOWN-ACK"), - (CT_ERROR, "ERROR"), - (CT_COOKIE_ECHO, "COOKIE-ECHO"), - (CT_COOKIE_ACK, "COOKIE-ACK"), - (CT_ECNE, "ECNE"), - (CT_CWR, "CWR"), - (CT_SHUTDOWN_COMPLETE, "SHUTDOWN-COMPLETE"), - (CT_RECONFIG, "RECONFIG"), - (CT_FORWARD_TSN, "FORWARD-TSN"), - (ChunkType(255), "Unknown ChunkType: 255"), - ]; - - for (ct, expected) in tests { - assert_eq!( - ct.to_string(), - expected, - "failed to stringify chunkType {ct}, expected {expected}" - ); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_abort_test -/////////////////////////////////////////////////////////////////// -use super::chunk_abort::*; -use crate::error_cause::*; - -#[test] -fn test_abort_chunk_one_error_cause() -> Result<()> { - let abort1 = ChunkAbort { - error_causes: vec![ErrorCause { - code: PROTOCOL_VIOLATION, - ..Default::default() - }], - }; - - let b = abort1.marshal()?; - let abort2 = ChunkAbort::unmarshal(&b)?; - - assert_eq!(abort2.error_causes.len(), 1, "should have only one cause"); - assert_eq!( - abort2.error_causes[0].error_cause_code(), - abort1.error_causes[0].error_cause_code(), - "errorCause code should match" - ); - - Ok(()) -} - -#[test] -fn test_abort_chunk_many_error_causes() -> Result<()> { - let abort1 = ChunkAbort { - error_causes: vec![ - ErrorCause { - code: INVALID_MANDATORY_PARAMETER, - ..Default::default() - }, - ErrorCause { - code: UNRECOGNIZED_CHUNK_TYPE, - ..Default::default() - }, - ErrorCause { - code: PROTOCOL_VIOLATION, - ..Default::default() - }, - ], - }; - - let b = abort1.marshal()?; - let abort2 = ChunkAbort::unmarshal(&b)?; - assert_eq!(abort2.error_causes.len(), 3, "should have only one cause"); - for (i, error_cause) in abort1.error_causes.iter().enumerate() { - assert_eq!( - abort2.error_causes[i].error_cause_code(), - error_cause.error_cause_code(), - "errorCause code should match" - ); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_error_test -/////////////////////////////////////////////////////////////////// -use bytes::BufMut; -use lazy_static::lazy_static; - -use super::chunk_error::*; - -const CHUNK_FLAGS: u8 = 0x00; -static ORG_UNRECOGNIZED_CHUNK: Bytes = - Bytes::from_static(&[0xc0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x3]); - -lazy_static! { - static ref RAW_IN: Bytes = { - let mut raw = BytesMut::new(); - raw.put_u8(CT_ERROR.0); - raw.put_u8(CHUNK_FLAGS); - raw.extend(vec![0x00, 0x10, 0x00, 0x06, 0x00, 0x0c]); - raw.extend(ORG_UNRECOGNIZED_CHUNK.clone()); - raw.freeze() - }; -} - -#[test] -fn test_chunk_error_unrecognized_chunk_type_unmarshal() -> Result<()> { - let c = ChunkError::unmarshal(&RAW_IN)?; - assert_eq!(c.header().typ, CT_ERROR, "chunk type should be ERROR"); - assert_eq!(c.error_causes.len(), 1, "there should be on errorCause"); - - let ec = &c.error_causes[0]; - assert_eq!( - ec.error_cause_code(), - UNRECOGNIZED_CHUNK_TYPE, - "cause code should be unrecognizedChunkType" - ); - assert_eq!( - ec.raw, ORG_UNRECOGNIZED_CHUNK, - "should have valid unrecognizedChunk" - ); - - Ok(()) -} - -#[test] -fn test_chunk_error_unrecognized_chunk_type_marshal() -> Result<()> { - let ec_unrecognized_chunk_type = ErrorCause { - code: UNRECOGNIZED_CHUNK_TYPE, - raw: ORG_UNRECOGNIZED_CHUNK.clone(), - }; - - let ec = ChunkError { - error_causes: vec![ec_unrecognized_chunk_type], - }; - - let raw = ec.marshal()?; - assert_eq!(raw, *RAW_IN, "unexpected serialization result"); - - Ok(()) -} - -#[test] -fn test_chunk_error_unrecognized_chunk_type_marshal_with_cause_value_being_nil() -> Result<()> { - let expected = - Bytes::from_static(&[CT_ERROR.0, CHUNK_FLAGS, 0x00, 0x08, 0x00, 0x06, 0x00, 0x04]); - let ec_unrecognized_chunk_type = ErrorCause { - code: UNRECOGNIZED_CHUNK_TYPE, - ..Default::default() - }; - - let ec = ChunkError { - error_causes: vec![ec_unrecognized_chunk_type], - }; - - let raw = ec.marshal()?; - assert_eq!(raw, expected, "unexpected serialization result"); - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_forward_tsn_test -/////////////////////////////////////////////////////////////////// -use super::chunk_forward_tsn::*; - -static CHUNK_FORWARD_TSN_BYTES: Bytes = - Bytes::from_static(&[0xc0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x3]); - -#[test] -fn test_chunk_forward_tsn_success() -> Result<()> { - let tests = vec![ - CHUNK_FORWARD_TSN_BYTES.clone(), - Bytes::from_static(&[0xc0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5]), - Bytes::from_static(&[ - 0xc0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5, 0x0, 0x6, 0x0, 0x7, - ]), - ]; - - for binary in tests { - let actual = ChunkForwardTsn::unmarshal(&binary)?; - let b = actual.marshal()?; - assert_eq!(b, binary, "test not equal"); - } - - Ok(()) -} - -#[test] -fn test_chunk_forward_tsn_unmarshal_failure() -> Result<()> { - let tests = vec![ - ("chunk header to short", Bytes::from_static(&[0xc0])), - ( - "missing New Cumulative TSN", - Bytes::from_static(&[0xc0, 0x0, 0x0, 0x4]), - ), - ( - "missing stream sequence", - Bytes::from_static(&[ - 0xc0, 0x0, 0x0, 0xe, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5, 0x0, 0x6, - ]), - ), - ]; - - for (name, binary) in tests { - let result = ChunkForwardTsn::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_reconfig_test -/////////////////////////////////////////////////////////////////// -use super::chunk_reconfig::*; - -static TEST_CHUNK_RECONFIG_PARAM_A: Bytes = Bytes::from_static(&[ - 0x0, 0xd, 0x0, 0x16, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, - 0x5, 0x0, 0x6, -]); - -static TEST_CHUNK_RECONFIG_PARAM_B: Bytes = Bytes::from_static(&[ - 0x0, 0xd, 0x0, 0x10, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, -]); - -static TEST_CHUNK_RECONFIG_RESPONSE: Bytes = - Bytes::from_static(&[0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1]); - -lazy_static! { - static ref TEST_CHUNK_RECONFIG_BYTES: Vec = { - let mut tests = vec![]; - { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x1a]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_A.clone()); - tests.push(test.freeze()); - } - { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x14]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_B.clone()); - tests.push(test.freeze()); - } - { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x10]); - test.extend(TEST_CHUNK_RECONFIG_RESPONSE.clone()); - tests.push(test.freeze()); - } - { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x2c]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_A.clone()); - test.extend(vec![0u8; 2]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_B.clone()); - tests.push(test.freeze()); - } - { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x2a]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_B.clone()); - test.extend(TEST_CHUNK_RECONFIG_PARAM_A.clone()); - tests.push(test.freeze()); - } - - tests - }; -} - -#[test] -fn test_chunk_reconfig_success() -> Result<()> { - for (i, binary) in TEST_CHUNK_RECONFIG_BYTES.iter().enumerate() { - let actual = ChunkReconfig::unmarshal(binary)?; - let b = actual.marshal()?; - assert_eq!(*binary, b, "test {} not equal: {:?} vs {:?}", i, *binary, b); - } - - Ok(()) -} - -#[test] -fn test_chunk_reconfig_unmarshal_failure() -> Result<()> { - let mut test = BytesMut::new(); - test.extend(vec![0x82, 0x0, 0x0, 0x18]); - test.extend(TEST_CHUNK_RECONFIG_PARAM_B.clone()); - test.extend(vec![0x0, 0xd, 0x0, 0x0]); - let tests = vec![ - ("chunk header to short", Bytes::from_static(&[0x82])), - ( - "missing parse param type (A)", - Bytes::from_static(&[0x82, 0x0, 0x0, 0x4]), - ), - ( - "wrong param (A)", - Bytes::from_static(&[0x82, 0x0, 0x0, 0x8, 0x0, 0xd, 0x0, 0x0]), - ), - ("wrong param (B)", test.freeze()), - ]; - - for (name, binary) in tests { - let result = ChunkReconfig::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_shutdown_test -/////////////////////////////////////////////////////////////////// -use super::chunk_shutdown::*; - -#[test] -fn test_chunk_shutdown_success() -> Result<()> { - let tests = vec![Bytes::from_static(&[ - 0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, - ])]; - - for binary in tests { - let actual = ChunkShutdown::unmarshal(&binary)?; - let b = actual.marshal()?; - assert_eq!(b, binary, "test not equal"); - } - - Ok(()) -} - -#[test] -fn test_chunk_shutdown_failure() -> Result<()> { - let tests = vec![ - ( - "length too short", - Bytes::from_static(&[0x07, 0x00, 0x00, 0x07, 0x12, 0x34, 0x56, 0x78]), - ), - ( - "length too long", - Bytes::from_static(&[0x07, 0x00, 0x00, 0x09, 0x12, 0x34, 0x56, 0x78]), - ), - ( - "payload too short", - Bytes::from_static(&[0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56]), - ), - ( - "payload too long", - Bytes::from_static(&[0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9f]), - ), - ( - "invalid type", - Bytes::from_static(&[0x08, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78]), - ), - ]; - - for (name, binary) in tests { - let result = ChunkShutdown::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_shutdown_ack_test -/////////////////////////////////////////////////////////////////// -use super::chunk_shutdown_ack::*; - -#[test] -fn test_chunk_shutdown_ack_success() -> Result<()> { - let tests = vec![Bytes::from_static(&[0x08, 0x00, 0x00, 0x04])]; - - for binary in tests { - let actual = ChunkShutdownAck::unmarshal(&binary)?; - let b = actual.marshal()?; - assert_eq!(binary, b, "test not equal"); - } - - Ok(()) -} - -#[test] -fn test_chunk_shutdown_ack_failure() -> Result<()> { - let tests = vec![ - ("length too short", Bytes::from_static(&[0x08, 0x00, 0x00])), - ( - "length too long", - Bytes::from_static(&[0x08, 0x00, 0x00, 0x04, 0x12]), - ), - ( - "invalid type", - Bytes::from_static(&[0x0f, 0x00, 0x00, 0x04]), - ), - ]; - - for (name, binary) in tests { - let result = ChunkShutdownAck::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_shutdown_complete_test -/////////////////////////////////////////////////////////////////// -use super::chunk_shutdown_complete::*; - -#[test] -fn test_chunk_shutdown_complete_success() -> Result<()> { - let tests = vec![Bytes::from_static(&[0x0e, 0x00, 0x00, 0x04])]; - - for binary in tests { - let actual = ChunkShutdownComplete::unmarshal(&binary)?; - let b = actual.marshal()?; - assert_eq!(b, binary, "test not equal"); - } - - Ok(()) -} - -#[test] -fn test_chunk_shutdown_complete_failure() -> Result<()> { - let tests = vec![ - ("length too short", Bytes::from_static(&[0x0e, 0x00, 0x00])), - ( - "length too long", - Bytes::from_static(&[0x0e, 0x00, 0x00, 0x04, 0x12]), - ), - ( - "invalid type", - Bytes::from_static(&[0x0f, 0x00, 0x00, 0x04]), - ), - ]; - - for (name, binary) in tests { - let result = ChunkShutdownComplete::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//chunk_test -/////////////////////////////////////////////////////////////////// -use crate::chunk::chunk_init::*; -use crate::chunk::chunk_payload_data::*; -use crate::chunk::chunk_selective_ack::ChunkSelectiveAck; -use crate::packet::*; -use crate::param::param_outgoing_reset_request::ParamOutgoingResetRequest; -use crate::param::param_state_cookie::*; - -#[test] -fn test_init_chunk() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x81, 0x46, 0x9d, 0xfc, 0x01, 0x00, 0x00, - 0x56, 0x55, 0xb9, 0x64, 0xa5, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0xe8, 0x6d, - 0x10, 0x30, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, - 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x9f, 0xeb, 0xbb, 0x5c, 0x50, 0xc9, 0xbf, 0x75, - 0x9c, 0xb1, 0x2c, 0x57, 0x4f, 0xa4, 0x5a, 0x51, 0xba, 0x60, 0x17, 0x78, 0x27, 0x94, 0x5c, - 0x31, 0xe6, 0x5d, 0x5b, 0x09, 0x47, 0xe2, 0x22, 0x06, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - - if let Some(c) = pkt.chunks[0].as_any().downcast_ref::() { - assert_eq!( - c.initiate_tag, 1438213285, - "Unmarshal passed for SCTP packet, but got incorrect initiate tag exp: {} act: {}", - 1438213285, c.initiate_tag - ); - assert_eq!(c.advertised_receiver_window_credit, 131072, "Unmarshal passed for SCTP packet, but got incorrect advertisedReceiverWindowCredit exp: {} act: {}", 131072, c.advertised_receiver_window_credit); - assert_eq!(c.num_outbound_streams, 1024, "Unmarshal passed for SCTP packet, but got incorrect numOutboundStreams tag exp:{} act: {}", 1024, c.num_outbound_streams); - assert_eq!( - c.num_inbound_streams, 2048, - "Unmarshal passed for SCTP packet, but got incorrect numInboundStreams exp: {} act: {}", - 2048, c.num_inbound_streams - ); - assert_eq!( - c.initial_tsn, 3899461680u32, - "Unmarshal passed for SCTP packet, but got incorrect initialTSN exp: {} act: {}", - 3899461680u32, c.initial_tsn - ); - } else { - panic!("Failed to cast Chunk -> Init"); - } - - Ok(()) -} - -#[test] -fn test_init_ack() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xce, 0x15, 0x79, 0xa2, 0x96, 0x19, 0xe8, 0xb2, 0x02, 0x00, 0x00, - 0x1c, 0xeb, 0x81, 0x4e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x50, 0xdf, - 0x90, 0xd9, 0x00, 0x07, 0x00, 0x08, 0x94, 0x06, 0x2f, 0x93, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - assert!( - pkt.chunks[0].as_any().downcast_ref::().is_some(), - "Failed to cast Chunk -> Init" - ); - - Ok(()) -} - -#[test] -fn test_chrome_chunk1_init() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0xbc, 0xb3, 0x45, 0xa2, 0x01, 0x00, 0x00, - 0x56, 0xce, 0x15, 0x79, 0xa2, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x94, 0x57, - 0x95, 0xc0, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, - 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0xff, 0x5c, 0x49, 0x19, 0x4a, 0x94, 0xe8, 0x2a, - 0xec, 0x58, 0x55, 0x62, 0x29, 0x1f, 0x8e, 0x23, 0xcd, 0x7c, 0xe8, 0x46, 0xba, 0x58, 0x1b, - 0x3d, 0xab, 0xd7, 0x7e, 0x50, 0xf2, 0x41, 0xb1, 0x2e, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - let raw_pkt2 = pkt.marshal()?; - assert_eq!(raw_pkt2, raw_pkt); - - Ok(()) -} - -#[test] -fn test_chrome_chunk2_init_ack() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xce, 0x15, 0x79, 0xa2, 0xb5, 0xdb, 0x2d, 0x93, 0x02, 0x00, 0x01, - 0x90, 0x9b, 0xd5, 0xb3, 0x6f, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0xef, 0xb4, - 0x72, 0x87, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, - 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x2e, 0xf9, 0x9c, 0x10, 0x63, 0x72, 0xed, 0x0d, - 0x33, 0xc2, 0xdc, 0x7f, 0x9f, 0xd7, 0xef, 0x1b, 0xc9, 0xc4, 0xa7, 0x41, 0x9a, 0x07, 0x68, - 0x6b, 0x66, 0xfb, 0x6a, 0x4e, 0x32, 0x5d, 0xe4, 0x25, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 0x00, 0x07, 0x01, 0x38, 0x4b, - 0x41, 0x4d, 0x45, 0x2d, 0x42, 0x53, 0x44, 0x20, 0x31, 0x2e, 0x31, 0x00, 0x00, 0x00, 0x00, - 0x9c, 0x1e, 0x49, 0x5b, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x42, 0x06, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x60, 0xea, 0x00, 0x00, 0xc4, 0x13, 0x3d, 0xe9, 0x86, 0xb1, 0x85, 0x75, 0xa2, 0x79, - 0x15, 0xce, 0x9b, 0xd5, 0xb3, 0x6f, 0x20, 0xe0, 0x9f, 0x89, 0xe0, 0x27, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0xe0, 0x9f, 0x89, - 0xe0, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x56, 0xce, 0x15, 0x79, 0xa2, 0x00, - 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x94, 0x57, 0x95, 0xc0, 0xc0, 0x00, 0x00, 0x04, - 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, - 0x24, 0xff, 0x5c, 0x49, 0x19, 0x4a, 0x94, 0xe8, 0x2a, 0xec, 0x58, 0x55, 0x62, 0x29, 0x1f, - 0x8e, 0x23, 0xcd, 0x7c, 0xe8, 0x46, 0xba, 0x58, 0x1b, 0x3d, 0xab, 0xd7, 0x7e, 0x50, 0xf2, - 0x41, 0xb1, 0x2e, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, - 0x80, 0xc1, 0x00, 0x00, 0x02, 0x00, 0x01, 0x90, 0x9b, 0xd5, 0xb3, 0x6f, 0x00, 0x02, 0x00, - 0x00, 0x04, 0x00, 0x08, 0x00, 0xef, 0xb4, 0x72, 0x87, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, - 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x2e, - 0xf9, 0x9c, 0x10, 0x63, 0x72, 0xed, 0x0d, 0x33, 0xc2, 0xdc, 0x7f, 0x9f, 0xd7, 0xef, 0x1b, - 0xc9, 0xc4, 0xa7, 0x41, 0x9a, 0x07, 0x68, 0x6b, 0x66, 0xfb, 0x6a, 0x4e, 0x32, 0x5d, 0xe4, - 0x25, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, - 0x00, 0x00, 0xca, 0x0c, 0x21, 0x11, 0xce, 0xf4, 0xfc, 0xb3, 0x66, 0x99, 0x4f, 0xdb, 0x4f, - 0x95, 0x6b, 0x6f, 0x3b, 0xb1, 0xdb, 0x5a, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - let raw_pkt2 = pkt.marshal()?; - assert_eq!(raw_pkt2, raw_pkt); - - Ok(()) -} - -#[test] -fn test_init_marshal_unmarshal() -> Result<()> { - let mut p = Packet { - destination_port: 1, - source_port: 1, - verification_tag: 123, - chunks: vec![], - }; - - let mut init_ack = ChunkInit { - is_ack: true, - initiate_tag: 123, - advertised_receiver_window_credit: 1024, - num_outbound_streams: 1, - num_inbound_streams: 1, - initial_tsn: 123, - params: vec![], - }; - - let cookie = Box::new(ParamStateCookie::new()); - init_ack.params.push(cookie); - - p.chunks.push(Box::new(init_ack)); - - let raw_pkt = p.marshal()?; - let pkt = Packet::unmarshal(&raw_pkt)?; - - if let Some(c) = pkt.chunks[0].as_any().downcast_ref::() { - assert_eq!( - c.initiate_tag, 123, - "Unmarshal passed for SCTP packet, but got incorrect initiate tag exp: {} act: {}", - 123, c.initiate_tag - ); - assert_eq!(c.advertised_receiver_window_credit, 1024, "Unmarshal passed for SCTP packet, but got incorrect advertisedReceiverWindowCredit exp: {} act: {}", 1024, c.advertised_receiver_window_credit); - assert_eq!(c.num_outbound_streams, 1, "Unmarshal passed for SCTP packet, but got incorrect numOutboundStreams tag exp:{} act: {}", 1, c.num_outbound_streams); - assert_eq!( - c.num_inbound_streams, 1, - "Unmarshal passed for SCTP packet, but got incorrect numInboundStreams exp: {} act: {}", - 1, c.num_inbound_streams - ); - assert_eq!( - c.initial_tsn, 123, - "Unmarshal passed for SCTP packet, but got incorrect initialTSN exp: {} act: {}", - 123, c.initial_tsn - ); - } else { - panic!("Failed to cast Chunk -> InitAck"); - } - - Ok(()) -} - -#[test] -fn test_payload_data_marshal_unmarshal() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xfc, 0xd6, 0x3f, 0xc6, 0xbe, 0xfa, 0xdc, 0x52, 0x0a, 0x00, 0x00, - 0x24, 0x9b, 0x28, 0x7e, 0x48, 0xa3, 0x7b, 0xc1, 0x83, 0xc4, 0x4b, 0x41, 0x04, 0xa4, 0xf7, - 0xed, 0x4c, 0x93, 0x62, 0xc3, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x1f, 0xa8, 0x79, 0xa1, 0xc7, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x66, 0x6f, 0x6f, 0x00, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - assert!( - pkt.chunks[1] - .as_any() - .downcast_ref::() - .is_some(), - "Failed to cast Chunk -> PayloadData" - ); - Ok(()) -} - -#[test] -fn test_select_ack_chunk() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xc2, 0x98, 0x98, 0x0f, 0x42, 0x31, 0xea, 0x78, 0x03, 0x00, 0x00, - 0x14, 0x87, 0x73, 0xbd, 0xa4, 0x00, 0x01, 0xfe, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, - 0x00, 0x02, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - assert!( - pkt.chunks[0] - .as_any() - .downcast_ref::() - .is_some(), - "Failed to cast Chunk -> SelectiveAck" - ); - Ok(()) -} - -#[test] -fn test_reconfig_chunk() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xb6, 0xa5, 0x12, 0xe5, 0x75, 0x3b, 0x12, 0xd3, 0x82, 0x0, 0x0, - 0x16, 0x0, 0xd, 0x0, 0x12, 0x4e, 0x1c, 0xb9, 0xe6, 0x3a, 0x74, 0x8d, 0xff, 0x4e, 0x1c, - 0xb9, 0xe6, 0x0, 0x1, 0x0, 0x0, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - if let Some(c) = pkt.chunks[0].as_any().downcast_ref::() { - assert!(c.param_a.is_some(), "param_a must not be none"); - assert_eq!( - c.param_a - .as_ref() - .unwrap() - .as_any() - .downcast_ref::() - .unwrap() - .stream_identifiers[0], - 1, - "unexpected stream identifier" - ); - } else { - panic!("Failed to cast Chunk -> Reconfig"); - } - - Ok(()) -} - -#[test] -fn test_forward_tsn_chunk() -> Result<()> { - let mut raw_pkt = BytesMut::new(); - raw_pkt.extend(vec![ - 0x13, 0x88, 0x13, 0x88, 0xb6, 0xa5, 0x12, 0xe5, 0x1f, 0x9d, 0xa0, 0xfb, - ]); - raw_pkt.extend(CHUNK_FORWARD_TSN_BYTES.clone()); - let raw_pkt = raw_pkt.freeze(); - let pkt = Packet::unmarshal(&raw_pkt)?; - - if let Some(c) = pkt.chunks[0].as_any().downcast_ref::() { - assert_eq!( - c.new_cumulative_tsn, 3, - "unexpected New Cumulative TSN: {}", - c.new_cumulative_tsn - ); - } else { - panic!("Failed to cast Chunk -> Forward TSN"); - } - - Ok(()) -} - -#[test] -fn test_select_ack_chunk_followed_by_a_payload_data_chunk() -> Result<()> { - let raw_pkt = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0xc2, 0x98, 0x98, 0x0f, 0x58, 0xcf, 0x38, - 0xC0, // A SACK chunk follows. - 0x03, 0x00, 0x00, 0x14, 0x87, 0x73, 0xbd, 0xa4, 0x00, 0x01, 0xfe, 0x74, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x02, // A payload data chunk follows. - 0x00, 0x07, 0x00, 0x3B, 0xA4, 0x50, 0x7B, 0xC5, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, 0x7B, 0x22, 0x65, 0x76, 0x65, 0x6E, 0x74, 0x22, 0x3A, 0x22, 0x72, 0x65, 0x73, 0x69, - 0x7A, 0x65, 0x22, 0x2C, 0x22, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x3A, 0x36, 0x36, 0x35, - 0x2C, 0x22, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3A, 0x34, 0x39, 0x39, 0x7D, 0x00, - ]); - let pkt = Packet::unmarshal(&raw_pkt)?; - assert!( - pkt.chunks[0] - .as_any() - .downcast_ref::() - .is_some(), - "Failed to cast Chunk -> SelectiveAck" - ); - assert!( - pkt.chunks[1] - .as_any() - .downcast_ref::() - .is_some(), - "Failed to cast Chunk -> PayloadData" - ); - Ok(()) -} diff --git a/sctp/src/chunk/chunk_type.rs b/sctp/src/chunk/chunk_type.rs deleted file mode 100644 index 60db10985..000000000 --- a/sctp/src/chunk/chunk_type.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::fmt; - -// chunkType is an enum for SCTP Chunk Type field -// This field identifies the type of information contained in the -// Chunk Value field. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub(crate) struct ChunkType(pub(crate) u8); - -pub(crate) const CT_PAYLOAD_DATA: ChunkType = ChunkType(0); -pub(crate) const CT_INIT: ChunkType = ChunkType(1); -pub(crate) const CT_INIT_ACK: ChunkType = ChunkType(2); -pub(crate) const CT_SACK: ChunkType = ChunkType(3); -pub(crate) const CT_HEARTBEAT: ChunkType = ChunkType(4); -pub(crate) const CT_HEARTBEAT_ACK: ChunkType = ChunkType(5); -pub(crate) const CT_ABORT: ChunkType = ChunkType(6); -pub(crate) const CT_SHUTDOWN: ChunkType = ChunkType(7); -pub(crate) const CT_SHUTDOWN_ACK: ChunkType = ChunkType(8); -pub(crate) const CT_ERROR: ChunkType = ChunkType(9); -pub(crate) const CT_COOKIE_ECHO: ChunkType = ChunkType(10); -pub(crate) const CT_COOKIE_ACK: ChunkType = ChunkType(11); -pub(crate) const CT_ECNE: ChunkType = ChunkType(12); -pub(crate) const CT_CWR: ChunkType = ChunkType(13); -pub(crate) const CT_SHUTDOWN_COMPLETE: ChunkType = ChunkType(14); -pub(crate) const CT_RECONFIG: ChunkType = ChunkType(130); -pub(crate) const CT_FORWARD_TSN: ChunkType = ChunkType(192); - -impl fmt::Display for ChunkType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let others = format!("Unknown ChunkType: {}", self.0); - let s = match *self { - CT_PAYLOAD_DATA => "DATA", - CT_INIT => "INIT", - CT_INIT_ACK => "INIT-ACK", - CT_SACK => "SACK", - CT_HEARTBEAT => "HEARTBEAT", - CT_HEARTBEAT_ACK => "HEARTBEAT-ACK", - CT_ABORT => "ABORT", - CT_SHUTDOWN => "SHUTDOWN", - CT_SHUTDOWN_ACK => "SHUTDOWN-ACK", - CT_ERROR => "ERROR", - CT_COOKIE_ECHO => "COOKIE-ECHO", - CT_COOKIE_ACK => "COOKIE-ACK", - CT_ECNE => "ECNE", // Explicit Congestion Notification Echo - CT_CWR => "CWR", // Reserved for Congestion Window Reduced (CWR) - CT_SHUTDOWN_COMPLETE => "SHUTDOWN-COMPLETE", - CT_RECONFIG => "RECONFIG", // Re-configuration - CT_FORWARD_TSN => "FORWARD-TSN", - _ => others.as_str(), - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_chunk_type_string() { - let tests = vec![ - (CT_PAYLOAD_DATA, "DATA"), - (CT_INIT, "INIT"), - (CT_INIT_ACK, "INIT-ACK"), - (CT_SACK, "SACK"), - (CT_HEARTBEAT, "HEARTBEAT"), - (CT_HEARTBEAT_ACK, "HEARTBEAT-ACK"), - (CT_ABORT, "ABORT"), - (CT_SHUTDOWN, "SHUTDOWN"), - (CT_SHUTDOWN_ACK, "SHUTDOWN-ACK"), - (CT_ERROR, "ERROR"), - (CT_COOKIE_ECHO, "COOKIE-ECHO"), - (CT_COOKIE_ACK, "COOKIE-ACK"), - (CT_ECNE, "ECNE"), - (CT_CWR, "CWR"), - (CT_SHUTDOWN_COMPLETE, "SHUTDOWN-COMPLETE"), - (CT_RECONFIG, "RECONFIG"), - (CT_FORWARD_TSN, "FORWARD-TSN"), - (ChunkType(255), "Unknown ChunkType: 255"), - ]; - - for (ct, expected) in tests { - assert_eq!( - ct.to_string(), - expected, - "failed to stringify chunkType {ct}, expected {expected}" - ); - } - } -} diff --git a/sctp/src/chunk/chunk_unknown.rs b/sctp/src/chunk/chunk_unknown.rs deleted file mode 100644 index 5ee94ee41..000000000 --- a/sctp/src/chunk/chunk_unknown.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::any::Any; -use std::fmt::{Debug, Display, Formatter}; - -use bytes::{Bytes, BytesMut}; - -use crate::chunk::chunk_header::{ChunkHeader, CHUNK_HEADER_SIZE}; -use crate::chunk::Chunk; - -#[derive(Clone, Debug)] -pub struct ChunkUnknown { - hdr: ChunkHeader, - value: Bytes, -} - -impl Display for ChunkUnknown { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "ChunkUnknown( {} {:?} )", self.hdr, self.value) - } -} - -impl Chunk for ChunkUnknown { - fn header(&self) -> ChunkHeader { - self.hdr.clone() - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn check(&self) -> crate::error::Result<()> { - Ok(()) - } - - fn value_length(&self) -> usize { - self.value.len() - } - - fn marshal_to(&self, buf: &mut BytesMut) -> crate::error::Result { - self.header().marshal_to(buf)?; - buf.extend(&self.value); - Ok(buf.len()) - } - - fn unmarshal(raw: &Bytes) -> crate::error::Result - where - Self: Sized, - { - let header = ChunkHeader::unmarshal(raw)?; - let len = header.value_length(); - Ok(Self { - hdr: header, - value: raw.slice(CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + len), - }) - } -} diff --git a/sctp/src/chunk/mod.rs b/sctp/src/chunk/mod.rs deleted file mode 100644 index e7fb6b96d..000000000 --- a/sctp/src/chunk/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -#[cfg(test)] -mod chunk_test; - -pub(crate) mod chunk_abort; -pub(crate) mod chunk_cookie_ack; -pub(crate) mod chunk_cookie_echo; -pub(crate) mod chunk_error; -pub(crate) mod chunk_forward_tsn; -pub(crate) mod chunk_header; -pub(crate) mod chunk_heartbeat; -pub(crate) mod chunk_heartbeat_ack; -pub(crate) mod chunk_init; -pub mod chunk_payload_data; -pub(crate) mod chunk_reconfig; -pub(crate) mod chunk_selective_ack; -pub(crate) mod chunk_shutdown; -pub(crate) mod chunk_shutdown_ack; -pub(crate) mod chunk_shutdown_complete; -pub(crate) mod chunk_type; -pub(crate) mod chunk_unknown; - -use std::any::Any; -use std::fmt; -use std::marker::Sized; - -use bytes::{Bytes, BytesMut}; -use chunk_header::*; - -use crate::error::{Error, Result}; - -pub(crate) trait Chunk: fmt::Display + fmt::Debug { - fn header(&self) -> ChunkHeader; - fn unmarshal(raw: &Bytes) -> Result - where - Self: Sized; - fn marshal_to(&self, buf: &mut BytesMut) -> Result; - fn check(&self) -> Result<()>; - fn value_length(&self) -> usize; - fn as_any(&self) -> &(dyn Any + Send + Sync); - - fn marshal(&self) -> Result { - let capacity = CHUNK_HEADER_SIZE + self.value_length(); - let mut buf = BytesMut::with_capacity(capacity); - self.marshal_to(&mut buf)?; - Ok(buf.freeze()) - } -} diff --git a/sctp/src/error.rs b/sctp/src/error.rs deleted file mode 100644 index 3069f02f4..000000000 --- a/sctp/src/error.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::io; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq, Eq, Clone)] -#[non_exhaustive] -pub enum Error { - #[error("raw is too small for a SCTP chunk")] - ErrChunkHeaderTooSmall, - #[error("not enough data left in SCTP packet to satisfy requested length")] - ErrChunkHeaderNotEnoughSpace, - #[error("chunk PADDING is non-zero at offset")] - ErrChunkHeaderPaddingNonZero, - #[error("chunk has invalid length")] - ErrChunkHeaderInvalidLength, - - #[error("ChunkType is not of type ABORT")] - ErrChunkTypeNotAbort, - #[error("failed build Abort Chunk")] - ErrBuildAbortChunkFailed, - #[error("ChunkType is not of type COOKIEACK")] - ErrChunkTypeNotCookieAck, - #[error("ChunkType is not of type COOKIEECHO")] - ErrChunkTypeNotCookieEcho, - #[error("ChunkType is not of type ctError")] - ErrChunkTypeNotCtError, - #[error("failed build Error Chunk")] - ErrBuildErrorChunkFailed, - #[error("failed to marshal stream")] - ErrMarshalStreamFailed, - #[error("chunk too short")] - ErrChunkTooShort, - #[error("ChunkType is not of type ForwardTsn")] - ErrChunkTypeNotForwardTsn, - #[error("ChunkType is not of type HEARTBEAT")] - ErrChunkTypeNotHeartbeat, - #[error("ChunkType is not of type HEARTBEATACK")] - ErrChunkTypeNotHeartbeatAck, - #[error("heartbeat is not long enough to contain Heartbeat Info")] - ErrHeartbeatNotLongEnoughInfo, - #[error("failed to parse param type")] - ErrParseParamTypeFailed, - #[error("heartbeat should only have HEARTBEAT param")] - ErrHeartbeatParam, - #[error("failed unmarshalling param in Heartbeat Chunk")] - ErrHeartbeatChunkUnmarshal, - #[error("unimplemented")] - ErrUnimplemented, - #[error("heartbeat Ack must have one param")] - ErrHeartbeatAckParams, - #[error("heartbeat Ack must have one param, and it should be a HeartbeatInfo")] - ErrHeartbeatAckNotHeartbeatInfo, - #[error("unable to marshal parameter for Heartbeat Ack")] - ErrHeartbeatAckMarshalParam, - - #[error("raw is too small for error cause")] - ErrErrorCauseTooSmall, - - #[error("unhandled ParamType `{typ}`")] - ErrParamTypeUnhandled { typ: u16 }, - - #[error("unexpected ParamType")] - ErrParamTypeUnexpected, - - #[error("param header too short")] - ErrParamHeaderTooShort, - #[error("param self reported length is shorter than header length")] - ErrParamHeaderSelfReportedLengthShorter, - #[error("param self reported length is longer than header length")] - ErrParamHeaderSelfReportedLengthLonger, - #[error("failed to parse param type")] - ErrParamHeaderParseFailed, - - #[error("packet to short")] - ErrParamPacketTooShort, - #[error("outgoing SSN reset request parameter too short")] - ErrSsnResetRequestParamTooShort, - #[error("reconfig response parameter too short")] - ErrReconfigRespParamTooShort, - #[error("invalid algorithm type")] - ErrInvalidAlgorithmType, - - #[error("failed to parse param type")] - ErrInitChunkParseParamTypeFailed, - #[error("failed unmarshalling param in Init Chunk")] - ErrInitChunkUnmarshalParam, - #[error("unable to marshal parameter for INIT/INITACK")] - ErrInitAckMarshalParam, - - #[error("ChunkType is not of type INIT")] - ErrChunkTypeNotTypeInit, - #[error("chunk Value isn't long enough for mandatory parameters exp")] - ErrChunkValueNotLongEnough, - #[error("ChunkType of type INIT flags must be all 0")] - ErrChunkTypeInitFlagZero, - #[error("failed to unmarshal INIT body")] - ErrChunkTypeInitUnmarshalFailed, - #[error("failed marshaling INIT common data")] - ErrChunkTypeInitMarshalFailed, - #[error("ChunkType of type INIT ACK InitiateTag must not be 0")] - ErrChunkTypeInitInitiateTagZero, - #[error("INIT ACK inbound stream request must be > 0")] - ErrInitInboundStreamRequestZero, - #[error("INIT ACK outbound stream request must be > 0")] - ErrInitOutboundStreamRequestZero, - #[error("INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500")] - ErrInitAdvertisedReceiver1500, - - #[error("packet is smaller than the header size")] - ErrChunkPayloadSmall, - #[error("ChunkType is not of type PayloadData")] - ErrChunkTypeNotPayloadData, - #[error("ChunkType is not of type Reconfig")] - ErrChunkTypeNotReconfig, - #[error("ChunkReconfig has invalid ParamA")] - ErrChunkReconfigInvalidParamA, - - #[error("failed to parse param type")] - ErrChunkParseParamTypeFailed, - #[error("unable to marshal parameter A for reconfig")] - ErrChunkMarshalParamAReconfigFailed, - #[error("unable to marshal parameter B for reconfig")] - ErrChunkMarshalParamBReconfigFailed, - - #[error("ChunkType is not of type SACK")] - ErrChunkTypeNotSack, - #[error("SACK Chunk size is not large enough to contain header")] - ErrSackSizeNotLargeEnoughInfo, - - #[error("invalid chunk size")] - ErrInvalidChunkSize, - #[error("ChunkType is not of type SHUTDOWN")] - ErrChunkTypeNotShutdown, - - #[error("ChunkType is not of type SHUTDOWN-ACK")] - ErrChunkTypeNotShutdownAck, - #[error("ChunkType is not of type SHUTDOWN-COMPLETE")] - ErrChunkTypeNotShutdownComplete, - - #[error("raw is smaller than the minimum length for a SCTP packet")] - ErrPacketRawTooSmall, - #[error("unable to parse SCTP chunk, not enough data for complete header")] - ErrParseSctpChunkNotEnoughData, - #[error("failed to unmarshal, contains unknown chunk type")] - ErrUnmarshalUnknownChunkType, - #[error("checksum mismatch theirs")] - ErrChecksumMismatch, - - #[error("unexpected chunk popped (unordered)")] - ErrUnexpectedChuckPoppedUnordered, - #[error("unexpected chunk popped (ordered)")] - ErrUnexpectedChuckPoppedOrdered, - #[error("unexpected q state (should've been selected)")] - ErrUnexpectedQState, - #[error("try again")] - ErrTryAgain, - - #[error("abort chunk, with following errors")] - ErrChunk, - #[error("shutdown called in non-Established state")] - ErrShutdownNonEstablished, - #[error("association closed before connecting")] - ErrAssociationClosedBeforeConn, - #[error("association init failed")] - ErrAssociationInitFailed, - #[error("association handshake closed")] - ErrAssociationHandshakeClosed, - #[error("silently discard")] - ErrSilentlyDiscard, - #[error("the init not stored to send")] - ErrInitNotStoredToSend, - #[error("cookieEcho not stored to send")] - ErrCookieEchoNotStoredToSend, - #[error("sctp packet must not have a source port of 0")] - ErrSctpPacketSourcePortZero, - #[error("sctp packet must not have a destination port of 0")] - ErrSctpPacketDestinationPortZero, - #[error("init chunk must not be bundled with any other chunk")] - ErrInitChunkBundled, - #[error("init chunk expects a verification tag of 0 on the packet when out-of-the-blue")] - ErrInitChunkVerifyTagNotZero, - #[error("todo: handle Init when in state")] - ErrHandleInitState, - #[error("no cookie in InitAck")] - ErrInitAckNoCookie, - #[error("there already exists a stream with identifier")] - ErrStreamAlreadyExist, - #[error("Failed to create a stream with identifier")] - ErrStreamCreateFailed, - #[error("unable to be popped from inflight queue TSN")] - ErrInflightQueueTsnPop, - #[error("requested non-existent TSN")] - ErrTsnRequestNotExist, - #[error("sending reset packet in non-Established state")] - ErrResetPacketInStateNotExist, - #[error("unexpected parameter type")] - ErrParameterType, - #[error("sending payload data in non-Established state")] - ErrPayloadDataStateNotExist, - #[error("unhandled chunk type")] - ErrChunkTypeUnhandled, - #[error("handshake failed (INIT ACK)")] - ErrHandshakeInitAck, - #[error("handshake failed (COOKIE ECHO)")] - ErrHandshakeCookieEcho, - - #[error("outbound packet larger than maximum message size")] - ErrOutboundPacketTooLarge, - #[error("Stream closed")] - ErrStreamClosed, - #[error("Short buffer (size: {size:?}) to be filled")] - ErrShortBuffer { size: usize }, - #[error("Io EOF")] - ErrEof, - #[error("Invalid SystemTime")] - ErrInvalidSystemTime, - #[error("Net Conn read error")] - ErrNetConnReadError, - #[error("Max Data Channel ID")] - ErrMaxDataChannelID, - - #[error("{0}")] - Other(String), -} - -impl From for io::Error { - fn from(error: Error) -> Self { - match error { - e @ Error::ErrEof => io::Error::new(io::ErrorKind::UnexpectedEof, e.to_string()), - e @ Error::ErrStreamClosed => { - io::Error::new(io::ErrorKind::ConnectionAborted, e.to_string()) - } - e => io::Error::new(io::ErrorKind::Other, e.to_string()), - } - } -} diff --git a/sctp/src/error_cause.rs b/sctp/src/error_cause.rs deleted file mode 100644 index d8afd02ce..000000000 --- a/sctp/src/error_cause.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use crate::error::{Error, Result}; - -/// errorCauseCode is a cause code that appears in either a ERROR or ABORT chunk -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub(crate) struct ErrorCauseCode(pub(crate) u16); - -pub(crate) const INVALID_STREAM_IDENTIFIER: ErrorCauseCode = ErrorCauseCode(1); -pub(crate) const MISSING_MANDATORY_PARAMETER: ErrorCauseCode = ErrorCauseCode(2); -pub(crate) const STALE_COOKIE_ERROR: ErrorCauseCode = ErrorCauseCode(3); -pub(crate) const OUT_OF_RESOURCE: ErrorCauseCode = ErrorCauseCode(4); -pub(crate) const UNRESOLVABLE_ADDRESS: ErrorCauseCode = ErrorCauseCode(5); -pub(crate) const UNRECOGNIZED_CHUNK_TYPE: ErrorCauseCode = ErrorCauseCode(6); -pub(crate) const INVALID_MANDATORY_PARAMETER: ErrorCauseCode = ErrorCauseCode(7); -pub(crate) const UNRECOGNIZED_PARAMETERS: ErrorCauseCode = ErrorCauseCode(8); -pub(crate) const NO_USER_DATA: ErrorCauseCode = ErrorCauseCode(9); -pub(crate) const COOKIE_RECEIVED_WHILE_SHUTTING_DOWN: ErrorCauseCode = ErrorCauseCode(10); -pub(crate) const RESTART_OF_AN_ASSOCIATION_WITH_NEW_ADDRESSES: ErrorCauseCode = ErrorCauseCode(11); -pub(crate) const USER_INITIATED_ABORT: ErrorCauseCode = ErrorCauseCode(12); -pub(crate) const PROTOCOL_VIOLATION: ErrorCauseCode = ErrorCauseCode(13); - -impl fmt::Display for ErrorCauseCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let others = format!("Unknown CauseCode: {}", self.0); - let s = match *self { - INVALID_STREAM_IDENTIFIER => "Invalid Stream Identifier", - MISSING_MANDATORY_PARAMETER => "Missing Mandatory Parameter", - STALE_COOKIE_ERROR => "Stale Cookie Error", - OUT_OF_RESOURCE => "Out Of Resource", - UNRESOLVABLE_ADDRESS => "Unresolvable IP", - UNRECOGNIZED_CHUNK_TYPE => "Unrecognized Chunk Type", - INVALID_MANDATORY_PARAMETER => "Invalid Mandatory Parameter", - UNRECOGNIZED_PARAMETERS => "Unrecognized Parameters", - NO_USER_DATA => "No User Data", - COOKIE_RECEIVED_WHILE_SHUTTING_DOWN => "Cookie Received While Shutting Down", - RESTART_OF_AN_ASSOCIATION_WITH_NEW_ADDRESSES => { - "Restart Of An Association With New Addresses" - } - USER_INITIATED_ABORT => "User Initiated Abort", - PROTOCOL_VIOLATION => "Protocol Violation", - _ => others.as_str(), - }; - write!(f, "{s}") - } -} - -/// ErrorCauseHeader represents the shared header that is shared by all error causes -#[derive(Debug, Clone, Default)] -pub(crate) struct ErrorCause { - pub(crate) code: ErrorCauseCode, - pub(crate) raw: Bytes, -} - -/// ErrorCauseInvalidMandatoryParameter represents an SCTP error cause -pub(crate) type ErrorCauseInvalidMandatoryParameter = ErrorCause; - -/// ErrorCauseUnrecognizedChunkType represents an SCTP error cause -pub(crate) type ErrorCauseUnrecognizedChunkType = ErrorCause; - -/// -/// This error cause MAY be included in ABORT chunks that are sent -/// because an SCTP endpoint detects a protocol violation of the peer -/// that is not covered by the error causes described in Section 3.3.10.1 -/// to Section 3.3.10.12. An implementation MAY provide additional -/// information specifying what kind of protocol violation has been -/// detected. -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Cause Code=13 | Cause Length=Variable | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// / Additional Information / -/// \ \ -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -pub(crate) type ErrorCauseProtocolViolation = ErrorCause; - -pub(crate) const ERROR_CAUSE_HEADER_LENGTH: usize = 4; - -/// makes ErrorCauseHeader printable -impl fmt::Display for ErrorCause { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.code) - } -} - -impl ErrorCause { - pub(crate) fn unmarshal(buf: &Bytes) -> Result { - if buf.len() < ERROR_CAUSE_HEADER_LENGTH { - return Err(Error::ErrErrorCauseTooSmall); - } - - let reader = &mut buf.clone(); - - let code = ErrorCauseCode(reader.get_u16()); - let len = reader.get_u16(); - - if len < ERROR_CAUSE_HEADER_LENGTH as u16 { - return Err(Error::ErrErrorCauseTooSmall); - } - if buf.len() < len as usize { - return Err(Error::ErrErrorCauseTooSmall); - } - - let value_length = len as usize - ERROR_CAUSE_HEADER_LENGTH; - - let raw = buf.slice(ERROR_CAUSE_HEADER_LENGTH..ERROR_CAUSE_HEADER_LENGTH + value_length); - - Ok(ErrorCause { code, raw }) - } - - pub(crate) fn marshal(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(self.length()); - let _ = self.marshal_to(&mut buf); - buf.freeze() - } - - pub(crate) fn marshal_to(&self, writer: &mut BytesMut) -> usize { - let len = self.raw.len() + ERROR_CAUSE_HEADER_LENGTH; - writer.put_u16(self.code.0); - writer.put_u16(len as u16); - writer.extend(self.raw.clone()); - writer.len() - } - - pub(crate) fn length(&self) -> usize { - self.raw.len() + ERROR_CAUSE_HEADER_LENGTH - } - - pub(crate) fn error_cause_code(&self) -> ErrorCauseCode { - self.code - } -} diff --git a/sctp/src/fuzz_artifact_test.rs b/sctp/src/fuzz_artifact_test.rs deleted file mode 100644 index c40c9db10..000000000 --- a/sctp/src/fuzz_artifact_test.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! # What are these tests? -//! -//! These tests ensure that regressions in the unmarshalling code are caught. -//! -//! They check all artifacts of the fuzzer that crashed this lib, and make sure they no longer crash the library. -//! -//! The content of the files is mostly garbage, but it triggers "interesting" behaviour in the unmarshalling code. -//! So if your change fails one of these tests you probably made an error somewhere. -//! -//! Sadly these tests cannot really tell you where your error is specifically outside the standard backtrace rust will provide to you. Sorry. - -use bytes::Bytes; - -#[test] -fn param_crash_artifacts() { - for artifact in std::fs::read_dir("fuzz/artifacts/param").unwrap() { - let artifact = artifact.unwrap(); - if artifact - .file_name() - .into_string() - .unwrap() - .starts_with("crash-") - { - let artifact = std::fs::read(artifact.path()).unwrap(); - crate::param::build_param(&Bytes::from(artifact)).ok(); - } - } -} - -#[test] -fn packet_crash_artifacts() { - for artifact in std::fs::read_dir("fuzz/artifacts/packet").unwrap() { - let artifact = artifact.unwrap(); - if artifact - .file_name() - .into_string() - .unwrap() - .starts_with("crash-") - { - let artifact = std::fs::read(artifact.path()).unwrap(); - crate::packet::Packet::unmarshal(&Bytes::from(artifact)).ok(); - } - } -} diff --git a/sctp/src/lib.rs b/sctp/src/lib.rs deleted file mode 100644 index f299abdb3..000000000 --- a/sctp/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod association; -pub mod chunk; -mod error; -pub mod error_cause; -pub mod packet; -pub mod param; -pub(crate) mod queue; -pub mod stream; -pub(crate) mod timer; -pub(crate) mod util; - -pub use error::Error; - -#[cfg(test)] -mod fuzz_artifact_test; diff --git a/sctp/src/packet.rs b/sctp/src/packet.rs deleted file mode 100644 index 2c37fa76a..000000000 --- a/sctp/src/packet.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use crate::chunk::chunk_abort::ChunkAbort; -use crate::chunk::chunk_cookie_ack::ChunkCookieAck; -use crate::chunk::chunk_cookie_echo::ChunkCookieEcho; -use crate::chunk::chunk_error::ChunkError; -use crate::chunk::chunk_forward_tsn::ChunkForwardTsn; -use crate::chunk::chunk_header::*; -use crate::chunk::chunk_heartbeat::ChunkHeartbeat; -use crate::chunk::chunk_init::ChunkInit; -use crate::chunk::chunk_payload_data::ChunkPayloadData; -use crate::chunk::chunk_reconfig::ChunkReconfig; -use crate::chunk::chunk_selective_ack::ChunkSelectiveAck; -use crate::chunk::chunk_shutdown::ChunkShutdown; -use crate::chunk::chunk_shutdown_ack::ChunkShutdownAck; -use crate::chunk::chunk_shutdown_complete::ChunkShutdownComplete; -use crate::chunk::chunk_type::*; -use crate::chunk::chunk_unknown::ChunkUnknown; -use crate::chunk::Chunk; -use crate::error::{Error, Result}; -use crate::util::*; - -///Packet represents an SCTP packet, defined in https://tools.ietf.org/html/rfc4960#section-3 -///An SCTP packet is composed of a common header and chunks. A chunk -///contains either control information or user data. -/// -/// -///SCTP Packet Format -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Common Header | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Chunk #1 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| ... | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Chunk #n | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// -///SCTP Common Header Format -/// -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Source Value Number | Destination Value Number | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Verification Tag | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Checksum | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug)] -pub(crate) struct Packet { - pub(crate) source_port: u16, - pub(crate) destination_port: u16, - pub(crate) verification_tag: u32, - pub(crate) chunks: Vec>, -} - -/// makes packet printable -impl fmt::Display for Packet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = format!( - "Packet: - source_port: {} - destination_port: {} - verification_tag: {} - ", - self.source_port, self.destination_port, self.verification_tag, - ); - for chunk in &self.chunks { - res += format!("Chunk: {chunk}").as_str(); - } - write!(f, "{res}") - } -} - -pub(crate) const PACKET_HEADER_SIZE: usize = 12; - -impl Packet { - pub(crate) fn unmarshal(raw: &Bytes) -> Result { - if raw.len() < PACKET_HEADER_SIZE { - return Err(Error::ErrPacketRawTooSmall); - } - - let reader = &mut raw.clone(); - - let source_port = reader.get_u16(); - let destination_port = reader.get_u16(); - let verification_tag = reader.get_u32(); - - #[cfg(not(fuzzing))] - // only check for checksums when we are not fuzzing. This lets the fuzzer test the code much easier without guessing correct checksums. - { - let their_checksum = reader.get_u32_le(); - let our_checksum = generate_packet_checksum(raw); - - if their_checksum != our_checksum { - return Err(Error::ErrChecksumMismatch); - } - } - - let mut chunks = vec![]; - let mut offset = PACKET_HEADER_SIZE; - loop { - // Exact match, no more chunks - if offset == raw.len() { - break; - } else if offset + CHUNK_HEADER_SIZE > raw.len() { - return Err(Error::ErrParseSctpChunkNotEnoughData); - } - - let ct = ChunkType(raw[offset]); - let c: Box = match ct { - CT_INIT => Box::new(ChunkInit::unmarshal(&raw.slice(offset..))?), - CT_INIT_ACK => Box::new(ChunkInit::unmarshal(&raw.slice(offset..))?), - CT_ABORT => Box::new(ChunkAbort::unmarshal(&raw.slice(offset..))?), - CT_COOKIE_ECHO => Box::new(ChunkCookieEcho::unmarshal(&raw.slice(offset..))?), - CT_COOKIE_ACK => Box::new(ChunkCookieAck::unmarshal(&raw.slice(offset..))?), - CT_HEARTBEAT => Box::new(ChunkHeartbeat::unmarshal(&raw.slice(offset..))?), - CT_PAYLOAD_DATA => Box::new(ChunkPayloadData::unmarshal(&raw.slice(offset..))?), - CT_SACK => Box::new(ChunkSelectiveAck::unmarshal(&raw.slice(offset..))?), - CT_RECONFIG => Box::new(ChunkReconfig::unmarshal(&raw.slice(offset..))?), - CT_FORWARD_TSN => Box::new(ChunkForwardTsn::unmarshal(&raw.slice(offset..))?), - CT_ERROR => Box::new(ChunkError::unmarshal(&raw.slice(offset..))?), - CT_SHUTDOWN => Box::new(ChunkShutdown::unmarshal(&raw.slice(offset..))?), - CT_SHUTDOWN_ACK => Box::new(ChunkShutdownAck::unmarshal(&raw.slice(offset..))?), - CT_SHUTDOWN_COMPLETE => { - Box::new(ChunkShutdownComplete::unmarshal(&raw.slice(offset..))?) - } - _ => Box::new(ChunkUnknown::unmarshal(&raw.slice(offset..))?), - }; - - let chunk_value_padding = get_padding_size(c.value_length()); - offset += CHUNK_HEADER_SIZE + c.value_length() + chunk_value_padding; - chunks.push(c); - } - - Ok(Packet { - source_port, - destination_port, - verification_tag, - chunks, - }) - } - - pub(crate) fn marshal_to(&self, writer: &mut BytesMut) -> Result { - // Populate static headers - // 8-12 is Checksum which will be populated when packet is complete - writer.put_u16(self.source_port); - writer.put_u16(self.destination_port); - writer.put_u32(self.verification_tag); - - // This is where the checksum will be written - let checksum_pos = writer.len(); - writer.extend_from_slice(&[0, 0, 0, 0]); - - // Populate chunks - for c in &self.chunks { - c.marshal_to(writer)?; - - let padding_needed = get_padding_size(writer.len()); - if padding_needed != 0 { - // padding needed if < 4 because we pad to 4 - writer.extend_from_slice(&[0u8; PADDING_MULTIPLE][..padding_needed]); - } - } - - let mut digest = ISCSI_CRC.digest(); - digest.update(writer); - let checksum = digest.finalize(); - - // Checksum is already in BigEndian - // Using LittleEndian stops it from being flipped - let checksum_place = &mut writer[checksum_pos..checksum_pos + 4]; - checksum_place.copy_from_slice(&checksum.to_le_bytes()); - - Ok(writer.len()) - } - - pub(crate) fn marshal(&self) -> Result { - let mut buf = BytesMut::with_capacity(PACKET_HEADER_SIZE); - self.marshal_to(&mut buf)?; - Ok(buf.freeze()) - } -} - -impl Packet { - pub(crate) fn check_packet(&self) -> Result<()> { - // All packets must adhere to these rules - - // This is the SCTP sender's port number. It can be used by the - // receiver in combination with the source IP address, the SCTP - // destination port, and possibly the destination IP address to - // identify the association to which this packet belongs. The port - // number 0 MUST NOT be used. - if self.source_port == 0 { - return Err(Error::ErrSctpPacketSourcePortZero); - } - - // This is the SCTP port number to which this packet is destined. - // The receiving host will use this port number to de-multiplex the - // SCTP packet to the correct receiving endpoint/application. The - // port number 0 MUST NOT be used. - if self.destination_port == 0 { - return Err(Error::ErrSctpPacketDestinationPortZero); - } - - // Check values on the packet that are specific to a particular chunk type - for c in &self.chunks { - if let Some(ci) = c.as_any().downcast_ref::() { - if !ci.is_ack { - // An INIT or INIT ACK chunk MUST NOT be bundled with any other chunk. - // They MUST be the only chunks present in the SCTP packets that carry - // them. - if self.chunks.len() != 1 { - return Err(Error::ErrInitChunkBundled); - } - - // A packet containing an INIT chunk MUST have a zero Verification - // Tag. - if self.verification_tag != 0 { - return Err(Error::ErrInitChunkVerifyTagNotZero); - } - } - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_packet_unmarshal() -> Result<()> { - let result = Packet::unmarshal(&Bytes::new()); - assert!( - result.is_err(), - "Unmarshal should fail when a packet is too small to be SCTP" - ); - - let header_only = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1, - ]); - let pkt = Packet::unmarshal(&header_only)?; - //assert!(result.o(), "Unmarshal failed for SCTP packet with no chunks: {}", result); - assert_eq!( - pkt.source_port, 5000, - "Unmarshal passed for SCTP packet, but got incorrect source port exp: {} act: {}", - 5000, pkt.source_port - ); - assert_eq!( - pkt.destination_port, 5000, - "Unmarshal passed for SCTP packet, but got incorrect destination port exp: {} act: {}", - 5000, pkt.destination_port - ); - assert_eq!( - pkt.verification_tag, 0, - "Unmarshal passed for SCTP packet, but got incorrect verification tag exp: {} act: {}", - 0, pkt.verification_tag - ); - - let raw_chunk = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x81, 0x46, 0x9d, 0xfc, 0x01, 0x00, - 0x00, 0x56, 0x55, 0xb9, 0x64, 0xa5, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, - 0xe8, 0x6d, 0x10, 0x30, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, - 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x9f, 0xeb, 0xbb, 0x5c, - 0x50, 0xc9, 0xbf, 0x75, 0x9c, 0xb1, 0x2c, 0x57, 0x4f, 0xa4, 0x5a, 0x51, 0xba, 0x60, - 0x17, 0x78, 0x27, 0x94, 0x5c, 0x31, 0xe6, 0x5d, 0x5b, 0x09, 0x47, 0xe2, 0x22, 0x06, - 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, - 0x00, 0x00, - ]); - - Packet::unmarshal(&raw_chunk)?; - - Ok(()) - } - - #[test] - fn test_packet_marshal() -> Result<()> { - let header_only = Bytes::from_static(&[ - 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1, - ]); - let pkt = Packet::unmarshal(&header_only)?; - let header_only_marshaled = pkt.marshal()?; - assert_eq!(header_only, header_only_marshaled, "Unmarshal/Marshaled header only packet did not match \nheaderOnly: {header_only:?} \nheader_only_marshaled {header_only_marshaled:?}"); - - Ok(()) - } - - /*fn BenchmarkPacketGenerateChecksum(b *testing.B) { - var data [1024]byte - - for i := 0; i < b.N; i++ { - _ = generatePacketChecksum(data[:]) - } - }*/ -} diff --git a/sctp/src/param/mod.rs b/sctp/src/param/mod.rs deleted file mode 100644 index 49ca3c962..000000000 --- a/sctp/src/param/mod.rs +++ /dev/null @@ -1,89 +0,0 @@ -#[cfg(test)] -mod param_test; - -pub(crate) mod param_chunk_list; -pub(crate) mod param_forward_tsn_supported; -pub(crate) mod param_header; -pub(crate) mod param_heartbeat_info; -pub(crate) mod param_outgoing_reset_request; -pub(crate) mod param_random; -pub(crate) mod param_reconfig_response; -pub(crate) mod param_requested_hmac_algorithm; -pub(crate) mod param_state_cookie; -pub(crate) mod param_supported_extensions; -pub(crate) mod param_type; -pub(crate) mod param_unknown; -pub(crate) mod param_unrecognized; - -use std::any::Any; -use std::fmt; - -use bytes::{Buf, Bytes, BytesMut}; -use param_header::*; -use param_type::*; - -use crate::error::{Error, Result}; -use crate::param::param_chunk_list::ParamChunkList; -use crate::param::param_forward_tsn_supported::ParamForwardTsnSupported; -use crate::param::param_heartbeat_info::ParamHeartbeatInfo; -use crate::param::param_outgoing_reset_request::ParamOutgoingResetRequest; -use crate::param::param_random::ParamRandom; -use crate::param::param_reconfig_response::ParamReconfigResponse; -use crate::param::param_requested_hmac_algorithm::ParamRequestedHmacAlgorithm; -use crate::param::param_state_cookie::ParamStateCookie; -use crate::param::param_supported_extensions::ParamSupportedExtensions; -use crate::param::param_unknown::ParamUnknown; - -pub(crate) trait Param: fmt::Display + fmt::Debug { - fn header(&self) -> ParamHeader; - fn unmarshal(raw: &Bytes) -> Result - where - Self: Sized; - fn marshal_to(&self, buf: &mut BytesMut) -> Result; - fn value_length(&self) -> usize; - fn clone_to(&self) -> Box; - fn as_any(&self) -> &(dyn Any + Send + Sync); - - fn marshal(&self) -> Result { - let capacity = PARAM_HEADER_LENGTH + self.value_length(); - let mut buf = BytesMut::with_capacity(capacity); - self.marshal_to(&mut buf)?; - Ok(buf.freeze()) - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_to() - } -} - -pub(crate) fn build_param(raw_param: &Bytes) -> Result> { - if raw_param.len() < PARAM_HEADER_LENGTH { - return Err(Error::ErrParamHeaderTooShort); - } - let reader = &mut raw_param.slice(..2); - let raw_type = reader.get_u16(); - match raw_type.into() { - ParamType::ForwardTsnSupp => Ok(Box::new(ParamForwardTsnSupported::unmarshal(raw_param)?)), - ParamType::SupportedExt => Ok(Box::new(ParamSupportedExtensions::unmarshal(raw_param)?)), - ParamType::Random => Ok(Box::new(ParamRandom::unmarshal(raw_param)?)), - ParamType::ReqHmacAlgo => Ok(Box::new(ParamRequestedHmacAlgorithm::unmarshal(raw_param)?)), - ParamType::ChunkList => Ok(Box::new(ParamChunkList::unmarshal(raw_param)?)), - ParamType::StateCookie => Ok(Box::new(ParamStateCookie::unmarshal(raw_param)?)), - ParamType::HeartbeatInfo => Ok(Box::new(ParamHeartbeatInfo::unmarshal(raw_param)?)), - ParamType::OutSsnResetReq => Ok(Box::new(ParamOutgoingResetRequest::unmarshal(raw_param)?)), - ParamType::ReconfigResp => Ok(Box::new(ParamReconfigResponse::unmarshal(raw_param)?)), - _ => { - // According to RFC https://datatracker.ietf.org/doc/html/rfc4960#section-3.2.1 - let stop_processing = ((raw_type >> 15) & 0x01) == 0; - if stop_processing { - Err(Error::ErrParamTypeUnhandled { typ: raw_type }) - } else { - // We still might need to report this param as unrecognized. - // This depends on the context though. - Ok(Box::new(ParamUnknown::unmarshal(raw_param)?)) - } - } - } -} diff --git a/sctp/src/param/param_chunk_list.rs b/sctp/src/param/param_chunk_list.rs deleted file mode 100644 index d3b704e73..000000000 --- a/sctp/src/param/param_chunk_list.rs +++ /dev/null @@ -1,73 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; -use crate::chunk::chunk_type::*; - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamChunkList { - pub(crate) chunk_types: Vec, -} - -impl fmt::Display for ParamChunkList { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}", - self.header(), - self.chunk_types - .iter() - .map(|ct| ct.to_string()) - .collect::>() - .join(" ") - ) - } -} - -impl Param for ParamChunkList { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::ChunkList, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - - if header.typ != ParamType::ChunkList { - return Err(Error::ErrParamTypeUnexpected); - } - - let reader = - &mut raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - - let mut chunk_types = vec![]; - while reader.has_remaining() { - chunk_types.push(ChunkType(reader.get_u8())); - } - - Ok(ParamChunkList { chunk_types }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for ct in &self.chunk_types { - buf.put_u8(ct.0); - } - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.chunk_types.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_forward_tsn_supported.rs b/sctp/src/param/param_forward_tsn_supported.rs deleted file mode 100644 index c742b1408..000000000 --- a/sctp/src/param/param_forward_tsn_supported.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -/// At the initialization of the association, the sender of the INIT or -/// INIT ACK chunk MAY include this OPTIONAL parameter to inform its peer -/// that it is able to support the Forward TSN chunk -/// -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Parameter Type = 49152 | Parameter Length = 4 | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamForwardTsnSupported; - -impl fmt::Display for ParamForwardTsnSupported { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.header()) - } -} - -impl Param for ParamForwardTsnSupported { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::ForwardTsnSupp, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let _ = ParamHeader::unmarshal(raw)?; - Ok(ParamForwardTsnSupported {}) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - 0 - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_header.rs b/sctp/src/param/param_header.rs deleted file mode 100644 index a004f3ab3..000000000 --- a/sctp/src/param/param_header.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_type::*; -use super::*; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct ParamHeader { - pub(crate) typ: ParamType, - pub(crate) value_length: u16, -} - -pub(crate) const PARAM_HEADER_LENGTH: usize = 4; - -/// String makes paramHeader printable -impl fmt::Display for ParamHeader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.typ) - } -} - -impl Param for ParamHeader { - fn header(&self) -> ParamHeader { - self.clone() - } - - fn unmarshal(raw: &Bytes) -> Result { - if raw.len() < PARAM_HEADER_LENGTH { - return Err(Error::ErrParamHeaderTooShort); - } - - let reader = &mut raw.clone(); - - let typ: ParamType = reader.get_u16().into(); - - let len = reader.get_u16() as usize; - if len < PARAM_HEADER_LENGTH || raw.len() < len { - return Err(Error::ErrParamHeaderTooShort); - } - - Ok(ParamHeader { - typ, - value_length: (len - PARAM_HEADER_LENGTH) as u16, - }) - } - - fn marshal_to(&self, writer: &mut BytesMut) -> Result { - writer.put_u16(self.typ.into()); - writer.put_u16(self.value_length + PARAM_HEADER_LENGTH as u16); - Ok(writer.len()) - } - - fn value_length(&self) -> usize { - self.value_length as usize - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_heartbeat_info.rs b/sctp/src/param/param_heartbeat_info.rs deleted file mode 100644 index 12b310381..000000000 --- a/sctp/src/param/param_heartbeat_info.rs +++ /dev/null @@ -1,52 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamHeartbeatInfo { - pub(crate) heartbeat_information: Bytes, -} - -impl fmt::Display for ParamHeartbeatInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {:?}", self.header(), self.heartbeat_information) - } -} - -impl Param for ParamHeartbeatInfo { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::HeartbeatInfo, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - let heartbeat_information = - raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - Ok(ParamHeartbeatInfo { - heartbeat_information, - }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.extend(self.heartbeat_information.clone()); - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.heartbeat_information.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_outgoing_reset_request.rs b/sctp/src/param/param_outgoing_reset_request.rs deleted file mode 100644 index abd2a78b7..000000000 --- a/sctp/src/param/param_outgoing_reset_request.rs +++ /dev/null @@ -1,124 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -pub(crate) const PARAM_OUTGOING_RESET_REQUEST_STREAM_IDENTIFIERS_OFFSET: usize = 12; - -///This parameter is used by the sender to request the reset of some or -///all outgoing streams. -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Parameter Type = 13 | Parameter Length = 16 + 2 * N | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Re-configuration Request Sequence Number | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Re-configuration Response Sequence Number | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Sender's Last Assigned TSN | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Stream Number 1 (optional) | Stream Number 2 (optional) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| ...... | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Stream Number N-1 (optional) | Stream Number N (optional) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamOutgoingResetRequest { - /// reconfig_request_sequence_number is used to identify the request. It is a monotonically - /// increasing number that is initialized to the same value as the - /// initial TSN. It is increased by 1 whenever sending a new Re- - /// configuration Request Parameter. - pub(crate) reconfig_request_sequence_number: u32, - /// When this Outgoing SSN Reset Request Parameter is sent in response - /// to an Incoming SSN Reset Request Parameter, this parameter is also - /// an implicit response to the incoming request. This field then - /// holds the Re-configuration Request Sequence Number of the incoming - /// request. In other cases, it holds the next expected - /// Re-configuration Request Sequence Number minus 1. - pub(crate) reconfig_response_sequence_number: u32, - /// This value holds the next TSN minus 1 -- in other words, the last - /// TSN that this sender assigned. - pub(crate) sender_last_tsn: u32, - /// This optional field, if included, is used to indicate specific - /// streams that are to be reset. If no streams are listed, then all - /// streams are to be reset. - pub(crate) stream_identifiers: Vec, -} - -impl fmt::Display for ParamOutgoingResetRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {} {} {:?}", - self.header(), - self.reconfig_request_sequence_number, - self.reconfig_request_sequence_number, - self.reconfig_response_sequence_number, - self.stream_identifiers - ) - } -} - -impl Param for ParamOutgoingResetRequest { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::OutSsnResetReq, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - - // validity of value_length is checked in ParamHeader::unmarshal - if header.value_length() < PARAM_OUTGOING_RESET_REQUEST_STREAM_IDENTIFIERS_OFFSET { - return Err(Error::ErrSsnResetRequestParamTooShort); - } - - let reader = - &mut raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - let reconfig_request_sequence_number = reader.get_u32(); - let reconfig_response_sequence_number = reader.get_u32(); - let sender_last_tsn = reader.get_u32(); - - let lim = - (header.value_length() - PARAM_OUTGOING_RESET_REQUEST_STREAM_IDENTIFIERS_OFFSET) / 2; - let mut stream_identifiers = vec![]; - for _ in 0..lim { - stream_identifiers.push(reader.get_u16()); - } - - Ok(ParamOutgoingResetRequest { - reconfig_request_sequence_number, - reconfig_response_sequence_number, - sender_last_tsn, - stream_identifiers, - }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.put_u32(self.reconfig_request_sequence_number); - buf.put_u32(self.reconfig_response_sequence_number); - buf.put_u32(self.sender_last_tsn); - for sid in &self.stream_identifiers { - buf.put_u16(*sid); - } - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - PARAM_OUTGOING_RESET_REQUEST_STREAM_IDENTIFIERS_OFFSET + self.stream_identifiers.len() * 2 - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_random.rs b/sctp/src/param/param_random.rs deleted file mode 100644 index d645b76e5..000000000 --- a/sctp/src/param/param_random.rs +++ /dev/null @@ -1,50 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamRandom { - pub(crate) random_data: Bytes, -} - -impl fmt::Display for ParamRandom { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {:?}", self.header(), self.random_data) - } -} - -impl Param for ParamRandom { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::Random, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - let random_data = - raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - Ok(ParamRandom { random_data }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.extend(self.random_data.clone()); - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.random_data.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_reconfig_response.rs b/sctp/src/param/param_reconfig_response.rs deleted file mode 100644 index 7ef7a029c..000000000 --- a/sctp/src/param/param_reconfig_response.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -#[derive(Default, Debug, Copy, Clone, PartialEq)] -#[repr(C)] -pub(crate) enum ReconfigResult { - SuccessNop = 0, - SuccessPerformed = 1, - Denied = 2, - ErrorWrongSsn = 3, - ErrorRequestAlreadyInProgress = 4, - ErrorBadSequenceNumber = 5, - InProgress = 6, - #[default] - Unknown, -} - -impl fmt::Display for ReconfigResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - ReconfigResult::SuccessNop => "0: Success - Nothing to do", - ReconfigResult::SuccessPerformed => "1: Success - Performed", - ReconfigResult::Denied => "2: Denied", - ReconfigResult::ErrorWrongSsn => "3: Error - Wrong SSN", - ReconfigResult::ErrorRequestAlreadyInProgress => { - "4: Error - Request already in progress" - } - ReconfigResult::ErrorBadSequenceNumber => "5: Error - Bad Sequence Number", - ReconfigResult::InProgress => "6: In progress", - _ => "Unknown ReconfigResult", - }; - write!(f, "{s}") - } -} - -impl From for ReconfigResult { - fn from(v: u32) -> ReconfigResult { - match v { - 0 => ReconfigResult::SuccessNop, - 1 => ReconfigResult::SuccessPerformed, - 2 => ReconfigResult::Denied, - 3 => ReconfigResult::ErrorWrongSsn, - 4 => ReconfigResult::ErrorRequestAlreadyInProgress, - 5 => ReconfigResult::ErrorBadSequenceNumber, - 6 => ReconfigResult::InProgress, - _ => ReconfigResult::Unknown, - } - } -} - -///This parameter is used by the receiver of a Re-configuration Request -///Parameter to respond to the request. -/// -///0 1 2 3 -///0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Parameter Type = 16 | Parameter Length | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Re-configuration Response Sequence Number | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Result | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Sender's Next TSN (optional) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -///| Receiver's Next TSN (optional) | -///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamReconfigResponse { - /// This value is copied from the request parameter and is used by the - /// receiver of the Re-configuration Response Parameter to tie the - /// response to the request. - pub(crate) reconfig_response_sequence_number: u32, - /// This value describes the result of the processing of the request. - pub(crate) result: ReconfigResult, -} - -impl fmt::Display for ParamReconfigResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {}", - self.header(), - self.reconfig_response_sequence_number, - self.result - ) - } -} - -impl Param for ParamReconfigResponse { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::ReconfigResp, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - - // validity of value_length is checked in ParamHeader::unmarshal - if header.value_length < 8 { - return Err(Error::ErrReconfigRespParamTooShort); - } - - let reader = - &mut raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - - let reconfig_response_sequence_number = reader.get_u32(); - let result = reader.get_u32().into(); - - Ok(ParamReconfigResponse { - reconfig_response_sequence_number, - result, - }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.put_u32(self.reconfig_response_sequence_number); - buf.put_u32(self.result as u32); - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - 8 - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_requested_hmac_algorithm.rs b/sctp/src/param/param_requested_hmac_algorithm.rs deleted file mode 100644 index e9ef97b32..000000000 --- a/sctp/src/param/param_requested_hmac_algorithm.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::fmt; - -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -#[derive(Debug, Copy, Clone, PartialEq)] -#[repr(C)] -pub(crate) enum HmacAlgorithm { - HmacResv1 = 0, - HmacSha128 = 1, - HmacResv2 = 2, - HmacSha256 = 3, - Unknown, -} - -impl fmt::Display for HmacAlgorithm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - HmacAlgorithm::HmacResv1 => "HMAC Reserved (0x00)", - HmacAlgorithm::HmacSha128 => "HMAC SHA-128", - HmacAlgorithm::HmacResv2 => "HMAC Reserved (0x02)", - HmacAlgorithm::HmacSha256 => "HMAC SHA-256", - _ => "Unknown HMAC Algorithm", - }; - write!(f, "{s}") - } -} - -impl From for HmacAlgorithm { - fn from(v: u16) -> HmacAlgorithm { - match v { - 0 => HmacAlgorithm::HmacResv1, - 1 => HmacAlgorithm::HmacSha128, - 2 => HmacAlgorithm::HmacResv2, - 3 => HmacAlgorithm::HmacSha256, - _ => HmacAlgorithm::Unknown, - } - } -} - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamRequestedHmacAlgorithm { - pub(crate) available_algorithms: Vec, -} - -impl fmt::Display for ParamRequestedHmacAlgorithm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}", - self.header(), - self.available_algorithms - .iter() - .map(|ct| ct.to_string()) - .collect::>() - .join(" "), - ) - } -} - -impl Param for ParamRequestedHmacAlgorithm { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::ReqHmacAlgo, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - - let reader = - &mut raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - - let mut available_algorithms = vec![]; - let mut offset = 0; - while offset + 1 < header.value_length() { - let a: HmacAlgorithm = reader.get_u16().into(); - if a == HmacAlgorithm::HmacSha128 || a == HmacAlgorithm::HmacSha256 { - available_algorithms.push(a); - } else { - return Err(Error::ErrInvalidAlgorithmType); - } - - offset += 2; - } - - Ok(ParamRequestedHmacAlgorithm { - available_algorithms, - }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for a in &self.available_algorithms { - buf.put_u16(*a as u16); - } - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - 2 * self.available_algorithms.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_state_cookie.rs b/sctp/src/param/param_state_cookie.rs deleted file mode 100644 index 7c9e785d3..000000000 --- a/sctp/src/param/param_state_cookie.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::fmt; - -use bytes::{Bytes, BytesMut}; -use rand::Rng; - -use super::param_header::*; -use super::param_type::*; -use super::*; - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamStateCookie { - pub(crate) cookie: Bytes, -} - -/// String makes paramStateCookie printable -impl fmt::Display for ParamStateCookie { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {:?}", self.header(), self.cookie) - } -} - -impl Param for ParamStateCookie { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::StateCookie, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - let cookie = raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - Ok(ParamStateCookie { cookie }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - buf.extend(self.cookie.clone()); - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.cookie.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} - -impl ParamStateCookie { - pub(crate) fn new() -> Self { - let mut cookie = BytesMut::new(); - cookie.resize(32, 0); - rand::thread_rng().fill(cookie.as_mut()); - - ParamStateCookie { - cookie: cookie.freeze(), - } - } -} diff --git a/sctp/src/param/param_supported_extensions.rs b/sctp/src/param/param_supported_extensions.rs deleted file mode 100644 index b1689959d..000000000 --- a/sctp/src/param/param_supported_extensions.rs +++ /dev/null @@ -1,69 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use super::param_header::*; -use super::param_type::*; -use super::*; -use crate::chunk::chunk_type::*; - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct ParamSupportedExtensions { - pub(crate) chunk_types: Vec, -} - -impl fmt::Display for ParamSupportedExtensions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}", - self.header(), - self.chunk_types - .iter() - .map(|ct| ct.to_string()) - .collect::>() - .join(" "), - ) - } -} - -impl Param for ParamSupportedExtensions { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::SupportedExt, - value_length: self.value_length() as u16, - } - } - - fn unmarshal(raw: &Bytes) -> Result { - let header = ParamHeader::unmarshal(raw)?; - - let reader = - &mut raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - - let mut chunk_types = vec![]; - while reader.has_remaining() { - chunk_types.push(ChunkType(reader.get_u8())); - } - - Ok(ParamSupportedExtensions { chunk_types }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> Result { - self.header().marshal_to(buf)?; - for ct in &self.chunk_types { - buf.put_u8(ct.0); - } - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.chunk_types.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } -} diff --git a/sctp/src/param/param_test.rs b/sctp/src/param/param_test.rs deleted file mode 100644 index d8c567ecd..000000000 --- a/sctp/src/param/param_test.rs +++ /dev/null @@ -1,270 +0,0 @@ -/////////////////////////////////////////////////////////////////// -//param_type_test -/////////////////////////////////////////////////////////////////// -use super::param_type::*; -use super::*; - -#[test] -fn test_parse_param_type_success() -> Result<()> { - let tests = vec![ - (Bytes::from_static(&[0x0, 0x1]), ParamType::HeartbeatInfo), - (Bytes::from_static(&[0x0, 0xd]), ParamType::OutSsnResetReq), - ]; - - for (mut binary, expected) in tests { - let pt: ParamType = binary.get_u16().into(); - assert_eq!(pt, expected); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//param_header_test -/////////////////////////////////////////////////////////////////// -use super::param_header::*; - -static PARAM_HEADER_BYTES: Bytes = Bytes::from_static(&[0x0, 0x1, 0x0, 0x4]); - -#[test] -fn test_param_header_success() -> Result<()> { - let tests = vec![( - PARAM_HEADER_BYTES.clone(), - ParamHeader { - typ: ParamType::HeartbeatInfo, - value_length: 0, - }, - )]; - - for (binary, parsed) in tests { - let actual = ParamHeader::unmarshal(&binary)?; - assert_eq!(actual, parsed); - let b = actual.marshal()?; - assert_eq!(b, binary); - } - - Ok(()) -} - -#[test] -fn test_param_header_unmarshal_failure() -> Result<()> { - let tests = vec![ - ("header too short", PARAM_HEADER_BYTES.slice(..2)), - // {"wrong param type", []byte{0x0, 0x0, 0x0, 0x4}}, // Not possible to fail parseParamType atm. - ( - "reported length below header length", - Bytes::from_static(&[0x0, 0xd, 0x0, 0x3]), - ), - ("wrong reported length", CHUNK_RECONFIG_PARAM_A.slice(0..4)), - ]; - - for (name, binary) in tests { - let result = ParamHeader::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//param_forward_tsn_supported_test -/////////////////////////////////////////////////////////////////// -use super::param_forward_tsn_supported::*; - -static PARAM_FORWARD_TSN_SUPPORTED_BYTES: Bytes = Bytes::from_static(&[0xc0, 0x0, 0x0, 0x4]); - -#[test] -fn test_param_forward_tsn_supported_success() -> Result<()> { - let tests = vec![( - PARAM_FORWARD_TSN_SUPPORTED_BYTES.clone(), - ParamForwardTsnSupported {}, - )]; - - for (binary, parsed) in tests { - let actual = ParamForwardTsnSupported::unmarshal(&binary)?; - assert_eq!(actual, parsed); - let b = actual.marshal()?; - assert_eq!(b, binary); - } - - Ok(()) -} - -#[test] -fn test_param_forward_tsn_supported_failure() -> Result<()> { - let tests = vec![("param too short", Bytes::from_static(&[0x0, 0xd, 0x0]))]; - - for (name, binary) in tests { - let result = ParamForwardTsnSupported::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//param_outgoing_reset_request_test -/////////////////////////////////////////////////////////////////// -use super::param_outgoing_reset_request::*; - -static CHUNK_RECONFIG_PARAM_A: Bytes = Bytes::from_static(&[ - 0x0, 0xd, 0x0, 0x16, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, - 0x5, 0x0, 0x6, -]); -static CHUNK_RECONFIG_PARAM_B: Bytes = Bytes::from_static(&[ - 0x0, 0xd, 0x0, 0x10, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, -]); - -#[test] -fn test_param_outgoing_reset_request_success() -> Result<()> { - let tests = vec![ - ( - CHUNK_RECONFIG_PARAM_A.clone(), - ParamOutgoingResetRequest { - reconfig_request_sequence_number: 1, - reconfig_response_sequence_number: 2, - sender_last_tsn: 3, - stream_identifiers: vec![4, 5, 6], - }, - ), - ( - CHUNK_RECONFIG_PARAM_B.clone(), - ParamOutgoingResetRequest { - reconfig_request_sequence_number: 1, - reconfig_response_sequence_number: 2, - sender_last_tsn: 3, - stream_identifiers: vec![], - }, - ), - ]; - - for (binary, parsed) in tests { - let actual = ParamOutgoingResetRequest::unmarshal(&binary)?; - assert_eq!(actual, parsed); - let b = actual.marshal()?; - assert_eq!(b, binary); - } - - Ok(()) -} - -#[test] -fn test_param_outgoing_reset_request_failure() -> Result<()> { - let tests = vec![ - ("packet too short", CHUNK_RECONFIG_PARAM_A.slice(..8)), - ("param too short", Bytes::from_static(&[0x0, 0xd, 0x0, 0x4])), - ]; - - for (name, binary) in tests { - let result = ParamOutgoingResetRequest::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//param_reconfig_response_test -/////////////////////////////////////////////////////////////////// -use bytes::Buf; - -use super::param_reconfig_response::*; - -static CHUNK_RECONFIG_RESPONSE: Bytes = - Bytes::from_static(&[0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1]); - -#[test] -fn test_param_reconfig_response_success() -> Result<()> { - let tests = vec![( - CHUNK_RECONFIG_RESPONSE.clone(), - ParamReconfigResponse { - reconfig_response_sequence_number: 1, - result: ReconfigResult::SuccessPerformed, - }, - )]; - - for (binary, parsed) in tests { - let actual = ParamReconfigResponse::unmarshal(&binary)?; - assert_eq!(actual, parsed); - let b = actual.marshal()?; - assert_eq!(b, binary); - } - - Ok(()) -} - -#[test] -fn test_param_reconfig_response_failure() -> Result<()> { - let tests = vec![ - ("packet too short", CHUNK_RECONFIG_RESPONSE.slice(..8)), - ( - "param too short", - Bytes::from_static(&[0x0, 0x10, 0x0, 0x4]), - ), - ]; - - for (name, binary) in tests { - let result = ParamReconfigResponse::unmarshal(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} - -#[test] -fn test_reconfig_result_stringer() -> Result<()> { - let tests = vec![ - (ReconfigResult::SuccessNop, "0: Success - Nothing to do"), - (ReconfigResult::SuccessPerformed, "1: Success - Performed"), - (ReconfigResult::Denied, "2: Denied"), - (ReconfigResult::ErrorWrongSsn, "3: Error - Wrong SSN"), - ( - ReconfigResult::ErrorRequestAlreadyInProgress, - "4: Error - Request already in progress", - ), - ( - ReconfigResult::ErrorBadSequenceNumber, - "5: Error - Bad Sequence Number", - ), - (ReconfigResult::InProgress, "6: In progress"), - ]; - - for (result, expected) in tests { - let actual = result.to_string(); - assert_eq!(actual, expected, "Test case {expected}"); - } - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//param_test -/////////////////////////////////////////////////////////////////// - -#[test] -fn test_build_param_success() -> Result<()> { - let tests = vec![CHUNK_RECONFIG_PARAM_A.clone()]; - - for binary in tests { - let p = build_param(&binary)?; - let b = p.marshal()?; - assert_eq!(b, binary); - } - - Ok(()) -} - -#[test] -fn test_build_param_failure() -> Result<()> { - let tests = vec![ - ("invalid ParamType", Bytes::from_static(&[0x0, 0x0])), - ("build failure", CHUNK_RECONFIG_PARAM_A.slice(..8)), - ]; - - for (name, binary) in tests { - let result = build_param(&binary); - assert!(result.is_err(), "expected unmarshal: {name} to fail."); - } - - Ok(()) -} diff --git a/sctp/src/param/param_type.rs b/sctp/src/param/param_type.rs deleted file mode 100644 index 7e195e5db..000000000 --- a/sctp/src/param/param_type.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::fmt; - -/// paramType represents a SCTP INIT/INITACK parameter -#[derive(Debug, Copy, Clone, PartialEq)] -#[repr(C)] -pub(crate) enum ParamType { - HeartbeatInfo, - /// Heartbeat Info [RFCRFC4960] - Ipv4Addr, - /// IPv4 IP [RFCRFC4960] - Ipv6Addr, - /// IPv6 IP [RFCRFC4960] - StateCookie, - /// State Cookie [RFCRFC4960] - UnrecognizedParam, - /// Unrecognized Parameters [RFCRFC4960] - CookiePreservative, - /// Cookie Preservative [RFCRFC4960] - HostNameAddr, - /// Host Name IP [RFCRFC4960] - SupportedAddrTypes, - /// Supported IP Types [RFCRFC4960] - OutSsnResetReq, - /// Outgoing SSN Reset Request Parameter [RFCRFC6525] - IncSsnResetReq, - /// Incoming SSN Reset Request Parameter [RFCRFC6525] - SsnTsnResetReq, - /// SSN/TSN Reset Request Parameter [RFCRFC6525] - ReconfigResp, - /// Re-configuration Response Parameter [RFCRFC6525] - AddOutStreamsReq, - /// Add Outgoing Streams Request Parameter [RFCRFC6525] - AddIncStreamsReq, - /// Add Incoming Streams Request Parameter [RFCRFC6525] - Random, - /// Random (0x8002) [RFCRFC4805] - ChunkList, - /// Chunk List (0x8003) [RFCRFC4895] - ReqHmacAlgo, - /// Requested HMAC Algorithm Parameter (0x8004) [RFCRFC4895] - Padding, - /// Padding (0x8005) - SupportedExt, - /// Supported Extensions (0x8008) [RFCRFC5061] - ForwardTsnSupp, - /// Forward TSN supported (0xC000) [RFCRFC3758] - AddIpAddr, - /// Add IP IP (0xC001) [RFCRFC5061] - DelIpaddr, - /// Delete IP IP (0xC002) [RFCRFC5061] - ErrClauseInd, - /// Error Cause Indication (0xC003) [RFCRFC5061] - SetPriAddr, - /// Set Primary IP (0xC004) [RFCRFC5061] - SuccessInd, - /// Success Indication (0xC005) [RFCRFC5061] - AdaptLayerInd, - /// Adaptation Layer Indication (0xC006) [RFCRFC5061] - Unknown { - param_type: u16, - }, -} - -impl fmt::Display for ParamType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - ParamType::HeartbeatInfo => "Heartbeat Info", - ParamType::Ipv4Addr => "IPv4 IP", - ParamType::Ipv6Addr => "IPv6 IP", - ParamType::StateCookie => "State Cookie", - ParamType::UnrecognizedParam => "Unrecognized Parameters", - ParamType::CookiePreservative => "Cookie Preservative", - ParamType::HostNameAddr => "Host Name IP", - ParamType::SupportedAddrTypes => "Supported IP Types", - ParamType::OutSsnResetReq => "Outgoing SSN Reset Request Parameter", - ParamType::IncSsnResetReq => "Incoming SSN Reset Request Parameter", - ParamType::SsnTsnResetReq => "SSN/TSN Reset Request Parameter", - ParamType::ReconfigResp => "Re-configuration Response Parameter", - ParamType::AddOutStreamsReq => "Add Outgoing Streams Request Parameter", - ParamType::AddIncStreamsReq => "Add Incoming Streams Request Parameter", - ParamType::Random => "Random", - ParamType::ChunkList => "Chunk List", - ParamType::ReqHmacAlgo => "Requested HMAC Algorithm Parameter", - ParamType::Padding => "Padding", - ParamType::SupportedExt => "Supported Extensions", - ParamType::ForwardTsnSupp => "Forward TSN supported", - ParamType::AddIpAddr => "Add IP IP", - ParamType::DelIpaddr => "Delete IP IP", - ParamType::ErrClauseInd => "Error Cause Indication", - ParamType::SetPriAddr => "Set Primary IP", - ParamType::SuccessInd => "Success Indication", - ParamType::AdaptLayerInd => "Adaptation Layer Indication", - _ => "Unknown ParamType", - }; - write!(f, "{s}") - } -} - -impl From for ParamType { - fn from(v: u16) -> ParamType { - match v { - 1 => ParamType::HeartbeatInfo, - 5 => ParamType::Ipv4Addr, - 6 => ParamType::Ipv6Addr, - 7 => ParamType::StateCookie, - 8 => ParamType::UnrecognizedParam, - 9 => ParamType::CookiePreservative, - 11 => ParamType::HostNameAddr, - 12 => ParamType::SupportedAddrTypes, - 13 => ParamType::OutSsnResetReq, - 14 => ParamType::IncSsnResetReq, - 15 => ParamType::SsnTsnResetReq, - 16 => ParamType::ReconfigResp, - 17 => ParamType::AddOutStreamsReq, - 18 => ParamType::AddIncStreamsReq, - 32770 => ParamType::Random, - 32771 => ParamType::ChunkList, - 32772 => ParamType::ReqHmacAlgo, - 32773 => ParamType::Padding, - 32776 => ParamType::SupportedExt, - 49152 => ParamType::ForwardTsnSupp, - 49153 => ParamType::AddIpAddr, - 49154 => ParamType::DelIpaddr, - 49155 => ParamType::ErrClauseInd, - 49156 => ParamType::SetPriAddr, - 49157 => ParamType::SuccessInd, - 49158 => ParamType::AdaptLayerInd, - unknown => ParamType::Unknown { - param_type: unknown, - }, - } - } -} - -impl From for u16 { - fn from(v: ParamType) -> u16 { - match v { - ParamType::HeartbeatInfo => 1, - ParamType::Ipv4Addr => 5, - ParamType::Ipv6Addr => 6, - ParamType::StateCookie => 7, - ParamType::UnrecognizedParam => 8, - ParamType::CookiePreservative => 9, - ParamType::HostNameAddr => 11, - ParamType::SupportedAddrTypes => 12, - ParamType::OutSsnResetReq => 13, - ParamType::IncSsnResetReq => 14, - ParamType::SsnTsnResetReq => 15, - ParamType::ReconfigResp => 16, - ParamType::AddOutStreamsReq => 17, - ParamType::AddIncStreamsReq => 18, - ParamType::Random => 32770, - ParamType::ChunkList => 32771, - ParamType::ReqHmacAlgo => 32772, - ParamType::Padding => 32773, - ParamType::SupportedExt => 32776, - ParamType::ForwardTsnSupp => 49152, - ParamType::AddIpAddr => 49153, - ParamType::DelIpaddr => 49154, - ParamType::ErrClauseInd => 49155, - ParamType::SetPriAddr => 49156, - ParamType::SuccessInd => 49157, - ParamType::AdaptLayerInd => 49158, - ParamType::Unknown { param_type, .. } => param_type, - } - } -} diff --git a/sctp/src/param/param_unknown.rs b/sctp/src/param/param_unknown.rs deleted file mode 100644 index 028b38169..000000000 --- a/sctp/src/param/param_unknown.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::any::Any; -use std::fmt::{Debug, Display, Formatter}; - -use bytes::{Bytes, BytesMut}; - -use crate::param::param_header::{ParamHeader, PARAM_HEADER_LENGTH}; -use crate::param::param_type::ParamType; -use crate::param::Param; - -/// This type is meant to represent ANY parameter for un/remarshaling purposes, where we do not have a more specific type for it. -/// This means we do not really understand the semantics of the param but can represent it. -/// -/// This is useful for usage in e.g.`ParamUnrecognized` where we want to report some unrecognized params back to the sender. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ParamUnknown { - typ: u16, - value: Bytes, -} - -impl Display for ParamUnknown { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "ParamUnknown( {} {:?} )", self.header(), self.value) - } -} - -impl Param for ParamUnknown { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::Unknown { - param_type: self.typ, - }, - value_length: self.value.len() as u16, - } - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn unmarshal(raw: &Bytes) -> crate::error::Result - where - Self: Sized, - { - let header = ParamHeader::unmarshal(raw)?; - let value = raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - Ok(Self { - typ: header.typ.into(), - value, - }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> crate::error::Result { - self.header().marshal_to(buf)?; - buf.extend(self.value.clone()); - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.value.len() - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/sctp/src/param/param_unrecognized.rs b/sctp/src/param/param_unrecognized.rs deleted file mode 100644 index dc3c6dfb4..000000000 --- a/sctp/src/param/param_unrecognized.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::any::Any; -use std::fmt::{Debug, Display, Formatter}; - -use bytes::{Bytes, BytesMut}; - -use crate::param::param_header::PARAM_HEADER_LENGTH; -use crate::param::param_type::ParamType; -use crate::param::{build_param, Param, ParamHeader}; - -/// This is the parameter type used to report unrecognized parameters in e.g. init chunks back to the sender in the init ack. -/// The contained param is likely to be a `ParamUnknown` but might be something more specific. -#[derive(Clone, Debug)] -pub struct ParamUnrecognized { - param: Box, -} - -impl ParamUnrecognized { - pub(crate) fn wrap(param: Box) -> Self { - Self { param } - } -} - -impl Display for ParamUnrecognized { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str("UnrecognizedParam")?; - Display::fmt(&self.param, f) - } -} - -impl Param for ParamUnrecognized { - fn header(&self) -> ParamHeader { - ParamHeader { - typ: ParamType::UnrecognizedParam, - value_length: self.value_length() as u16, - } - } - - fn as_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn unmarshal(raw: &Bytes) -> crate::error::Result - where - Self: Sized, - { - let header = ParamHeader::unmarshal(raw)?; - let raw_param = raw.slice(PARAM_HEADER_LENGTH..PARAM_HEADER_LENGTH + header.value_length()); - let param = build_param(&raw_param)?; - Ok(Self { param }) - } - - fn marshal_to(&self, buf: &mut BytesMut) -> crate::error::Result { - self.header().marshal_to(buf)?; - self.param.marshal_to(buf)?; - Ok(buf.len()) - } - - fn value_length(&self) -> usize { - self.param.value_length() + PARAM_HEADER_LENGTH - } - - fn clone_to(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/sctp/src/queue/control_queue.rs b/sctp/src/queue/control_queue.rs deleted file mode 100644 index 10b19539f..000000000 --- a/sctp/src/queue/control_queue.rs +++ /dev/null @@ -1,6 +0,0 @@ -use std::collections::VecDeque; - -use crate::packet::Packet; - -/// control queue -pub(crate) type ControlQueue = VecDeque; diff --git a/sctp/src/queue/mod.rs b/sctp/src/queue/mod.rs deleted file mode 100644 index 836e7aeb4..000000000 --- a/sctp/src/queue/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod queue_test; - -pub(crate) mod control_queue; -pub(crate) mod payload_queue; -pub(crate) mod pending_queue; -pub(crate) mod reassembly_queue; diff --git a/sctp/src/queue/payload_queue.rs b/sctp/src/queue/payload_queue.rs deleted file mode 100644 index 481d99d9e..000000000 --- a/sctp/src/queue/payload_queue.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicUsize; - -use crate::chunk::chunk_payload_data::ChunkPayloadData; -use crate::chunk::chunk_selective_ack::GapAckBlock; -use crate::util::*; - -#[derive(Default, Debug)] -pub(crate) struct PayloadQueue { - pub(crate) length: Arc, - pub(crate) chunk_map: HashMap, - pub(crate) sorted: VecDeque, - pub(crate) dup_tsn: Vec, - pub(crate) n_bytes: usize, -} - -impl PayloadQueue { - pub(crate) fn new(length: Arc) -> Self { - length.store(0, Ordering::SeqCst); - PayloadQueue { - length, - ..Default::default() - } - } - - pub(crate) fn can_push(&self, p: &ChunkPayloadData, cumulative_tsn: u32) -> bool { - !(self.chunk_map.contains_key(&p.tsn) || sna32lte(p.tsn, cumulative_tsn)) - } - - pub(crate) fn push_no_check(&mut self, p: ChunkPayloadData) { - let tsn = p.tsn; - self.n_bytes += p.user_data.len(); - self.chunk_map.insert(tsn, p); - self.length.fetch_add(1, Ordering::SeqCst); - - if self.sorted.is_empty() || sna32gt(tsn, *self.sorted.back().unwrap()) { - self.sorted.push_back(tsn); - } else if sna32lt(tsn, *self.sorted.front().unwrap()) { - self.sorted.push_front(tsn); - } else { - fn compare_tsn(a: u32, b: u32) -> std::cmp::Ordering { - if sna32lt(a, b) { - std::cmp::Ordering::Less - } else { - std::cmp::Ordering::Greater - } - } - let pos = match self - .sorted - .binary_search_by(|element| compare_tsn(*element, tsn)) - { - Ok(pos) => pos, - Err(pos) => pos, - }; - self.sorted.insert(pos, tsn); - } - } - - /// push pushes a payload data. If the payload data is already in our queue or - /// older than our cumulative_tsn marker, it will be recorded as duplications, - /// which can later be retrieved using popDuplicates. - pub(crate) fn push(&mut self, p: ChunkPayloadData, cumulative_tsn: u32) -> bool { - let ok = self.chunk_map.contains_key(&p.tsn); - if ok || sna32lte(p.tsn, cumulative_tsn) { - // Found the packet, log in dups - self.dup_tsn.push(p.tsn); - return false; - } - - self.push_no_check(p); - true - } - - /// pop pops only if the oldest chunk's TSN matches the given TSN. - pub(crate) fn pop(&mut self, tsn: u32) -> Option { - if Some(&tsn) == self.sorted.front() { - self.sorted.pop_front(); - if let Some(c) = self.chunk_map.remove(&tsn) { - self.length.fetch_sub(1, Ordering::SeqCst); - self.n_bytes -= c.user_data.len(); - return Some(c); - } - } - - None - } - - /// get returns reference to chunkPayloadData with the given TSN value. - pub(crate) fn get(&self, tsn: u32) -> Option<&ChunkPayloadData> { - self.chunk_map.get(&tsn) - } - pub(crate) fn get_mut(&mut self, tsn: u32) -> Option<&mut ChunkPayloadData> { - self.chunk_map.get_mut(&tsn) - } - - /// popDuplicates returns an array of TSN values that were found duplicate. - pub(crate) fn pop_duplicates(&mut self) -> Vec { - self.dup_tsn.drain(..).collect() - } - - pub(crate) fn get_gap_ack_blocks(&self, cumulative_tsn: u32) -> Vec { - if self.chunk_map.is_empty() { - return vec![]; - } - - let mut b = GapAckBlock::default(); - let mut gap_ack_blocks = vec![]; - for (i, tsn) in self.sorted.iter().enumerate() { - let diff = if *tsn >= cumulative_tsn { - (*tsn - cumulative_tsn) as u16 - } else { - 0 - }; - - if i == 0 { - b.start = diff; - b.end = b.start; - } else if b.end + 1 == diff { - b.end += 1; - } else { - gap_ack_blocks.push(b); - - b.start = diff; - b.end = diff; - } - } - - gap_ack_blocks.push(b); - - gap_ack_blocks - } - - pub(crate) fn get_gap_ack_blocks_string(&self, cumulative_tsn: u32) -> String { - let mut s = format!("cumTSN={cumulative_tsn}"); - for b in self.get_gap_ack_blocks(cumulative_tsn) { - s += format!(",{}-{}", b.start, b.end).as_str(); - } - s - } - - pub(crate) fn mark_as_acked(&mut self, tsn: u32) -> usize { - let n_bytes_acked = if let Some(c) = self.chunk_map.get_mut(&tsn) { - c.acked = true; - c.retransmit = false; - let n = c.user_data.len(); - self.n_bytes -= n; - c.user_data.clear(); - n - } else { - 0 - }; - - n_bytes_acked - } - - pub(crate) fn get_last_tsn_received(&self) -> Option<&u32> { - self.sorted.back() - } - - pub(crate) fn mark_all_to_retrasmit(&mut self) { - for c in self.chunk_map.values_mut() { - if c.acked || c.abandoned() { - continue; - } - c.retransmit = true; - } - } - - pub(crate) fn get_num_bytes(&self) -> usize { - self.n_bytes - } - - pub(crate) fn len(&self) -> usize { - assert_eq!(self.chunk_map.len(), self.length.load(Ordering::SeqCst)); - self.chunk_map.len() - } - - pub(crate) fn is_empty(&self) -> bool { - self.len() == 0 - } -} diff --git a/sctp/src/queue/pending_queue.rs b/sctp/src/queue/pending_queue.rs deleted file mode 100644 index caff6ab42..000000000 --- a/sctp/src/queue/pending_queue.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::collections::VecDeque; -use std::sync::atomic::Ordering; - -use portable_atomic::{AtomicBool, AtomicUsize}; -use tokio::sync::{Mutex, Semaphore}; -use util::sync::RwLock; - -use crate::chunk::chunk_payload_data::ChunkPayloadData; - -// TODO: benchmark performance between multiple Atomic+Mutex vs one Mutex - -// Some tests push a lot of data before starting to process any data... -#[cfg(test)] -const QUEUE_BYTES_LIMIT: usize = 128 * 1024 * 1024; -/// Maximum size of the pending queue, in bytes. -#[cfg(not(test))] -const QUEUE_BYTES_LIMIT: usize = 128 * 1024; -/// Total user data size, beyond which the packet will be split into chunks. The chunks will be -/// added to the pending queue one by one. -const QUEUE_APPEND_LARGE: usize = (QUEUE_BYTES_LIMIT * 2) / 3; - -/// Basic queue for either ordered or unordered chunks. -pub(crate) type PendingBaseQueue = VecDeque; - -/// A queue for both ordered and unordered chunks. -#[derive(Debug)] -pub(crate) struct PendingQueue { - // These two fields limit appending bytes to the queue - // This two step process is necessary because - // A) We need backpressure which the semaphore applies by limiting the total amount of bytes via the permits - // B) The chunks of one fragmented message need to be put in direct sequence into the queue which the lock guarantees - // - // The semaphore is not inside the lock because the permits need to be returned without needing a lock on the semaphore - semaphore_lock: Mutex<()>, - semaphore: Semaphore, - - unordered_queue: RwLock, - ordered_queue: RwLock, - queue_len: AtomicUsize, - n_bytes: AtomicUsize, - selected: AtomicBool, - unordered_is_selected: AtomicBool, -} - -impl Default for PendingQueue { - fn default() -> Self { - PendingQueue::new() - } -} - -impl PendingQueue { - pub(crate) fn new() -> Self { - Self { - semaphore_lock: Mutex::default(), - semaphore: Semaphore::new(QUEUE_BYTES_LIMIT), - unordered_queue: Default::default(), - ordered_queue: Default::default(), - queue_len: Default::default(), - n_bytes: Default::default(), - selected: Default::default(), - unordered_is_selected: Default::default(), - } - } - - /// Appends a chunk to the back of the pending queue. - pub(crate) async fn push(&self, c: ChunkPayloadData) { - let user_data_len = c.user_data.len(); - - { - let _sem_lock = self.semaphore_lock.lock().await; - let permits = self.semaphore.acquire_many(user_data_len as u32).await; - // unwrap ok because we never close the semaphore unless we have dropped self - permits.unwrap().forget(); - - if c.unordered { - let mut unordered_queue = self.unordered_queue.write(); - unordered_queue.push_back(c); - } else { - let mut ordered_queue = self.ordered_queue.write(); - ordered_queue.push_back(c); - } - } - - self.n_bytes.fetch_add(user_data_len, Ordering::SeqCst); - self.queue_len.fetch_add(1, Ordering::SeqCst); - } - - /// Appends chunks to the back of the pending queue. - /// - /// # Panics - /// - /// If it's a mix of unordered and ordered chunks. - pub(crate) async fn append(&self, chunks: Vec) { - if chunks.is_empty() { - return; - } - - let total_user_data_len = chunks.iter().fold(0, |acc, c| acc + c.user_data.len()); - - if total_user_data_len >= QUEUE_APPEND_LARGE { - self.append_large(chunks).await - } else { - let _sem_lock = self.semaphore_lock.lock().await; - let permits = self - .semaphore - .acquire_many(total_user_data_len as u32) - .await; - // unwrap ok because we never close the semaphore unless we have dropped self - permits.unwrap().forget(); - self.append_unlimited(chunks, total_user_data_len); - } - } - - // If this is a very large message we append chunks one by one to allow progress while we are appending - async fn append_large(&self, chunks: Vec) { - // lock this for the whole duration - let _sem_lock = self.semaphore_lock.lock().await; - - for chunk in chunks.into_iter() { - let user_data_len = chunk.user_data.len(); - let permits = self.semaphore.acquire_many(user_data_len as u32).await; - // unwrap ok because we never close the semaphore unless we have dropped self - permits.unwrap().forget(); - - if chunk.unordered { - let mut unordered_queue = self.unordered_queue.write(); - unordered_queue.push_back(chunk); - } else { - let mut ordered_queue = self.ordered_queue.write(); - ordered_queue.push_back(chunk); - } - self.n_bytes.fetch_add(user_data_len, Ordering::SeqCst); - self.queue_len.fetch_add(1, Ordering::SeqCst); - } - } - - /// Assumes that A) enough permits have been acquired and forget from the semaphore and that the semaphore_lock is held - fn append_unlimited(&self, chunks: Vec, total_user_data_len: usize) { - let chunks_len = chunks.len(); - let unordered = chunks - .first() - .expect("chunks to not be empty because of the above check") - .unordered; - if unordered { - let mut unordered_queue = self.unordered_queue.write(); - assert!( - chunks.iter().all(|c| c.unordered), - "expected all chunks to be unordered" - ); - unordered_queue.extend(chunks); - } else { - let mut ordered_queue = self.ordered_queue.write(); - assert!( - chunks.iter().all(|c| !c.unordered), - "expected all chunks to be ordered" - ); - ordered_queue.extend(chunks); - } - - self.n_bytes - .fetch_add(total_user_data_len, Ordering::SeqCst); - self.queue_len.fetch_add(chunks_len, Ordering::SeqCst); - } - - pub(crate) fn peek(&self) -> Option { - if self.selected.load(Ordering::SeqCst) { - if self.unordered_is_selected.load(Ordering::SeqCst) { - let unordered_queue = self.unordered_queue.read(); - return unordered_queue.front().cloned(); - } else { - let ordered_queue = self.ordered_queue.read(); - return ordered_queue.front().cloned(); - } - } - - let c = { - let unordered_queue = self.unordered_queue.read(); - unordered_queue.front().cloned() - }; - - if c.is_some() { - return c; - } - - let ordered_queue = self.ordered_queue.read(); - ordered_queue.front().cloned() - } - - pub(crate) fn pop( - &self, - beginning_fragment: bool, - unordered: bool, - ) -> Option { - let popped = if self.selected.load(Ordering::SeqCst) { - let popped = if self.unordered_is_selected.load(Ordering::SeqCst) { - let mut unordered_queue = self.unordered_queue.write(); - unordered_queue.pop_front() - } else { - let mut ordered_queue = self.ordered_queue.write(); - ordered_queue.pop_front() - }; - if let Some(p) = &popped { - if p.ending_fragment { - self.selected.store(false, Ordering::SeqCst); - } - } - popped - } else { - if !beginning_fragment { - return None; - } - if unordered { - let popped = { - let mut unordered_queue = self.unordered_queue.write(); - unordered_queue.pop_front() - }; - if let Some(p) = &popped { - if !p.ending_fragment { - self.selected.store(true, Ordering::SeqCst); - self.unordered_is_selected.store(true, Ordering::SeqCst); - } - } - popped - } else { - let popped = { - let mut ordered_queue = self.ordered_queue.write(); - ordered_queue.pop_front() - }; - if let Some(p) = &popped { - if !p.ending_fragment { - self.selected.store(true, Ordering::SeqCst); - self.unordered_is_selected.store(false, Ordering::SeqCst); - } - } - popped - } - }; - - if let Some(p) = &popped { - let user_data_len = p.user_data.len(); - self.n_bytes.fetch_sub(user_data_len, Ordering::SeqCst); - self.queue_len.fetch_sub(1, Ordering::SeqCst); - self.semaphore.add_permits(user_data_len); - } - - popped - } - - pub(crate) fn get_num_bytes(&self) -> usize { - self.n_bytes.load(Ordering::SeqCst) - } - - pub(crate) fn len(&self) -> usize { - self.queue_len.load(Ordering::SeqCst) - } - - pub(crate) fn is_empty(&self) -> bool { - self.len() == 0 - } -} diff --git a/sctp/src/queue/queue_test.rs b/sctp/src/queue/queue_test.rs deleted file mode 100644 index 4ba1a92c6..000000000 --- a/sctp/src/queue/queue_test.rs +++ /dev/null @@ -1,997 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -/////////////////////////////////////////////////////////////////// -//payload_queue_test -/////////////////////////////////////////////////////////////////// -use super::payload_queue::*; -use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; -use crate::chunk::chunk_selective_ack::GapAckBlock; -use crate::error::{Error, Result}; - -fn make_payload(tsn: u32, n_bytes: usize) -> ChunkPayloadData { - ChunkPayloadData { - tsn, - user_data: { - let mut b = BytesMut::new(); - b.resize(n_bytes, 0); - b.freeze() - }, - ..Default::default() - } -} - -#[test] -fn test_payload_queue_push_no_check() -> Result<()> { - let mut pq = PayloadQueue::new(Arc::new(AtomicUsize::new(0))); - - pq.push_no_check(make_payload(0, 10)); - assert_eq!(pq.get_num_bytes(), 10, "total bytes mismatch"); - assert_eq!(pq.len(), 1, "item count mismatch"); - pq.push_no_check(make_payload(1, 11)); - assert_eq!(pq.get_num_bytes(), 21, "total bytes mismatch"); - assert_eq!(pq.len(), 2, "item count mismatch"); - pq.push_no_check(make_payload(2, 12)); - assert_eq!(pq.get_num_bytes(), 33, "total bytes mismatch"); - assert_eq!(pq.len(), 3, "item count mismatch"); - - for i in 0..3 { - assert!(!pq.sorted.is_empty(), "should not be empty"); - let c = pq.pop(i); - assert!(c.is_some(), "pop should succeed"); - if let Some(c) = c { - assert_eq!(c.tsn, i, "TSN should match"); - } - } - - assert_eq!(pq.get_num_bytes(), 0, "total bytes mismatch"); - assert_eq!(pq.len(), 0, "item count mismatch"); - - assert!(pq.sorted.is_empty(), "should be empty"); - pq.push_no_check(make_payload(3, 13)); - assert_eq!(pq.get_num_bytes(), 13, "total bytes mismatch"); - pq.push_no_check(make_payload(4, 14)); - assert_eq!(pq.get_num_bytes(), 27, "total bytes mismatch"); - - for i in 3..5 { - assert!(!pq.sorted.is_empty(), "should not be empty"); - let c = pq.pop(i); - assert!(c.is_some(), "pop should succeed"); - if let Some(c) = c { - assert_eq!(c.tsn, i, "TSN should match"); - } - } - - assert_eq!(pq.get_num_bytes(), 0, "total bytes mismatch"); - assert_eq!(pq.len(), 0, "item count mismatch"); - - Ok(()) -} - -#[test] -fn test_payload_queue_get_gap_ack_block() -> Result<()> { - let mut pq = PayloadQueue::new(Arc::new(AtomicUsize::new(0))); - - pq.push(make_payload(1, 0), 0); - pq.push(make_payload(2, 0), 0); - pq.push(make_payload(3, 0), 0); - pq.push(make_payload(4, 0), 0); - pq.push(make_payload(5, 0), 0); - pq.push(make_payload(6, 0), 0); - - let gab1 = [GapAckBlock { start: 1, end: 6 }]; - let gab2 = pq.get_gap_ack_blocks(0); - assert!(!gab2.is_empty()); - assert_eq!(gab2.len(), 1); - - assert_eq!(gab2[0].start, gab1[0].start); - assert_eq!(gab2[0].end, gab1[0].end); - - pq.push(make_payload(8, 0), 0); - pq.push(make_payload(9, 0), 0); - - let gab1 = [ - GapAckBlock { start: 1, end: 6 }, - GapAckBlock { start: 8, end: 9 }, - ]; - let gab2 = pq.get_gap_ack_blocks(0); - assert!(!gab2.is_empty()); - assert_eq!(gab2.len(), 2); - - assert_eq!(gab2[0].start, gab1[0].start); - assert_eq!(gab2[0].end, gab1[0].end); - assert_eq!(gab2[1].start, gab1[1].start); - assert_eq!(gab2[1].end, gab1[1].end); - - Ok(()) -} - -#[test] -fn test_payload_queue_get_last_tsn_received() -> Result<()> { - let mut pq = PayloadQueue::new(Arc::new(AtomicUsize::new(0))); - - // empty queie should return false - let ok = pq.get_last_tsn_received(); - assert!(ok.is_none(), "should be none"); - - let ok = pq.push(make_payload(20, 0), 0); - assert!(ok, "should be true"); - let tsn = pq.get_last_tsn_received(); - assert!(tsn.is_some(), "should be false"); - assert_eq!(tsn, Some(&20), "should match"); - - // append should work - let ok = pq.push(make_payload(21, 0), 0); - assert!(ok, "should be true"); - let tsn = pq.get_last_tsn_received(); - assert!(tsn.is_some(), "should be false"); - assert_eq!(tsn, Some(&21), "should match"); - - // check if sorting applied - let ok = pq.push(make_payload(19, 0), 0); - assert!(ok, "should be true"); - let tsn = pq.get_last_tsn_received(); - assert!(tsn.is_some(), "should be false"); - assert_eq!(tsn, Some(&21), "should match"); - - Ok(()) -} - -#[test] -fn test_payload_queue_mark_all_to_retrasmit() -> Result<()> { - let mut pq = PayloadQueue::new(Arc::new(AtomicUsize::new(0))); - - for i in 0..3 { - pq.push(make_payload(i + 1, 10), 0); - } - pq.mark_as_acked(2); - pq.mark_all_to_retrasmit(); - - let c = pq.get(1); - assert!(c.is_some(), "should be true"); - assert!(c.unwrap().retransmit, "should be marked as retransmit"); - let c = pq.get(2); - assert!(c.is_some(), "should be true"); - assert!(!c.unwrap().retransmit, "should NOT be marked as retransmit"); - let c = pq.get(3); - assert!(c.is_some(), "should be true"); - assert!(c.unwrap().retransmit, "should be marked as retransmit"); - - Ok(()) -} - -#[test] -fn test_payload_queue_reset_retransmit_flag_on_ack() -> Result<()> { - let mut pq = PayloadQueue::new(Arc::new(AtomicUsize::new(0))); - - for i in 0..4 { - pq.push(make_payload(i + 1, 10), 0); - } - - pq.mark_all_to_retrasmit(); - pq.mark_as_acked(2); // should cancel retransmission for TSN 2 - pq.mark_as_acked(4); // should cancel retransmission for TSN 4 - - let c = pq.get(1); - assert!(c.is_some(), "should be true"); - assert!(c.unwrap().retransmit, "should be marked as retransmit"); - let c = pq.get(2); - assert!(c.is_some(), "should be true"); - assert!(!c.unwrap().retransmit, "should NOT be marked as retransmit"); - let c = pq.get(3); - assert!(c.is_some(), "should be true"); - assert!(c.unwrap().retransmit, "should be marked as retransmit"); - let c = pq.get(4); - assert!(c.is_some(), "should be true"); - assert!(!c.unwrap().retransmit, "should NOT be marked as retransmit"); - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//pending_queue_test -/////////////////////////////////////////////////////////////////// -use super::pending_queue::*; - -const NO_FRAGMENT: usize = 0; -const FRAG_BEGIN: usize = 1; -const FRAG_MIDDLE: usize = 2; -const FRAG_END: usize = 3; - -fn make_data_chunk(tsn: u32, unordered: bool, frag: usize) -> ChunkPayloadData { - let mut b = false; - let mut e = false; - - match frag { - NO_FRAGMENT => { - b = true; - e = true; - } - FRAG_BEGIN => { - b = true; - } - FRAG_END => e = true, - _ => {} - }; - - ChunkPayloadData { - tsn, - unordered, - beginning_fragment: b, - ending_fragment: e, - user_data: { - let mut b = BytesMut::new(); - b.resize(10, 0); // always 10 bytes - b.freeze() - }, - ..Default::default() - } -} - -#[test] -fn test_pending_base_queue_push_and_pop() -> Result<()> { - let mut pq = PendingBaseQueue::new(); - pq.push_back(make_data_chunk(0, false, NO_FRAGMENT)); - pq.push_back(make_data_chunk(1, false, NO_FRAGMENT)); - pq.push_back(make_data_chunk(2, false, NO_FRAGMENT)); - - for i in 0..3 { - let c = pq.get(i); - assert!(c.is_some(), "should not be none"); - assert_eq!(c.unwrap().tsn, i as u32, "TSN should match"); - } - - for i in 0..3 { - let c = pq.pop_front(); - assert!(c.is_some(), "should not be none"); - assert_eq!(c.unwrap().tsn, i, "TSN should match"); - } - - pq.push_back(make_data_chunk(3, false, NO_FRAGMENT)); - pq.push_back(make_data_chunk(4, false, NO_FRAGMENT)); - - for i in 3..5 { - let c = pq.pop_front(); - assert!(c.is_some(), "should not be none"); - assert_eq!(c.unwrap().tsn, i, "TSN should match"); - } - Ok(()) -} - -#[test] -fn test_pending_base_queue_out_of_bounce() -> Result<()> { - let mut pq = PendingBaseQueue::new(); - assert!(pq.pop_front().is_none(), "should be none"); - assert!(pq.front().is_none(), "should be none"); - - pq.push_back(make_data_chunk(0, false, NO_FRAGMENT)); - assert!(pq.get(1).is_none(), "should be none"); - - Ok(()) -} - -// NOTE: TSN is not used in pendingQueue in the actual usage. -// Following tests use TSN field as a chunk ID. -#[tokio::test] -async fn test_pending_queue_push_and_pop() -> Result<()> { - let pq = PendingQueue::new(); - pq.push(make_data_chunk(0, false, NO_FRAGMENT)).await; - assert_eq!(pq.get_num_bytes(), 10, "total bytes mismatch"); - pq.push(make_data_chunk(1, false, NO_FRAGMENT)).await; - assert_eq!(pq.get_num_bytes(), 20, "total bytes mismatch"); - pq.push(make_data_chunk(2, false, NO_FRAGMENT)).await; - assert_eq!(pq.get_num_bytes(), 30, "total bytes mismatch"); - - for i in 0..3 { - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, i, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error: {i}"); - } - - assert_eq!(pq.get_num_bytes(), 0, "total bytes mismatch"); - - pq.push(make_data_chunk(3, false, NO_FRAGMENT)).await; - assert_eq!(pq.get_num_bytes(), 10, "total bytes mismatch"); - pq.push(make_data_chunk(4, false, NO_FRAGMENT)).await; - assert_eq!(pq.get_num_bytes(), 20, "total bytes mismatch"); - - for i in 3..5 { - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, i, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error: {i}"); - } - - assert_eq!(pq.get_num_bytes(), 0, "total bytes mismatch"); - - Ok(()) -} - -#[tokio::test] -async fn test_pending_queue_unordered_wins() -> Result<()> { - let pq = PendingQueue::new(); - - pq.push(make_data_chunk(0, false, NO_FRAGMENT)).await; - assert_eq!(10, pq.get_num_bytes(), "total bytes mismatch"); - pq.push(make_data_chunk(1, true, NO_FRAGMENT)).await; - assert_eq!(20, pq.get_num_bytes(), "total bytes mismatch"); - pq.push(make_data_chunk(2, false, NO_FRAGMENT)).await; - assert_eq!(30, pq.get_num_bytes(), "total bytes mismatch"); - pq.push(make_data_chunk(3, true, NO_FRAGMENT)).await; - assert_eq!(40, pq.get_num_bytes(), "total bytes mismatch"); - - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, 1, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error"); - - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, 3, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error"); - - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, 0, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error"); - - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, 2, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error"); - - assert_eq!(pq.get_num_bytes(), 0, "total bytes mismatch"); - - Ok(()) -} - -#[tokio::test] -async fn test_pending_queue_fragments() -> Result<()> { - let pq = PendingQueue::new(); - pq.push(make_data_chunk(0, false, FRAG_BEGIN)).await; - pq.push(make_data_chunk(1, false, FRAG_MIDDLE)).await; - pq.push(make_data_chunk(2, false, FRAG_END)).await; - pq.push(make_data_chunk(3, true, FRAG_BEGIN)).await; - pq.push(make_data_chunk(4, true, FRAG_MIDDLE)).await; - pq.push(make_data_chunk(5, true, FRAG_END)).await; - - let expects = vec![3, 4, 5, 0, 1, 2]; - - for exp in expects { - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, exp, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error: {exp}"); - } - - Ok(()) -} - -// Once decided ordered or unordered, the decision should persist until -// it pops a chunk with ending_fragment flags set to true. -#[tokio::test] -async fn test_pending_queue_selection_persistence() -> Result<()> { - let pq = PendingQueue::new(); - pq.push(make_data_chunk(0, false, FRAG_BEGIN)).await; - - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, 0, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error: {}", 0); - - pq.push(make_data_chunk(1, true, NO_FRAGMENT)).await; - pq.push(make_data_chunk(2, false, FRAG_MIDDLE)).await; - pq.push(make_data_chunk(3, false, FRAG_END)).await; - - let expects = vec![2, 3, 1]; - - for exp in expects { - let c = pq.peek(); - assert!(c.is_some(), "peek error"); - let c = c.unwrap(); - assert_eq!(c.tsn, exp, "TSN should match"); - let (beginning_fragment, unordered) = (c.beginning_fragment, c.unordered); - let result = pq.pop(beginning_fragment, unordered); - assert!(result.is_some(), "should not error: {exp}"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_pending_queue_append() -> Result<()> { - let pq = PendingQueue::new(); - pq.append(vec![ - make_data_chunk(0, false, NO_FRAGMENT), - make_data_chunk(1, false, NO_FRAGMENT), - make_data_chunk(3, false, NO_FRAGMENT), - ]) - .await; - assert_eq!(pq.get_num_bytes(), 30, "total bytes mismatch"); - assert_eq!(pq.len(), 3, "len mismatch"); - - Ok(()) -} - -/////////////////////////////////////////////////////////////////// -//reassembly_queue_test -/////////////////////////////////////////////////////////////////// -use std::sync::Arc; - -use portable_atomic::AtomicUsize; - -use super::reassembly_queue::*; - -#[test] -fn test_reassembly_queue_ordered_fragments() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - tsn: 1, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - ending_fragment: true, - tsn: 2, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"DEFG"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(7, rq.get_num_bytes(), "num bytes mismatch"); - - let mut buf = vec![0u8; 16]; - - let (n, ppi) = rq.read(&mut buf)?; - assert_eq!(n, 7, "should received 7 bytes"); - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - assert_eq!(ppi, org_ppi, "should have valid ppi"); - assert_eq!(&buf[..n], b"ABCDEFG", "data should match"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_unordered_fragments() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - beginning_fragment: true, - tsn: 1, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - tsn: 2, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"DEFG"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 7, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - ending_fragment: true, - tsn: 3, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"H"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(rq.get_num_bytes(), 8, "num bytes mismatch"); - - let mut buf = vec![0u8; 16]; - - let (n, ppi) = rq.read(&mut buf)?; - assert_eq!(n, 8, "should received 8 bytes"); - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - assert_eq!(ppi, org_ppi, "should have valid ppi"); - assert_eq!(&buf[..n], b"ABCDEFGH", "data should match"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_ordered_and_unordered_fragments() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - let org_ppi = PayloadProtocolIdentifier::Binary; - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - ending_fragment: true, - tsn: 1, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - beginning_fragment: true, - ending_fragment: true, - tsn: 2, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"DEF"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(rq.get_num_bytes(), 6, "num bytes mismatch"); - - // - // Now we have two complete chunks ready to read in the reassemblyQueue. - // - - let mut buf = vec![0u8; 16]; - - // Should read unordered chunks first - let (n, ppi) = rq.read(&mut buf)?; - assert_eq!(n, 3, "should received 3 bytes"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - assert_eq!(ppi, org_ppi, "should have valid ppi"); - assert_eq!(&buf[..n], b"DEF", "data should match"); - - // Next should read ordered chunks - let (n, ppi) = rq.read(&mut buf)?; - assert_eq!(n, 3, "should received 3 bytes"); - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - assert_eq!(ppi, org_ppi, "should have valid ppi"); - assert_eq!(&buf[..n], b"ABC", "data should match"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_unordered_complete_skips_incomplete() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - beginning_fragment: true, - tsn: 10, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"IN"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(2, rq.get_num_bytes(), "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - ending_fragment: true, - tsn: 12, // <- incongiguous - stream_sequence_number: 1, - user_data: Bytes::from_static(b"COMPLETE"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 10, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - beginning_fragment: true, - ending_fragment: true, - tsn: 13, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"GOOD"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(rq.get_num_bytes(), 14, "num bytes mismatch"); - - // - // Now we have two complete chunks ready to read in the reassemblyQueue. - // - - let mut buf = vec![0u8; 16]; - - // Should pick the one that has "GOOD" - let (n, ppi) = rq.read(&mut buf)?; - assert_eq!(n, 4, "should receive 4 bytes"); - assert_eq!(rq.get_num_bytes(), 10, "num bytes mismatch"); - assert_eq!(ppi, org_ppi, "should have valid ppi"); - assert_eq!(&buf[..n], b"GOOD", "data should match"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_ignores_chunk_with_wrong_si() -> Result<()> { - let mut rq = ReassemblyQueue::new(123); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - stream_identifier: 124, - beginning_fragment: true, - ending_fragment: true, - tsn: 10, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"IN"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk should be ignored"); - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - Ok(()) -} - -#[test] -fn test_reassembly_queue_ignores_chunk_with_stale_ssn() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - rq.next_ssn = 7; // forcibly set expected SSN to 7 - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - ending_fragment: true, - tsn: 10, - stream_sequence_number: 6, // <-- stale - user_data: Bytes::from_static(b"IN"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk should not be ignored"); - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_should_fail_to_read_incomplete_chunk() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - tsn: 123, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"IN"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "the set should not be complete"); - assert_eq!(rq.get_num_bytes(), 2, "num bytes mismatch"); - - let mut buf = vec![0u8; 16]; - let result = rq.read(&mut buf); - assert!(result.is_err(), "read() should not succeed"); - assert_eq!(rq.get_num_bytes(), 2, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_should_fail_to_read_if_the_nex_ssn_is_not_ready() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - ending_fragment: true, - tsn: 123, - stream_sequence_number: 1, - user_data: Bytes::from_static(b"IN"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "the set should be complete"); - assert_eq!(rq.get_num_bytes(), 2, "num bytes mismatch"); - - let mut buf = vec![0u8; 16]; - let result = rq.read(&mut buf); - assert!(result.is_err(), "read() should not succeed"); - assert_eq!(rq.get_num_bytes(), 2, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_detect_buffer_too_short() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - ending_fragment: true, - tsn: 123, - stream_sequence_number: 0, - user_data: Bytes::from_static(b"0123456789"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "the set should be complete"); - assert_eq!(rq.get_num_bytes(), 10, "num bytes mismatch"); - - let mut buf = vec![0u8; 8]; // <- passing buffer too short - let result = rq.read(&mut buf); - assert!(result.is_err(), "read() should not succeed"); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrShortBuffer { size: 8 }, - "read() should not succeed" - ); - } - assert_eq!(rq.get_num_bytes(), 0, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_forward_tsn_for_ordered_fragments() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let ssn_complete = 5u16; - let ssn_dropped = 6u16; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - ending_fragment: true, - tsn: 10, - stream_sequence_number: ssn_complete, - user_data: Bytes::from_static(b"123"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(complete, "chunk set should be complete"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - beginning_fragment: true, - tsn: 11, - stream_sequence_number: ssn_dropped, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 6, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - tsn: 12, - stream_sequence_number: ssn_dropped, - user_data: Bytes::from_static(b"DEF"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 9, "num bytes mismatch"); - - rq.forward_tsn_for_ordered(ssn_dropped); - - assert_eq!(rq.ordered.len(), 1, "there should be one chunk left"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_reassembly_queue_forward_tsn_for_unordered_fragments() -> Result<()> { - let mut rq = ReassemblyQueue::new(0); - - let org_ppi = PayloadProtocolIdentifier::Binary; - - let ssn_dropped = 6u16; - let ssn_kept = 7u16; - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - beginning_fragment: true, - tsn: 11, - stream_sequence_number: ssn_dropped, - user_data: Bytes::from_static(b"ABC"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - tsn: 12, - stream_sequence_number: ssn_dropped, - user_data: Bytes::from_static(b"DEF"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 6, "num bytes mismatch"); - - let chunk = ChunkPayloadData { - payload_type: org_ppi, - unordered: true, - tsn: 14, - beginning_fragment: true, - stream_sequence_number: ssn_kept, - user_data: Bytes::from_static(b"SOS"), - ..Default::default() - }; - - let complete = rq.push(chunk); - assert!(!complete, "chunk set should not be complete yet"); - assert_eq!(rq.get_num_bytes(), 9, "num bytes mismatch"); - - // At this point, there are 3 chunks in the rq.unorderedChunks. - // This call should remove chunks with tsn equals to 13 or older. - rq.forward_tsn_for_unordered(13); - - // As a result, there should be one chunk (tsn=14) - assert_eq!( - rq.unordered_chunks.len(), - 1, - "there should be one chunk kept" - ); - assert_eq!(rq.get_num_bytes(), 3, "num bytes mismatch"); - - Ok(()) -} - -#[test] -fn test_chunk_set_empty_chunk_set() -> Result<()> { - let cset = ChunkSet::new(0, PayloadProtocolIdentifier::default()); - assert!(!cset.is_complete(), "empty chunkSet cannot be complete"); - Ok(()) -} - -#[test] -fn test_chunk_set_push_dup_chunks_to_chunk_set() -> Result<()> { - let mut cset = ChunkSet::new(0, PayloadProtocolIdentifier::default()); - cset.push(ChunkPayloadData { - tsn: 100, - beginning_fragment: true, - ..Default::default() - }); - let complete = cset.push(ChunkPayloadData { - tsn: 100, - ending_fragment: true, - ..Default::default() - }); - assert!(!complete, "chunk with dup TSN is not complete"); - assert_eq!(cset.chunks.len(), 1, "chunk with dup TSN should be ignored"); - Ok(()) -} - -#[test] -fn test_chunk_set_incomplete_chunk_set_no_beginning() -> Result<()> { - let cset = ChunkSet { - ssn: 0, - ppi: PayloadProtocolIdentifier::default(), - chunks: vec![], - }; - assert!( - !cset.is_complete(), - "chunkSet not starting with B=1 cannot be complete" - ); - Ok(()) -} - -#[test] -fn test_chunk_set_incomplete_chunk_set_no_contiguous_tsn() -> Result<()> { - let cset = ChunkSet { - ssn: 0, - ppi: PayloadProtocolIdentifier::default(), - chunks: vec![ - ChunkPayloadData { - tsn: 100, - beginning_fragment: true, - ..Default::default() - }, - ChunkPayloadData { - tsn: 101, - ..Default::default() - }, - ChunkPayloadData { - tsn: 103, - ending_fragment: true, - ..Default::default() - }, - ], - }; - assert!( - !cset.is_complete(), - "chunkSet not starting with incontiguous tsn cannot be complete" - ); - Ok(()) -} diff --git a/sctp/src/queue/reassembly_queue.rs b/sctp/src/queue/reassembly_queue.rs deleted file mode 100644 index 3aafecd23..000000000 --- a/sctp/src/queue/reassembly_queue.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::cmp::Ordering; - -use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; -use crate::error::{Error, Result}; -use crate::util::*; - -fn sort_chunks_by_tsn(c: &mut [ChunkPayloadData]) { - c.sort_by(|a, b| { - if sna32lt(a.tsn, b.tsn) { - Ordering::Less - } else { - Ordering::Greater - } - }); -} - -fn sort_chunks_by_ssn(c: &mut [ChunkSet]) { - c.sort_by(|a, b| { - if sna16lt(a.ssn, b.ssn) { - Ordering::Less - } else { - Ordering::Greater - } - }); -} - -/// chunkSet is a set of chunks that share the same SSN -#[derive(Debug, Clone)] -pub(crate) struct ChunkSet { - /// used only with the ordered chunks - pub(crate) ssn: u16, - pub(crate) ppi: PayloadProtocolIdentifier, - pub(crate) chunks: Vec, -} - -impl ChunkSet { - pub(crate) fn new(ssn: u16, ppi: PayloadProtocolIdentifier) -> Self { - ChunkSet { - ssn, - ppi, - chunks: vec![], - } - } - - pub(crate) fn push(&mut self, chunk: ChunkPayloadData) -> bool { - // check if dup - for c in &self.chunks { - if c.tsn == chunk.tsn { - return false; - } - } - - // append and sort - self.chunks.push(chunk); - sort_chunks_by_tsn(&mut self.chunks); - - // Check if we now have a complete set - self.is_complete() - } - - pub(crate) fn is_complete(&self) -> bool { - // Condition for complete set - // 0. Has at least one chunk. - // 1. Begins with beginningFragment set to true - // 2. Ends with endingFragment set to true - // 3. TSN monotinically increase by 1 from beginning to end - - // 0. - let n_chunks = self.chunks.len(); - if n_chunks == 0 { - return false; - } - - // 1. - if !self.chunks[0].beginning_fragment { - return false; - } - - // 2. - if !self.chunks[n_chunks - 1].ending_fragment { - return false; - } - - // 3. - let mut last_tsn = 0u32; - for (i, c) in self.chunks.iter().enumerate() { - if i > 0 { - // Fragments must have contiguous TSN - // From RFC 4960 Section 3.3.1: - // When a user message is fragmented into multiple chunks, the TSNs are - // used by the receiver to reassemble the message. This means that the - // TSNs for each fragment of a fragmented user message MUST be strictly - // sequential. - if c.tsn != last_tsn + 1 { - // mid or end fragment is missing - return false; - } - } - - last_tsn = c.tsn; - } - - true - } -} - -#[derive(Default, Debug)] -pub(crate) struct ReassemblyQueue { - pub(crate) si: u16, - pub(crate) next_ssn: u16, - /// expected SSN for next ordered chunk - pub(crate) ordered: Vec, - pub(crate) unordered: Vec, - pub(crate) unordered_chunks: Vec, - pub(crate) n_bytes: usize, -} - -impl ReassemblyQueue { - /// From RFC 4960 Sec 6.5: - /// The Stream Sequence Number in all the streams MUST start from 0 when - /// the association is Established. Also, when the Stream Sequence - /// Number reaches the value 65535 the next Stream Sequence Number MUST - /// be set to 0. - pub(crate) fn new(si: u16) -> Self { - ReassemblyQueue { - si, - next_ssn: 0, // From RFC 4960 Sec 6.5: - ordered: vec![], - unordered: vec![], - unordered_chunks: vec![], - n_bytes: 0, - } - } - - pub(crate) fn push(&mut self, chunk: ChunkPayloadData) -> bool { - if chunk.stream_identifier != self.si { - return false; - } - - if chunk.unordered { - // First, insert into unordered_chunks array - //atomic.AddUint64(&r.n_bytes, uint64(len(chunk.userData))) - self.n_bytes += chunk.user_data.len(); - self.unordered_chunks.push(chunk); - sort_chunks_by_tsn(&mut self.unordered_chunks); - - // Scan unordered_chunks that are contiguous (in TSN) - // If found, append the complete set to the unordered array - if let Some(cset) = self.find_complete_unordered_chunk_set() { - self.unordered.push(cset); - return true; - } - - false - } else { - // This is an ordered chunk - if sna16lt(chunk.stream_sequence_number, self.next_ssn) { - return false; - } - - self.n_bytes += chunk.user_data.len(); - - // Check if a chunkSet with the SSN already exists - for s in &mut self.ordered { - if s.ssn == chunk.stream_sequence_number { - return s.push(chunk); - } - } - - // If not found, create a new chunkSet - let mut cset = ChunkSet::new(chunk.stream_sequence_number, chunk.payload_type); - let unordered = chunk.unordered; - let ok = cset.push(chunk); - self.ordered.push(cset); - if !unordered { - sort_chunks_by_ssn(&mut self.ordered); - } - - ok - } - } - - pub(crate) fn find_complete_unordered_chunk_set(&mut self) -> Option { - let mut start_idx = -1isize; - let mut n_chunks = 0usize; - let mut last_tsn = 0u32; - let mut found = false; - - for (i, c) in self.unordered_chunks.iter().enumerate() { - // seek beigining - if c.beginning_fragment { - start_idx = i as isize; - n_chunks = 1; - last_tsn = c.tsn; - - if c.ending_fragment { - found = true; - break; - } - continue; - } - - if start_idx < 0 { - continue; - } - - // Check if contiguous in TSN - if c.tsn != last_tsn + 1 { - start_idx = -1; - continue; - } - - last_tsn = c.tsn; - n_chunks += 1; - - if c.ending_fragment { - found = true; - break; - } - } - - if !found { - return None; - } - - // Extract the range of chunks - let chunks: Vec = self - .unordered_chunks - .drain(start_idx as usize..(start_idx as usize) + n_chunks) - .collect(); - - let mut chunk_set = ChunkSet::new(0, chunks[0].payload_type); - chunk_set.chunks = chunks; - - Some(chunk_set) - } - - pub(crate) fn is_readable(&self) -> bool { - // Check unordered first - if !self.unordered.is_empty() { - // The chunk sets in r.unordered should all be complete. - return true; - } - - // Check ordered sets - if !self.ordered.is_empty() { - let cset = &self.ordered[0]; - if cset.is_complete() && sna16lte(cset.ssn, self.next_ssn) { - return true; - } - } - false - } - - pub(crate) fn read(&mut self, buf: &mut [u8]) -> Result<(usize, PayloadProtocolIdentifier)> { - // Check unordered first - let cset = if !self.unordered.is_empty() { - self.unordered.remove(0) - } else if !self.ordered.is_empty() { - // Now, check ordered - let cset = &self.ordered[0]; - if !cset.is_complete() { - return Err(Error::ErrTryAgain); - } - if sna16gt(cset.ssn, self.next_ssn) { - return Err(Error::ErrTryAgain); - } - if cset.ssn == self.next_ssn { - // From RFC 4960 Sec 6.5: - self.next_ssn = self.next_ssn.wrapping_add(1); - } - self.ordered.remove(0) - } else { - return Err(Error::ErrTryAgain); - }; - - // Concat all fragments into the buffer - let mut n_written = 0; - let mut err = None; - for c in &cset.chunks { - let to_copy = c.user_data.len(); - self.subtract_num_bytes(to_copy); - if err.is_none() { - let n = std::cmp::min(to_copy, buf.len() - n_written); - buf[n_written..n_written + n].copy_from_slice(&c.user_data[..n]); - n_written += n; - if n < to_copy { - err = Some(Error::ErrShortBuffer { size: buf.len() }); - } - } - } - - if let Some(err) = err { - Err(err) - } else { - Ok((n_written, cset.ppi)) - } - } - - /// Use last_ssn to locate a chunkSet then remove it if the set has - /// not been complete - pub(crate) fn forward_tsn_for_ordered(&mut self, last_ssn: u16) { - let num_bytes = self - .ordered - .iter() - .filter(|s| sna16lte(s.ssn, last_ssn) && !s.is_complete()) - .fold(0, |n, s| { - n + s.chunks.iter().fold(0, |acc, c| acc + c.user_data.len()) - }); - self.subtract_num_bytes(num_bytes); - - self.ordered - .retain(|s| !sna16lte(s.ssn, last_ssn) || s.is_complete()); - - // Finally, forward next_ssn - if sna16lte(self.next_ssn, last_ssn) { - self.next_ssn = last_ssn.wrapping_add(1); - } - } - - /// Remove all fragments in the unordered sets that contains chunks - /// equal to or older than `new_cumulative_tsn`. - /// We know all sets in the r.unordered are complete ones. - /// Just remove chunks that are equal to or older than new_cumulative_tsn - /// from the unordered_chunks - pub(crate) fn forward_tsn_for_unordered(&mut self, new_cumulative_tsn: u32) { - let mut last_idx: isize = -1; - for (i, c) in self.unordered_chunks.iter().enumerate() { - if sna32gt(c.tsn, new_cumulative_tsn) { - break; - } - last_idx = i as isize; - } - if last_idx >= 0 { - for i in 0..(last_idx + 1) as usize { - self.subtract_num_bytes(self.unordered_chunks[i].user_data.len()); - } - self.unordered_chunks.drain(..(last_idx + 1) as usize); - } - } - - pub(crate) fn subtract_num_bytes(&mut self, n_bytes: usize) { - if self.n_bytes >= n_bytes { - self.n_bytes -= n_bytes; - } else { - self.n_bytes = 0; - } - } - - pub(crate) fn get_num_bytes(&self) -> usize { - self.n_bytes - } -} diff --git a/sctp/src/stream/mod.rs b/sctp/src/stream/mod.rs deleted file mode 100644 index 1ae1980c1..000000000 --- a/sctp/src/stream/mod.rs +++ /dev/null @@ -1,852 +0,0 @@ -#[cfg(test)] -mod stream_test; - -use std::future::Future; -use std::net::Shutdown; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::{fmt, io}; - -use arc_swap::ArcSwapOption; -use bytes::Bytes; -use portable_atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU8, AtomicUsize}; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tokio::sync::{mpsc, Mutex, Notify}; - -use crate::association::AssociationState; -use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; -use crate::error::{Error, Result}; -use crate::queue::pending_queue::PendingQueue; -use crate::queue::reassembly_queue::ReassemblyQueue; - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[repr(C)] -pub enum ReliabilityType { - /// ReliabilityTypeReliable is used for reliable transmission - #[default] - Reliable = 0, - /// ReliabilityTypeRexmit is used for partial reliability by retransmission count - Rexmit = 1, - /// ReliabilityTypeTimed is used for partial reliability by retransmission duration - Timed = 2, -} - -impl fmt::Display for ReliabilityType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - ReliabilityType::Reliable => "Reliable", - ReliabilityType::Rexmit => "Rexmit", - ReliabilityType::Timed => "Timed", - }; - write!(f, "{s}") - } -} - -impl From for ReliabilityType { - fn from(v: u8) -> ReliabilityType { - match v { - 1 => ReliabilityType::Rexmit, - 2 => ReliabilityType::Timed, - _ => ReliabilityType::Reliable, - } - } -} - -pub type OnBufferedAmountLowFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -// TODO: benchmark performance between multiple Atomic+Mutex vs one Mutex - -/// Stream represents an SCTP stream -#[derive(Default)] -pub struct Stream { - pub(crate) max_payload_size: u32, - pub(crate) max_message_size: Arc, // clone from association - pub(crate) state: Arc, // clone from association - pub(crate) awake_write_loop_ch: Option>>, - pub(crate) pending_queue: Arc, - - pub(crate) stream_identifier: u16, - pub(crate) default_payload_type: AtomicU32, //PayloadProtocolIdentifier, - pub(crate) reassembly_queue: Mutex, - pub(crate) sequence_number: AtomicU16, - pub(crate) read_notifier: Notify, - pub(crate) read_shutdown: AtomicBool, - pub(crate) write_shutdown: AtomicBool, - pub(crate) unordered: AtomicBool, - pub(crate) reliability_type: AtomicU8, //ReliabilityType, - pub(crate) reliability_value: AtomicU32, - pub(crate) buffered_amount: AtomicUsize, - pub(crate) buffered_amount_low: AtomicUsize, - pub(crate) on_buffered_amount_low: ArcSwapOption>, - pub(crate) name: String, -} - -impl fmt::Debug for Stream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Stream") - .field("max_payload_size", &self.max_payload_size) - .field("max_message_size", &self.max_message_size) - .field("state", &self.state) - .field("awake_write_loop_ch", &self.awake_write_loop_ch) - .field("stream_identifier", &self.stream_identifier) - .field("default_payload_type", &self.default_payload_type) - .field("reassembly_queue", &self.reassembly_queue) - .field("sequence_number", &self.sequence_number) - .field("read_shutdown", &self.read_shutdown) - .field("write_shutdown", &self.write_shutdown) - .field("unordered", &self.unordered) - .field("reliability_type", &self.reliability_type) - .field("reliability_value", &self.reliability_value) - .field("buffered_amount", &self.buffered_amount) - .field("buffered_amount_low", &self.buffered_amount_low) - .field("name", &self.name) - .finish() - } -} - -impl Stream { - pub(crate) fn new( - name: String, - stream_identifier: u16, - max_payload_size: u32, - max_message_size: Arc, - state: Arc, - awake_write_loop_ch: Option>>, - pending_queue: Arc, - ) -> Self { - Stream { - max_payload_size, - max_message_size, - state, - awake_write_loop_ch, - pending_queue, - - stream_identifier, - default_payload_type: AtomicU32::new(0), //PayloadProtocolIdentifier::Unknown, - reassembly_queue: Mutex::new(ReassemblyQueue::new(stream_identifier)), - sequence_number: AtomicU16::new(0), - read_notifier: Notify::new(), - read_shutdown: AtomicBool::new(false), - write_shutdown: AtomicBool::new(false), - unordered: AtomicBool::new(false), - reliability_type: AtomicU8::new(0), //ReliabilityType::Reliable, - reliability_value: AtomicU32::new(0), - buffered_amount: AtomicUsize::new(0), - buffered_amount_low: AtomicUsize::new(0), - on_buffered_amount_low: ArcSwapOption::empty(), - name, - } - } - - /// stream_identifier returns the Stream identifier associated to the stream. - pub fn stream_identifier(&self) -> u16 { - self.stream_identifier - } - - /// set_default_payload_type sets the default payload type used by write. - pub fn set_default_payload_type(&self, default_payload_type: PayloadProtocolIdentifier) { - self.default_payload_type - .store(default_payload_type as u32, Ordering::SeqCst); - } - - /// set_reliability_params sets reliability parameters for this stream. - pub fn set_reliability_params(&self, unordered: bool, rel_type: ReliabilityType, rel_val: u32) { - log::debug!( - "[{}] reliability params: ordered={} type={} value={}", - self.name, - !unordered, - rel_type, - rel_val - ); - self.unordered.store(unordered, Ordering::SeqCst); - self.reliability_type - .store(rel_type as u8, Ordering::SeqCst); - self.reliability_value.store(rel_val, Ordering::SeqCst); - } - - /// Reads a packet of len(p) bytes, dropping the Payload Protocol Identifier. - /// - /// Returns `Error::ErrShortBuffer` if `p` is too short. - /// Returns `0` if the reading half of this stream is shutdown or it (the stream) was reset. - pub async fn read(&self, p: &mut [u8]) -> Result { - let (n, _) = self.read_sctp(p).await?; - Ok(n) - } - - /// Reads a packet of len(p) bytes and returns the associated Payload Protocol Identifier. - /// - /// Returns `Error::ErrShortBuffer` if `p` is too short. - /// Returns `(0, PayloadProtocolIdentifier::Unknown)` if the reading half of this stream is shutdown or it (the stream) was reset. - pub async fn read_sctp(&self, p: &mut [u8]) -> Result<(usize, PayloadProtocolIdentifier)> { - loop { - if self.read_shutdown.load(Ordering::SeqCst) { - return Ok((0, PayloadProtocolIdentifier::Unknown)); - } - - let result = { - let mut reassembly_queue = self.reassembly_queue.lock().await; - reassembly_queue.read(p) - }; - - match result { - Ok(_) | Err(Error::ErrShortBuffer { .. }) => return result, - Err(_) => { - // wait for the next chunk to become available - self.read_notifier.notified().await; - } - } - } - } - - pub(crate) async fn handle_data(&self, pd: ChunkPayloadData) { - let readable = { - let mut reassembly_queue = self.reassembly_queue.lock().await; - if reassembly_queue.push(pd) { - let readable = reassembly_queue.is_readable(); - log::debug!("[{}] reassemblyQueue readable={}", self.name, readable); - readable - } else { - false - } - }; - - if readable { - log::debug!("[{}] readNotifier.signal()", self.name); - self.read_notifier.notify_one(); - log::debug!("[{}] readNotifier.signal() done", self.name); - } - } - - pub(crate) async fn handle_forward_tsn_for_ordered(&self, ssn: u16) { - if self.unordered.load(Ordering::SeqCst) { - return; // unordered chunks are handled by handleForwardUnordered method - } - - // Remove all chunks older than or equal to the new TSN from - // the reassembly_queue. - let readable = { - let mut reassembly_queue = self.reassembly_queue.lock().await; - reassembly_queue.forward_tsn_for_ordered(ssn); - reassembly_queue.is_readable() - }; - - // Notify the reader asynchronously if there's a data chunk to read. - if readable { - self.read_notifier.notify_one(); - } - } - - pub(crate) async fn handle_forward_tsn_for_unordered(&self, new_cumulative_tsn: u32) { - if !self.unordered.load(Ordering::SeqCst) { - return; // ordered chunks are handled by handleForwardTSNOrdered method - } - - // Remove all chunks older than or equal to the new TSN from - // the reassembly_queue. - let readable = { - let mut reassembly_queue = self.reassembly_queue.lock().await; - reassembly_queue.forward_tsn_for_unordered(new_cumulative_tsn); - reassembly_queue.is_readable() - }; - - // Notify the reader asynchronously if there's a data chunk to read. - if readable { - self.read_notifier.notify_one(); - } - } - - /// Writes `p` to the DTLS connection with the default Payload Protocol Identifier. - /// - /// Returns an error if the write half of this stream is shutdown or `p` is too large. - pub async fn write(&self, p: &Bytes) -> Result { - self.write_sctp(p, self.default_payload_type.load(Ordering::SeqCst).into()) - .await - } - - /// Writes `p` to the DTLS connection with the given Payload Protocol Identifier. - /// - /// Returns an error if the write half of this stream is shutdown or `p` is too large. - pub async fn write_sctp(&self, p: &Bytes, ppi: PayloadProtocolIdentifier) -> Result { - let chunks = self.prepare_write(p, ppi)?; - self.send_payload_data(chunks).await?; - - Ok(p.len()) - } - - /// common stuff for write and try_write - fn prepare_write( - &self, - p: &Bytes, - ppi: PayloadProtocolIdentifier, - ) -> Result> { - if self.write_shutdown.load(Ordering::SeqCst) { - return Err(Error::ErrStreamClosed); - } - - if p.len() > self.max_message_size.load(Ordering::SeqCst) as usize { - return Err(Error::ErrOutboundPacketTooLarge); - } - - let state: AssociationState = self.state.load(Ordering::SeqCst).into(); - match state { - AssociationState::ShutdownSent - | AssociationState::ShutdownAckSent - | AssociationState::ShutdownPending - | AssociationState::ShutdownReceived => return Err(Error::ErrStreamClosed), - _ => {} - }; - - Ok(self.packetize(p, ppi)) - } - - fn packetize(&self, raw: &Bytes, ppi: PayloadProtocolIdentifier) -> Vec { - let mut i = 0; - let mut remaining = raw.len(); - - // From draft-ietf-rtcweb-data-protocol-09, section 6: - // All Data Channel Establishment Protocol messages MUST be sent using - // ordered delivery and reliable transmission. - let unordered = - ppi != PayloadProtocolIdentifier::Dcep && self.unordered.load(Ordering::SeqCst); - - let mut chunks = vec![]; - - let head_abandoned = Arc::new(AtomicBool::new(false)); - let head_all_inflight = Arc::new(AtomicBool::new(false)); - while remaining != 0 { - let fragment_size = std::cmp::min(self.max_payload_size as usize, remaining); //self.association.max_payload_size - - // Copy the userdata since we'll have to store it until acked - // and the caller may re-use the buffer in the mean time - let user_data = raw.slice(i..i + fragment_size); - - let chunk = ChunkPayloadData { - stream_identifier: self.stream_identifier, - user_data, - unordered, - beginning_fragment: i == 0, - ending_fragment: remaining - fragment_size == 0, - immediate_sack: false, - payload_type: ppi, - stream_sequence_number: self.sequence_number.load(Ordering::SeqCst), - abandoned: head_abandoned.clone(), // all fragmented chunks use the same abandoned - all_inflight: head_all_inflight.clone(), // all fragmented chunks use the same all_inflight - ..Default::default() - }; - - chunks.push(chunk); - - remaining -= fragment_size; - i += fragment_size; - } - - // RFC 4960 Sec 6.6 - // Note: When transmitting ordered and unordered data, an endpoint does - // not increment its Stream Sequence Number when transmitting a DATA - // chunk with U flag set to 1. - if !unordered { - self.sequence_number.fetch_add(1, Ordering::SeqCst); - } - - let old_value = self.buffered_amount.fetch_add(raw.len(), Ordering::SeqCst); - log::trace!("[{}] bufferedAmount = {}", self.name, old_value + raw.len()); - - chunks - } - - /// Closes both read and write halves of this stream. - /// - /// Use [`Stream::shutdown`] instead. - #[deprecated] - pub async fn close(&self) -> Result<()> { - self.shutdown(Shutdown::Both).await - } - - /// Shuts down the read, write, or both halves of this stream. - /// - /// This function will cause all pending and future I/O on the specified portions to return - /// immediately with an appropriate value (see the documentation of [`Shutdown`]). - /// - /// Resets the stream when both halves of this stream are shutdown. - pub async fn shutdown(&self, how: Shutdown) -> Result<()> { - if self.read_shutdown.load(Ordering::SeqCst) && self.write_shutdown.load(Ordering::SeqCst) { - return Ok(()); - } - - if how == Shutdown::Write || how == Shutdown::Both { - self.write_shutdown.store(true, Ordering::SeqCst); - } - - if (how == Shutdown::Read || how == Shutdown::Both) - && !self.read_shutdown.swap(true, Ordering::SeqCst) - { - self.read_notifier.notify_waiters(); - } - - if how == Shutdown::Both - || (self.read_shutdown.load(Ordering::SeqCst) - && self.write_shutdown.load(Ordering::SeqCst)) - { - // Reset the stream - // https://tools.ietf.org/html/rfc6525 - self.send_reset_request(self.stream_identifier).await?; - } - - Ok(()) - } - - /// buffered_amount returns the number of bytes of data currently queued to be sent over this stream. - pub fn buffered_amount(&self) -> usize { - self.buffered_amount.load(Ordering::SeqCst) - } - - /// buffered_amount_low_threshold returns the number of bytes of buffered outgoing data that is - /// considered "low." Defaults to 0. - pub fn buffered_amount_low_threshold(&self) -> usize { - self.buffered_amount_low.load(Ordering::SeqCst) - } - - /// set_buffered_amount_low_threshold is used to update the threshold. - /// See buffered_amount_low_threshold(). - pub fn set_buffered_amount_low_threshold(&self, th: usize) { - self.buffered_amount_low.store(th, Ordering::SeqCst); - } - - /// on_buffered_amount_low sets the callback handler which would be called when the number of - /// bytes of outgoing data buffered is lower than the threshold. - pub fn on_buffered_amount_low(&self, f: OnBufferedAmountLowFn) { - self.on_buffered_amount_low - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// This method is called by association's read_loop (go-)routine to notify this stream - /// of the specified amount of outgoing data has been delivered to the peer. - pub(crate) async fn on_buffer_released(&self, n_bytes_released: i64) { - if n_bytes_released <= 0 { - return; - } - - let from_amount = self.buffered_amount.load(Ordering::SeqCst); - let new_amount = if from_amount < n_bytes_released as usize { - self.buffered_amount.store(0, Ordering::SeqCst); - log::error!( - "[{}] released buffer size {} should be <= {}", - self.name, - n_bytes_released, - 0, - ); - 0 - } else { - self.buffered_amount - .fetch_sub(n_bytes_released as usize, Ordering::SeqCst); - - from_amount - n_bytes_released as usize - }; - - let buffered_amount_low = self.buffered_amount_low.load(Ordering::SeqCst); - - log::trace!( - "[{}] bufferedAmount = {}, from_amount = {}, buffered_amount_low = {}", - self.name, - new_amount, - from_amount, - buffered_amount_low, - ); - - if from_amount > buffered_amount_low && new_amount <= buffered_amount_low { - if let Some(handler) = &*self.on_buffered_amount_low.load() { - let mut f = handler.lock().await; - f().await; - } - } - } - - /// get_num_bytes_in_reassembly_queue returns the number of bytes of data currently queued to - /// be read (once chunk is complete). - pub(crate) async fn get_num_bytes_in_reassembly_queue(&self) -> usize { - // No lock is required as it reads the size with atomic load function. - let reassembly_queue = self.reassembly_queue.lock().await; - reassembly_queue.get_num_bytes() - } - - /// get_state atomically returns the state of the Association. - fn get_state(&self) -> AssociationState { - self.state.load(Ordering::SeqCst).into() - } - - fn awake_write_loop(&self) { - //log::debug!("[{}] awake_write_loop_ch.notify_one", self.name); - if let Some(awake_write_loop_ch) = &self.awake_write_loop_ch { - let _ = awake_write_loop_ch.try_send(()); - } - } - - async fn send_payload_data(&self, chunks: Vec) -> Result<()> { - let state = self.get_state(); - if state != AssociationState::Established { - return Err(Error::ErrPayloadDataStateNotExist); - } - - // NOTE: append is used here instead of push in order to prevent chunks interlacing. - self.pending_queue.append(chunks).await; - - self.awake_write_loop(); - Ok(()) - } - - async fn send_reset_request(&self, stream_identifier: u16) -> Result<()> { - let state = self.get_state(); - if state != AssociationState::Established { - return Err(Error::ErrResetPacketInStateNotExist); - } - - // Create DATA chunk which only contains valid stream identifier with - // nil userData and use it as a EOS from the stream. - let c = ChunkPayloadData { - stream_identifier, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::new(), - ..Default::default() - }; - - self.pending_queue.push(c).await; - - self.awake_write_loop(); - Ok(()) - } -} - -/// Default capacity of the temporary read buffer used by [`PollStream`]. -const DEFAULT_READ_BUF_SIZE: usize = 8192; - -/// State of the read `Future` in [`PollStream`]. -enum ReadFut { - /// Nothing in progress. - Idle, - /// Reading data from the underlying stream. - Reading(Pin>> + Send>>), - /// Finished reading, but there's unread data in the temporary buffer. - RemainingData(Vec), -} - -enum ShutdownFut { - /// Nothing in progress. - Idle, - /// Reading data from the underlying stream. - ShuttingDown(Pin>>>), - /// Shutdown future has run - Done, - Errored(crate::error::Error), -} - -impl ReadFut { - /// Gets a mutable reference to the future stored inside `Reading(future)`. - /// - /// # Panics - /// - /// Panics if `ReadFut` variant is not `Reading`. - fn get_reading_mut(&mut self) -> &mut Pin>> + Send>> { - match self { - ReadFut::Reading(ref mut fut) => fut, - _ => panic!("expected ReadFut to be Reading"), - } - } -} - -impl ShutdownFut { - /// Gets a mutable reference to the future stored inside `ShuttingDown(future)`. - /// - /// # Panics - /// - /// Panics if `ShutdownFut` variant is not `ShuttingDown`. - fn get_shutting_down_mut( - &mut self, - ) -> &mut Pin>>> { - match self { - ShutdownFut::ShuttingDown(ref mut fut) => fut, - _ => panic!("expected ShutdownFut to be ShuttingDown"), - } - } -} - -/// A wrapper around around [`Stream`], which implements [`AsyncRead`] and -/// [`AsyncWrite`]. -/// -/// Both `poll_read` and `poll_write` calls allocate temporary buffers, which results in an -/// additional overhead. -pub struct PollStream { - stream: Arc, - - read_fut: ReadFut, - write_fut: Option>>>>, - shutdown_fut: ShutdownFut, - - read_buf_cap: usize, -} - -impl PollStream { - /// Constructs a new `PollStream`. - /// - /// # Examples - /// - /// ``` - /// use webrtc_sctp::stream::{Stream, PollStream}; - /// use std::sync::Arc; - /// - /// let stream = Arc::new(Stream::default()); - /// let poll_stream = PollStream::new(stream); - /// ``` - pub fn new(stream: Arc) -> Self { - Self { - stream, - read_fut: ReadFut::Idle, - write_fut: None, - shutdown_fut: ShutdownFut::Idle, - read_buf_cap: DEFAULT_READ_BUF_SIZE, - } - } - - /// Get back the inner stream. - #[must_use] - pub fn into_inner(self) -> Arc { - self.stream - } - - /// Obtain a clone of the inner stream. - #[must_use] - pub fn clone_inner(&self) -> Arc { - self.stream.clone() - } - - /// stream_identifier returns the Stream identifier associated to the stream. - pub fn stream_identifier(&self) -> u16 { - self.stream.stream_identifier - } - - /// buffered_amount returns the number of bytes of data currently queued to be sent over this stream. - pub fn buffered_amount(&self) -> usize { - self.stream.buffered_amount.load(Ordering::SeqCst) - } - - /// buffered_amount_low_threshold returns the number of bytes of buffered outgoing data that is - /// considered "low." Defaults to 0. - pub fn buffered_amount_low_threshold(&self) -> usize { - self.stream.buffered_amount_low.load(Ordering::SeqCst) - } - - /// get_num_bytes_in_reassembly_queue returns the number of bytes of data currently queued to - /// be read (once chunk is complete). - pub(crate) async fn get_num_bytes_in_reassembly_queue(&self) -> usize { - // No lock is required as it reads the size with atomic load function. - let reassembly_queue = self.stream.reassembly_queue.lock().await; - reassembly_queue.get_num_bytes() - } - - /// Set the capacity of the temporary read buffer (default: 8192). - pub fn set_read_buf_capacity(&mut self, capacity: usize) { - self.read_buf_cap = capacity - } -} - -impl AsyncRead for PollStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if buf.remaining() == 0 { - return Poll::Ready(Ok(())); - } - - let fut = match self.read_fut { - ReadFut::Idle => { - // read into a temporary buffer because `buf` has an unonymous lifetime, which can - // be shorter than the lifetime of `read_fut`. - let stream = self.stream.clone(); - let mut temp_buf = vec![0; self.read_buf_cap]; - self.read_fut = ReadFut::Reading(Box::pin(async move { - stream.read(temp_buf.as_mut_slice()).await.map(|n| { - temp_buf.truncate(n); - temp_buf - }) - })); - self.read_fut.get_reading_mut() - } - ReadFut::Reading(ref mut fut) => fut, - ReadFut::RemainingData(ref mut data) => { - let remaining = buf.remaining(); - let len = std::cmp::min(data.len(), remaining); - buf.put_slice(&data[..len]); - if data.len() > remaining { - // ReadFut remains to be RemainingData - data.drain(0..len); - } else { - self.read_fut = ReadFut::Idle; - } - return Poll::Ready(Ok(())); - } - }; - - loop { - match fut.as_mut().poll(cx) { - Poll::Pending => return Poll::Pending, - // retry immediately upon empty data or incomplete chunks - // since there's no way to setup a waker. - Poll::Ready(Err(Error::ErrTryAgain)) => {} - // EOF has been reached => don't touch buf and just return Ok - Poll::Ready(Err(Error::ErrEof)) => { - self.read_fut = ReadFut::Idle; - return Poll::Ready(Ok(())); - } - Poll::Ready(Err(e)) => { - self.read_fut = ReadFut::Idle; - return Poll::Ready(Err(e.into())); - } - Poll::Ready(Ok(mut temp_buf)) => { - let remaining = buf.remaining(); - let len = std::cmp::min(temp_buf.len(), remaining); - buf.put_slice(&temp_buf[..len]); - if temp_buf.len() > remaining { - temp_buf.drain(0..len); - self.read_fut = ReadFut::RemainingData(temp_buf); - } else { - self.read_fut = ReadFut::Idle; - } - return Poll::Ready(Ok(())); - } - } - } - } -} - -impl AsyncWrite for PollStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - if buf.is_empty() { - return Poll::Ready(Ok(0)); - } - - if let Some(fut) = self.write_fut.as_mut() { - match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - let stream = self.stream.clone(); - let bytes = Bytes::copy_from_slice(buf); - self.write_fut = Some(Box::pin(async move { stream.write(&bytes).await })); - Poll::Ready(Err(e.into())) - } - // Given the data is buffered, it's okay to ignore the number of written bytes. - // - // TODO: In the long term, `stream.write` should be made sync. Then we could - // remove the whole `if` condition and just call `stream.write`. - Poll::Ready(Ok(_)) => { - let stream = self.stream.clone(); - let bytes = Bytes::copy_from_slice(buf); - self.write_fut = Some(Box::pin(async move { stream.write(&bytes).await })); - Poll::Ready(Ok(buf.len())) - } - } - } else { - let stream = self.stream.clone(); - let bytes = Bytes::copy_from_slice(buf); - let fut = self - .write_fut - .insert(Box::pin(async move { stream.write(&bytes).await })); - - match fut.as_mut().poll(cx) { - // If it's the first time we're polling the future, `Poll::Pending` can't be - // returned because that would mean the `PollStream` is not ready for writing. And - // this is not true since we've just created a future, which is going to write the - // buf to the underlying stream. - // - // It's okay to return `Poll::Ready` if the data is buffered (this is what the - // buffered writer and `File` do). - Poll::Pending => Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(e)) => { - self.write_fut = None; - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(n)) => { - self.write_fut = None; - Poll::Ready(Ok(n)) - } - } - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.write_fut.as_mut() { - Some(fut) => match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - self.write_fut = None; - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(_)) => { - self.write_fut = None; - Poll::Ready(Ok(())) - } - }, - None => Poll::Ready(Ok(())), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(_) => {} - } - let fut = match self.shutdown_fut { - ShutdownFut::Done => return Poll::Ready(Ok(())), - ShutdownFut::Errored(ref err) => return Poll::Ready(Err(err.clone().into())), - ShutdownFut::ShuttingDown(ref mut fut) => fut, - ShutdownFut::Idle => { - let stream = self.stream.clone(); - self.shutdown_fut = ShutdownFut::ShuttingDown(Box::pin(async move { - stream.shutdown(Shutdown::Write).await - })); - self.shutdown_fut.get_shutting_down_mut() - } - }; - - match fut.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - self.shutdown_fut = ShutdownFut::Errored(e.clone()); - Poll::Ready(Err(e.into())) - } - Poll::Ready(Ok(_)) => { - self.shutdown_fut = ShutdownFut::Done; - Poll::Ready(Ok(())) - } - } - } -} - -impl Clone for PollStream { - fn clone(&self) -> PollStream { - PollStream::new(self.clone_inner()) - } -} - -impl fmt::Debug for PollStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PollStream") - .field("stream", &self.stream) - .field("read_buf_cap", &self.read_buf_cap) - .finish() - } -} - -impl AsRef for PollStream { - fn as_ref(&self) -> &Stream { - &self.stream - } -} diff --git a/sctp/src/stream/stream_test.rs b/sctp/src/stream/stream_test.rs deleted file mode 100644 index 59aaa5ec3..000000000 --- a/sctp/src/stream/stream_test.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicU32; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -use super::*; - -#[test] -fn test_stream_buffered_amount() -> Result<()> { - let s = Stream::default(); - - assert_eq!(s.buffered_amount(), 0); - assert_eq!(s.buffered_amount_low_threshold(), 0); - - s.buffered_amount.store(8192, Ordering::SeqCst); - s.set_buffered_amount_low_threshold(2048); - assert_eq!(s.buffered_amount(), 8192, "unexpected bufferedAmount"); - assert_eq!( - s.buffered_amount_low_threshold(), - 2048, - "unexpected threshold" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_stream_amount_on_buffered_amount_low() -> Result<()> { - let s = Stream::default(); - - s.buffered_amount.store(4096, Ordering::SeqCst); - s.set_buffered_amount_low_threshold(2048); - - let n_cbs = Arc::new(AtomicU32::new(0)); - let n_cbs2 = n_cbs.clone(); - - s.on_buffered_amount_low(Box::new(move || { - n_cbs2.fetch_add(1, Ordering::SeqCst); - Box::pin(async {}) - })); - - // Negative value should be ignored (by design) - s.on_buffer_released(-32).await; // bufferedAmount = 3072 - assert_eq!(s.buffered_amount(), 4096, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 0, "callback count mismatch"); - - // Above to above, no callback - s.on_buffer_released(1024).await; // bufferedAmount = 3072 - assert_eq!(s.buffered_amount(), 3072, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 0, "callback count mismatch"); - - // Above to equal, callback should be made - s.on_buffer_released(1024).await; // bufferedAmount = 2048 - assert_eq!(s.buffered_amount(), 2048, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 1, "callback count mismatch"); - - // Eaual to below, no callback - s.on_buffer_released(1024).await; // bufferedAmount = 1024 - assert_eq!(s.buffered_amount(), 1024, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 1, "callback count mismatch"); - - // Blow to below, no callback - s.on_buffer_released(1024).await; // bufferedAmount = 0 - assert_eq!(s.buffered_amount(), 0, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 1, "callback count mismatch"); - - // Capped at 0, no callback - s.on_buffer_released(1024).await; // bufferedAmount = 0 - assert_eq!(s.buffered_amount(), 0, "unexpected bufferedAmount"); - assert_eq!(n_cbs.load(Ordering::SeqCst), 1, "callback count mismatch"); - - Ok(()) -} - -#[tokio::test] -async fn test_stream() -> std::result::Result<(), io::Error> { - let s = Stream::new( - "test_poll_stream".to_owned(), - 0, - 4096, - Arc::new(AtomicU32::new(4096)), - Arc::new(AtomicU8::new(AssociationState::Established as u8)), - None, - Arc::new(PendingQueue::new()), - ); - - // getters - assert_eq!(s.stream_identifier(), 0); - assert_eq!(s.buffered_amount(), 0); - assert_eq!(s.buffered_amount_low_threshold(), 0); - assert_eq!(s.get_num_bytes_in_reassembly_queue().await, 0); - - // setters - s.set_default_payload_type(PayloadProtocolIdentifier::Binary); - s.set_reliability_params(true, ReliabilityType::Reliable, 0); - - // write - let n = s.write(&Bytes::from("Hello ")).await?; - assert_eq!(n, 6); - assert_eq!(s.buffered_amount(), 6); - let n = s - .write_sctp(&Bytes::from("world"), PayloadProtocolIdentifier::Binary) - .await?; - assert_eq!(n, 5); - assert_eq!(s.buffered_amount(), 11); - - // async read - // 1. pretend that we've received a chunk - s.handle_data(ChunkPayloadData { - unordered: true, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::from_static(&[0, 1, 2, 3, 4]), - payload_type: PayloadProtocolIdentifier::Binary, - ..Default::default() - }) - .await; - // 2. read it - let mut buf = [0; 5]; - s.read(&mut buf).await?; - assert_eq!(buf, [0, 1, 2, 3, 4]); - - // shutdown write - s.shutdown(Shutdown::Write).await?; - // write must fail - assert!(s.write(&Bytes::from("error")).await.is_err()); - // read should continue working - s.handle_data(ChunkPayloadData { - unordered: true, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::from_static(&[5, 6, 7, 8, 9]), - payload_type: PayloadProtocolIdentifier::Binary, - ..Default::default() - }) - .await; - let mut buf = [0; 5]; - s.read(&mut buf).await?; - assert_eq!(buf, [5, 6, 7, 8, 9]); - - // shutdown read - s.shutdown(Shutdown::Read).await?; - // read must return 0 - assert_eq!(s.read(&mut buf).await, Ok(0)); - - Ok(()) -} - -#[tokio::test] -async fn test_poll_stream() -> std::result::Result<(), io::Error> { - let s = Arc::new(Stream::new( - "test_poll_stream".to_owned(), - 0, - 4096, - Arc::new(AtomicU32::new(4096)), - Arc::new(AtomicU8::new(AssociationState::Established as u8)), - None, - Arc::new(PendingQueue::new()), - )); - let mut poll_stream = PollStream::new(s.clone()); - - // getters - assert_eq!(poll_stream.stream_identifier(), 0); - assert_eq!(poll_stream.buffered_amount(), 0); - assert_eq!(poll_stream.buffered_amount_low_threshold(), 0); - assert_eq!(poll_stream.get_num_bytes_in_reassembly_queue().await, 0); - - // async write - let n = poll_stream.write(&[1, 2, 3]).await?; - assert_eq!(n, 3); - poll_stream.flush().await?; - assert_eq!(poll_stream.buffered_amount(), 3); - - // async read - // 1. pretend that we've received a chunk - let sc = s.clone(); - sc.handle_data(ChunkPayloadData { - unordered: true, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::from_static(&[0, 1, 2, 3, 4]), - payload_type: PayloadProtocolIdentifier::Binary, - ..Default::default() - }) - .await; - // 2. read it - let mut buf = [0; 5]; - poll_stream.read_exact(&mut buf).await?; - assert_eq!(buf, [0, 1, 2, 3, 4]); - - // shutdown write - poll_stream.shutdown().await?; - // write must fail - assert!(poll_stream.write(&[1, 2, 3]).await.is_err()); - // read should continue working - sc.handle_data(ChunkPayloadData { - unordered: true, - beginning_fragment: true, - ending_fragment: true, - user_data: Bytes::from_static(&[5, 6, 7, 8, 9]), - payload_type: PayloadProtocolIdentifier::Binary, - ..Default::default() - }) - .await; - let mut buf = [0; 5]; - poll_stream.read_exact(&mut buf).await?; - assert_eq!(buf, [5, 6, 7, 8, 9]); - - // misc. - let clone = poll_stream.clone(); - assert_eq!(clone.stream_identifier(), poll_stream.stream_identifier()); - - Ok(()) -} diff --git a/sctp/src/timer/ack_timer.rs b/sctp/src/timer/ack_timer.rs deleted file mode 100644 index 9a3d8074f..000000000 --- a/sctp/src/timer/ack_timer.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::sync::Weak; - -use async_trait::async_trait; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; - -pub(crate) const ACK_INTERVAL: Duration = Duration::from_millis(200); - -/// ackTimerObserver is the interface to an ack timer observer. -#[async_trait] -pub(crate) trait AckTimerObserver { - async fn on_ack_timeout(&mut self); -} - -/// ackTimer provides the retnransmission timer conforms with RFC 4960 Sec 6.3.1 -#[derive(Default, Debug)] -pub(crate) struct AckTimer { - pub(crate) timeout_observer: Weak>, - pub(crate) interval: Duration, - pub(crate) close_tx: Option>, -} - -impl AckTimer { - /// newAckTimer creates a new acknowledgement timer used to enable delayed ack. - pub(crate) fn new(timeout_observer: Weak>, interval: Duration) -> Self { - AckTimer { - timeout_observer, - interval, - close_tx: None, - } - } - - /// start starts the timer. - pub(crate) fn start(&mut self) -> bool { - // this timer is already closed - if self.close_tx.is_some() { - return false; - } - - let (close_tx, mut close_rx) = mpsc::channel(1); - let interval = self.interval; - let timeout_observer = self.timeout_observer.clone(); - - tokio::spawn(async move { - let timer = tokio::time::sleep(interval); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() => { - if let Some(observer) = timeout_observer.upgrade(){ - let mut observer = observer.lock().await; - observer.on_ack_timeout().await; - } - } - _ = close_rx.recv() => {}, - } - }); - - self.close_tx = Some(close_tx); - true - } - - /// stops the timer. this is similar to stop() but subsequent start() call - /// will fail (the timer is no longer usable) - pub(crate) fn stop(&mut self) { - self.close_tx.take(); - } - - /// isRunning tests if the timer is running. - /// Debug purpose only - pub(crate) fn is_running(&self) -> bool { - self.close_tx.is_some() - } -} diff --git a/sctp/src/timer/mod.rs b/sctp/src/timer/mod.rs deleted file mode 100644 index 822246260..000000000 --- a/sctp/src/timer/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(test)] -mod timer_test; - -pub(crate) mod ack_timer; -pub(crate) mod rtx_timer; diff --git a/sctp/src/timer/rtx_timer.rs b/sctp/src/timer/rtx_timer.rs deleted file mode 100644 index ab8ad66dc..000000000 --- a/sctp/src/timer/rtx_timer.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::sync::{Arc, Weak}; - -use async_trait::async_trait; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; - -use crate::association::RtxTimerId; - -pub(crate) const RTO_INITIAL: u64 = 3000; // msec -pub(crate) const RTO_MIN: u64 = 1000; // msec -pub(crate) const RTO_MAX: u64 = 60000; // msec -pub(crate) const RTO_ALPHA: u64 = 1; -pub(crate) const RTO_BETA: u64 = 2; -pub(crate) const RTO_BASE: u64 = 8; -pub(crate) const MAX_INIT_RETRANS: usize = 8; -pub(crate) const PATH_MAX_RETRANS: usize = 5; -pub(crate) const NO_MAX_RETRANS: usize = 0; - -/// rtoManager manages Rtx timeout values. -/// This is an implementation of RFC 4960 sec 6.3.1. -#[derive(Default, Debug)] -pub(crate) struct RtoManager { - pub(crate) srtt: u64, - pub(crate) rttvar: f64, - pub(crate) rto: u64, - pub(crate) no_update: bool, -} - -impl RtoManager { - /// newRTOManager creates a new rtoManager. - pub(crate) fn new() -> Self { - RtoManager { - rto: RTO_INITIAL, - ..Default::default() - } - } - - /// set_new_rtt takes a newly measured RTT then adjust the RTO in msec. - pub(crate) fn set_new_rtt(&mut self, rtt: u64) -> u64 { - if self.no_update { - return self.srtt; - } - - if self.srtt == 0 { - // First measurement - self.srtt = rtt; - self.rttvar = rtt as f64 / 2.0; - } else { - // Subsequent rtt measurement - self.rttvar = ((RTO_BASE - RTO_BETA) as f64 * self.rttvar - + RTO_BETA as f64 * (self.srtt as i64 - rtt as i64).abs() as f64) - / RTO_BASE as f64; - self.srtt = ((RTO_BASE - RTO_ALPHA) * self.srtt + RTO_ALPHA * rtt) / RTO_BASE; - } - - self.rto = (self.srtt + (4.0 * self.rttvar) as u64).clamp(RTO_MIN, RTO_MAX); - - self.srtt - } - - /// get_rto simply returns the current RTO in msec. - pub(crate) fn get_rto(&self) -> u64 { - self.rto - } - - /// reset resets the RTO variables to the initial values. - pub(crate) fn reset(&mut self) { - if self.no_update { - return; - } - - self.srtt = 0; - self.rttvar = 0.0; - self.rto = RTO_INITIAL; - } - - /// set RTO value for testing - pub(crate) fn set_rto(&mut self, rto: u64, no_update: bool) { - self.rto = rto; - self.no_update = no_update; - } -} - -pub(crate) fn calculate_next_timeout(rto: u64, n_rtos: usize) -> u64 { - // RFC 4096 sec 6.3.3. Handle T3-rtx Expiration - // E2) For the destination address for which the timer expires, set RTO - // <- RTO * 2 ("back off the timer"). The maximum value discussed - // in rule C7 above (RTO.max) may be used to provide an upper bound - // to this doubling operation. - if n_rtos < 31 { - std::cmp::min(rto << n_rtos, RTO_MAX) - } else { - RTO_MAX - } -} - -/// rtxTimerObserver is the interface to a timer observer. -/// NOTE: Observers MUST NOT call start() or stop() method on rtxTimer -/// from within these callbacks. -#[async_trait] -pub(crate) trait RtxTimerObserver { - async fn on_retransmission_timeout(&mut self, timer_id: RtxTimerId, n: usize); - async fn on_retransmission_failure(&mut self, timer_id: RtxTimerId); -} - -/// rtxTimer provides the retnransmission timer conforms with RFC 4960 Sec 6.3.1 -#[derive(Default, Debug)] -pub(crate) struct RtxTimer { - pub(crate) timeout_observer: Weak>, - pub(crate) id: RtxTimerId, - pub(crate) max_retrans: usize, - pub(crate) close_tx: Arc>>>, -} - -impl RtxTimer { - /// newRTXTimer creates a new retransmission timer. - /// if max_retrans is set to 0, it will keep retransmitting until stop() is called. - /// (it will never make on_retransmission_failure() callback. - pub(crate) fn new( - timeout_observer: Weak>, - id: RtxTimerId, - max_retrans: usize, - ) -> Self { - RtxTimer { - timeout_observer, - id, - max_retrans, - close_tx: Arc::new(Mutex::new(None)), - } - } - - /// start starts the timer. - pub(crate) async fn start(&self, rto: u64) -> bool { - // Note: rto value is intentionally not capped by RTO.Min to allow - // fast timeout for the tests. Non-test code should pass in the - // rto generated by rtoManager get_rto() method which caps the - // value at RTO.Min or at RTO.Max. - - // this timer is already closed - let mut close_rx = { - let mut close = self.close_tx.lock().await; - if close.is_some() { - return false; - } - - let (close_tx, close_rx) = mpsc::channel(1); - *close = Some(close_tx); - close_rx - }; - - let id = self.id; - let max_retrans = self.max_retrans; - let close_tx = Arc::clone(&self.close_tx); - let timeout_observer = self.timeout_observer.clone(); - - tokio::spawn(async move { - let mut n_rtos = 0; - - loop { - let interval = calculate_next_timeout(rto, n_rtos); - let timer = tokio::time::sleep(Duration::from_millis(interval)); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() => { - n_rtos+=1; - - let failure = { - if let Some(observer) = timeout_observer.upgrade(){ - let mut observer = observer.lock().await; - if max_retrans == 0 || n_rtos <= max_retrans { - observer.on_retransmission_timeout(id, n_rtos).await; - false - } else { - observer.on_retransmission_failure(id).await; - true - } - }else{ - true - } - }; - if failure { - let mut close = close_tx.lock().await; - *close = None; - break; - } - } - _ = close_rx.recv() => break, - } - } - }); - - true - } - - /// stop stops the timer. - pub(crate) async fn stop(&self) { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - /// isRunning tests if the timer is running. - /// Debug purpose only - pub(crate) async fn is_running(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_some() - } -} diff --git a/sctp/src/timer/timer_test.rs b/sctp/src/timer/timer_test.rs deleted file mode 100644 index d7afdf300..000000000 --- a/sctp/src/timer/timer_test.rs +++ /dev/null @@ -1,511 +0,0 @@ -// Silence warning on `for i in 0..vec.len() { … }`: -#![allow(clippy::needless_range_loop)] -// Silence warning on `..Default::default()` with no effect: -#![allow(clippy::needless_update)] - -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use portable_atomic::AtomicU32; -use tokio::sync::Mutex; -use tokio::time::{sleep, Duration}; - -/////////////////////////////////////////////////////////////////// -//ack_timer_test -/////////////////////////////////////////////////////////////////// -use super::ack_timer::*; - -mod test_ack_timer { - use super::*; - use crate::error::Result; - - struct TestAckTimerObserver { - ncbs: Arc, - } - - #[async_trait] - impl AckTimerObserver for TestAckTimerObserver { - async fn on_ack_timeout(&mut self) { - log::trace!("ack timed out"); - self.ncbs.fetch_add(1, Ordering::SeqCst); - } - } - - #[tokio::test] - async fn test_ack_timer_start_and_stop() -> Result<()> { - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestAckTimerObserver { ncbs: ncbs.clone() })); - - let mut rt = AckTimer::new(Arc::downgrade(&obs), ACK_INTERVAL); - - // should start ok - let ok = rt.start(); - assert!(ok, "start() should succeed"); - assert!(rt.is_running(), "should be running"); - - // stop immedidately - rt.stop(); - assert!(!rt.is_running(), "should not be running"); - - // Sleep more than 200msec of interval to test if it never times out - sleep(ACK_INTERVAL + Duration::from_millis(50)).await; - - assert_eq!( - ncbs.load(Ordering::SeqCst), - 0, - "should not be timed out (actual: {})", - ncbs.load(Ordering::SeqCst) - ); - - // can start again - let ok = rt.start(); - assert!(ok, "start() should succeed again"); - assert!(rt.is_running(), "should be running"); - - // should close ok - rt.stop(); - assert!(!rt.is_running(), "should not be running"); - - Ok(()) - } -} - -/////////////////////////////////////////////////////////////////// -//rtx_timer_test -/////////////////////////////////////////////////////////////////// -use super::rtx_timer::*; - -mod test_rto_manager { - use super::*; - use crate::error::Result; - - #[tokio::test] - async fn test_rto_manager_initial_values() -> Result<()> { - let m = RtoManager::new(); - assert_eq!(m.rto, RTO_INITIAL, "should be rtoInitial"); - assert_eq!(m.get_rto(), RTO_INITIAL, "should be rtoInitial"); - assert_eq!(m.srtt, 0, "should be 0"); - assert_eq!(m.rttvar, 0.0, "should be 0.0"); - - Ok(()) - } - - #[tokio::test] - async fn test_rto_manager_rto_calculation_small_rtt() -> Result<()> { - let mut m = RtoManager::new(); - let exp = [1800, 1500, 1275, 1106, 1000]; - - for i in 0..5 { - m.set_new_rtt(600); - let rto = m.get_rto(); - assert_eq!(rto, exp[i], "should be equal: {i}"); - } - - Ok(()) - } - - #[tokio::test] - async fn test_rto_manager_rto_calculation_large_rtt() -> Result<()> { - let mut m = RtoManager::new(); - let exp = [ - 60000, // capped at RTO.Max - 60000, // capped at RTO.Max - 60000, // capped at RTO.Max - 55312, 48984, - ]; - - for i in 0..5 { - m.set_new_rtt(30000); - let rto = m.get_rto(); - assert_eq!(rto, exp[i], "should be equal: {i}"); - } - - Ok(()) - } - - #[tokio::test] - async fn test_rto_manager_calculate_next_timeout() -> Result<()> { - let rto = calculate_next_timeout(1, 0); - assert_eq!(rto, 1, "should match"); - let rto = calculate_next_timeout(1, 1); - assert_eq!(rto, 2, "should match"); - let rto = calculate_next_timeout(1, 2); - assert_eq!(rto, 4, "should match"); - let rto = calculate_next_timeout(1, 30); - assert_eq!(rto, 60000, "should match"); - let rto = calculate_next_timeout(1, 63); - assert_eq!(rto, 60000, "should match"); - let rto = calculate_next_timeout(1, 64); - assert_eq!(rto, 60000, "should match"); - - Ok(()) - } - - #[tokio::test] - async fn test_rto_manager_reset() -> Result<()> { - let mut m = RtoManager::new(); - for _ in 0..10 { - m.set_new_rtt(200); - } - - m.reset(); - assert_eq!(m.get_rto(), RTO_INITIAL, "should be rtoInitial"); - assert_eq!(m.srtt, 0, "should be 0"); - assert_eq!(m.rttvar, 0.0, "should be 0"); - - Ok(()) - } -} - -//TODO: remove this conditional test -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -mod test_rtx_timer { - use std::time::SystemTime; - - use tokio::sync::mpsc; - - use super::*; - use crate::association::RtxTimerId; - use crate::error::Result; - - struct TestTimerObserver { - ncbs: Arc, - timer_id: RtxTimerId, - done_tx: Option>, - max_rtos: usize, - } - - impl Default for TestTimerObserver { - fn default() -> Self { - TestTimerObserver { - ncbs: Arc::new(AtomicU32::new(0)), - timer_id: RtxTimerId::T1Init, - done_tx: None, - max_rtos: 0, - } - } - } - - #[async_trait] - impl RtxTimerObserver for TestTimerObserver { - async fn on_retransmission_timeout(&mut self, timer_id: RtxTimerId, n_rtos: usize) { - self.ncbs.fetch_add(1, Ordering::SeqCst); - // 30 : 1 (30) - // 60 : 2 (90) - // 120: 3 (210) - // 240: 4 (550) <== expected in 650 msec - assert_eq!(self.timer_id, timer_id, "unexpected timer ID: {timer_id}"); - if (self.max_rtos > 0 && n_rtos == self.max_rtos) || self.max_rtos == usize::MAX { - if let Some(done) = &self.done_tx { - let elapsed = SystemTime::now(); - let _ = done.send(elapsed).await; - } - } - } - - async fn on_retransmission_failure(&mut self, timer_id: RtxTimerId) { - if self.max_rtos == 0 { - if let Some(done) = &self.done_tx { - assert_eq!(self.timer_id, timer_id, "unexpted timer ID: {timer_id}"); - let elapsed = SystemTime::now(); - //t.Logf("onRtxFailure: elapsed=%.03f\n", elapsed) - let _ = done.send(elapsed).await; - } - } else { - panic!("timer should not fail"); - } - } - } - - #[tokio::test] - async fn test_rtx_timer_callback_interval() -> Result<()> { - let timer_id = RtxTimerId::T1Init; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - assert!(!rt.is_running().await, "should not be running"); - - // since := time.Now() - let ok = rt.start(30).await; - assert!(ok, "should be true"); - assert!(rt.is_running().await, "should be running"); - - sleep(Duration::from_millis(650)).await; - rt.stop().await; - assert!(!rt.is_running().await, "should not be running"); - - assert_eq!(ncbs.load(Ordering::SeqCst), 4, "should be called 4 times"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_last_start_wins() -> Result<()> { - let timer_id = RtxTimerId::T3RTX; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - let interval = 30; - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - let ok = rt.start(interval * 99).await; // should ignored - assert!(!ok, "should be ignored"); - let ok = rt.start(interval * 99).await; // should ignored - assert!(!ok, "should be ignored"); - - sleep(Duration::from_millis((interval * 3) / 2)).await; - rt.stop().await; - - assert!(!rt.is_running().await, "should not be running"); - assert_eq!(ncbs.load(Ordering::SeqCst), 1, "must be called once"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_stop_right_after_start() -> Result<()> { - let timer_id = RtxTimerId::T3RTX; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - let interval = 30; - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - rt.stop().await; - - sleep(Duration::from_millis((interval * 3) / 2)).await; - rt.stop().await; - - assert!(!rt.is_running().await, "should not be running"); - assert_eq!(ncbs.load(Ordering::SeqCst), 0, "no callback should be made"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_start_stop_then_start() -> Result<()> { - let timer_id = RtxTimerId::T1Cookie; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - let interval = 30; - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - rt.stop().await; - assert!(!rt.is_running().await, "should NOT be running"); - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "should be running"); - - sleep(Duration::from_millis((interval * 3) / 2)).await; - rt.stop().await; - - assert!(!rt.is_running().await, "should NOT be running"); - assert_eq!(ncbs.load(Ordering::SeqCst), 1, "must be called once"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_start_and_stop_in_atight_loop() -> Result<()> { - let timer_id = RtxTimerId::T2Shutdown; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - for _ in 0..1000 { - let ok = rt.start(30).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "should be running"); - rt.stop().await; - assert!(!rt.is_running().await, "should NOT be running"); - } - - assert_eq!(ncbs.load(Ordering::SeqCst), 0, "no callback should be made"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_should_stop_after_rtx_failure() -> Result<()> { - let (done_tx, mut done_rx) = mpsc::channel(1); - - let timer_id = RtxTimerId::Reconfig; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - done_tx: Some(done_tx), - ..Default::default() - })); - - let since = SystemTime::now(); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - // RTO(msec) Total(msec) - // 10 10 1st RTO - // 20 30 2nd RTO - // 40 70 3rd RTO - // 80 150 4th RTO - // 160 310 5th RTO (== Path.Max.Retrans) - // 320 630 Failure - - let interval = 10; - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "should be running"); - - let elapsed = done_rx.recv().await; - - assert!(!rt.is_running().await, "should not be running"); - assert_eq!(ncbs.load(Ordering::SeqCst), 5, "should be called 5 times"); - - if let Some(elapsed) = elapsed { - let diff = elapsed.duration_since(since).unwrap(); - assert!( - diff > Duration::from_millis(600), - "must have taken more than 600 msec" - ); - assert!( - diff < Duration::from_millis(700), - "must fail in less than 700 msec" - ); - } - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_should_not_stop_if_max_retrans_is_zero() -> Result<()> { - let (done_tx, mut done_rx) = mpsc::channel(1); - - let timer_id = RtxTimerId::Reconfig; - let max_rtos = 6; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - done_tx: Some(done_tx), - max_rtos, - ..Default::default() - })); - - let since = SystemTime::now(); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, 0); - - // RTO(msec) Total(msec) - // 10 10 1st RTO - // 20 30 2nd RTO - // 40 70 3rd RTO - // 80 150 4th RTO - // 160 310 5th RTO - // 320 630 6th RTO => exit test (timer should still be running) - - let interval = 10; - let ok = rt.start(interval).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "should be running"); - - let elapsed = done_rx.recv().await; - - assert!(rt.is_running().await, "should still be running"); - assert_eq!(ncbs.load(Ordering::SeqCst), 6, "should be called 6 times"); - - if let Some(elapsed) = elapsed { - let diff = elapsed.duration_since(since).unwrap(); - assert!( - diff > Duration::from_millis(600), - "must have taken more than 600 msec" - ); - assert!( - diff < Duration::from_millis(700), - "must fail in less than 700 msec" - ); - } - - rt.stop().await; - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_stop_timer_that_is_not_running_is_noop() -> Result<()> { - let (done_tx, mut done_rx) = mpsc::channel(1); - - let timer_id = RtxTimerId::Reconfig; - let obs = Arc::new(Mutex::new(TestTimerObserver { - timer_id, - done_tx: Some(done_tx), - max_rtos: usize::MAX, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - for _ in 0..10 { - rt.stop().await; - } - - let ok = rt.start(20).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "must be running"); - - let _ = done_rx.recv().await; - rt.stop().await; - assert!(!rt.is_running().await, "must be false"); - - Ok(()) - } - - #[tokio::test] - async fn test_rtx_timer_closed_timer_wont_start() -> Result<()> { - let timer_id = RtxTimerId::Reconfig; - let ncbs = Arc::new(AtomicU32::new(0)); - let obs = Arc::new(Mutex::new(TestTimerObserver { - ncbs: ncbs.clone(), - timer_id, - ..Default::default() - })); - let rt = RtxTimer::new(Arc::downgrade(&obs), timer_id, PATH_MAX_RETRANS); - - let ok = rt.start(20).await; - assert!(ok, "should be accepted"); - assert!(rt.is_running().await, "must be running"); - - rt.stop().await; - assert!(!rt.is_running().await, "must be false"); - - //let ok = rt.start(obs.clone(), 20).await; - //assert!(!ok, "should not start"); - assert!(!rt.is_running().await, "must not be running"); - - sleep(Duration::from_millis(100)).await; - assert_eq!(ncbs.load(Ordering::SeqCst), 0, "RTO should not occur"); - - Ok(()) - } -} diff --git a/sctp/src/util.rs b/sctp/src/util.rs deleted file mode 100644 index ee0121a4f..000000000 --- a/sctp/src/util.rs +++ /dev/null @@ -1,241 +0,0 @@ -use bytes::Bytes; -use crc::{Crc, Table, CRC_32_ISCSI}; - -pub(crate) const PADDING_MULTIPLE: usize = 4; - -pub(crate) fn get_padding_size(len: usize) -> usize { - (PADDING_MULTIPLE - (len % PADDING_MULTIPLE)) % PADDING_MULTIPLE -} - -/// Allocate and zero this data once. -/// We need to use it for the checksum and don't want to allocate/clear each time. -pub(crate) static FOUR_ZEROES: Bytes = Bytes::from_static(&[0, 0, 0, 0]); - -pub(crate) const ISCSI_CRC: Crc> = Crc::>::new(&CRC_32_ISCSI); - -/// Fastest way to do a crc32 without allocating. -pub(crate) fn generate_packet_checksum(raw: &Bytes) -> u32 { - let mut digest = ISCSI_CRC.digest(); - digest.update(&raw[0..8]); - digest.update(&FOUR_ZEROES[..]); - digest.update(&raw[12..]); - digest.finalize() -} - -/// Serial Number Arithmetic (RFC 1982) -#[inline] -pub(crate) fn sna32lt(i1: u32, i2: u32) -> bool { - (i1 < i2 && i2 - i1 < 1 << 31) || (i1 > i2 && i1 - i2 > 1 << 31) -} - -#[inline] -pub(crate) fn sna32lte(i1: u32, i2: u32) -> bool { - i1 == i2 || sna32lt(i1, i2) -} - -#[inline] -pub(crate) fn sna32gt(i1: u32, i2: u32) -> bool { - (i1 < i2 && (i2 - i1) >= 1 << 31) || (i1 > i2 && (i1 - i2) <= 1 << 31) -} - -#[inline] -pub(crate) fn sna32gte(i1: u32, i2: u32) -> bool { - i1 == i2 || sna32gt(i1, i2) -} - -#[inline] -pub(crate) fn sna32eq(i1: u32, i2: u32) -> bool { - i1 == i2 -} - -#[inline] -pub(crate) fn sna16lt(i1: u16, i2: u16) -> bool { - (i1 < i2 && (i2 - i1) < 1 << 15) || (i1 > i2 && (i1 - i2) > 1 << 15) -} - -#[inline] -pub(crate) fn sna16lte(i1: u16, i2: u16) -> bool { - i1 == i2 || sna16lt(i1, i2) -} - -#[inline] -pub(crate) fn sna16gt(i1: u16, i2: u16) -> bool { - (i1 < i2 && (i2 - i1) >= 1 << 15) || (i1 > i2 && (i1 - i2) <= 1 << 15) -} - -#[inline] -pub(crate) fn sna16gte(i1: u16, i2: u16) -> bool { - i1 == i2 || sna16gt(i1, i2) -} - -#[inline] -pub(crate) fn sna16eq(i1: u16, i2: u16) -> bool { - i1 == i2 -} - -#[cfg(test)] -mod test { - use super::*; - use crate::error::Result; - - const DIV: isize = 16; - - #[test] - fn test_serial_number_arithmetic32bit() -> Result<()> { - const SERIAL_BITS: u32 = 32; - const INTERVAL: u32 = ((1u64 << (SERIAL_BITS as u64)) / (DIV as u64)) as u32; - const MAX_FORWARD_DISTANCE: u32 = 1 << ((SERIAL_BITS - 1) - 1); - const MAX_BACKWARD_DISTANCE: u32 = 1 << (SERIAL_BITS - 1); - - for i in 0..DIV as u32 { - let s1 = i * INTERVAL; - let s2f = s1.checked_add(MAX_FORWARD_DISTANCE); - let s2b = s1.checked_add(MAX_BACKWARD_DISTANCE); - - if let (Some(s2f), Some(s2b)) = (s2f, s2b) { - assert!(sna32lt(s1, s2f), "s1 < s2 should be true: s1={s1} s2={s2f}"); - assert!( - !sna32lt(s1, s2b), - "s1 < s2 should be false: s1={s1} s2={s2b}" - ); - - assert!( - !sna32gt(s1, s2f), - "s1 > s2 should be false: s1={s1} s2={s2f}" - ); - assert!(sna32gt(s1, s2b), "s1 > s2 should be true: s1={s1} s2={s2b}"); - - assert!( - sna32lte(s1, s2f), - "s1 <= s2 should be true: s1={s1} s2={s2f}" - ); - assert!( - !sna32lte(s1, s2b), - "s1 <= s2 should be false: s1={s1} s2={s2b}" - ); - - assert!( - !sna32gte(s1, s2f), - "s1 >= s2 should be fales: s1={s1} s2={s2f}" - ); - assert!( - sna32gte(s1, s2b), - "s1 >= s2 should be true: s1={s1} s2={s2b}" - ); - - assert!( - sna32eq(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - assert!( - sna32lte(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - assert!( - sna32gte(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - } - - if let Some(s1add1) = s1.checked_add(1) { - assert!( - !sna32eq(s1, s1add1), - "s1 == s1+1 should be false: s1={s1} s1+1={s1add1}" - ); - } - - if let Some(s1sub1) = s1.checked_sub(1) { - assert!( - !sna32eq(s1, s1sub1), - "s1 == s1-1 should be false: s1={s1} s1-1={s1sub1}" - ); - } - - assert!(sna32eq(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - assert!(sna32lte(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - - assert!(sna32gte(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - } - - Ok(()) - } - - #[test] - fn test_serial_number_arithmetic16bit() -> Result<()> { - const SERIAL_BITS: u16 = 16; - const INTERVAL: u16 = ((1u64 << (SERIAL_BITS as u64)) / (DIV as u64)) as u16; - const MAX_FORWARD_DISTANCE: u16 = 1 << ((SERIAL_BITS - 1) - 1); - const MAX_BACKWARD_DISTANCE: u16 = 1 << (SERIAL_BITS - 1); - - for i in 0..DIV as u16 { - let s1 = i * INTERVAL; - let s2f = s1.checked_add(MAX_FORWARD_DISTANCE); - let s2b = s1.checked_add(MAX_BACKWARD_DISTANCE); - - if let (Some(s2f), Some(s2b)) = (s2f, s2b) { - assert!(sna16lt(s1, s2f), "s1 < s2 should be true: s1={s1} s2={s2f}"); - assert!( - !sna16lt(s1, s2b), - "s1 < s2 should be false: s1={s1} s2={s2b}" - ); - - assert!( - !sna16gt(s1, s2f), - "s1 > s2 should be fales: s1={s1} s2={s2f}" - ); - assert!(sna16gt(s1, s2b), "s1 > s2 should be true: s1={s1} s2={s2b}"); - - assert!( - sna16lte(s1, s2f), - "s1 <= s2 should be true: s1={s1} s2={s2f}" - ); - assert!( - !sna16lte(s1, s2b), - "s1 <= s2 should be false: s1={s1} s2={s2b}" - ); - - assert!( - !sna16gte(s1, s2f), - "s1 >= s2 should be fales: s1={s1} s2={s2f}" - ); - assert!( - sna16gte(s1, s2b), - "s1 >= s2 should be true: s1={s1} s2={s2b}" - ); - - assert!( - sna16eq(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - assert!( - sna16lte(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - assert!( - sna16gte(s2b, s2b), - "s2 == s2 should be true: s2={s2b} s2={s2b}" - ); - } - - assert!(sna16eq(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - - if let Some(s1add1) = s1.checked_add(1) { - assert!( - !sna16eq(s1, s1add1), - "s1 == s1+1 should be false: s1={s1} s1+1={s1add1}" - ); - } - if let Some(s1sub1) = s1.checked_sub(1) { - assert!( - !sna16eq(s1, s1sub1), - "s1 == s1-1 should be false: s1={s1} s1-1={s1sub1}" - ); - } - - assert!(sna16lte(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - assert!(sna16gte(s1, s1), "s1 == s1 should be true: s1={s1} s2={s1}"); - } - - Ok(()) - } -} diff --git a/sdp/.gitignore b/sdp/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/sdp/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/sdp/CHANGELOG.md b/sdp/CHANGELOG.md deleted file mode 100644 index dc8be9014..000000000 --- a/sdp/CHANGELOG.md +++ /dev/null @@ -1,18 +0,0 @@ -# sdp changelog - -## Unreleased - -* Implement from and tryfrom string traits for SessionDescription. - -## v0.5.3 - -* Increased minimum support rust version to `1.60.0`. - -## v0.5.2 - -* [#10 update deps + loosen some requirements](https://github.com/webrtc-rs/sdp/pull/10) by [@melekes](https://github.com/melekes). - -## Prior to 0.5.2 - -Before 0.5.2 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/sdp/releases). - diff --git a/sdp/Cargo.toml b/sdp/Cargo.toml deleted file mode 100644 index 6f5eda650..000000000 --- a/sdp/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "sdp" -version = "0.6.2" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of SDP" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/sdp" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/sdp" - -[dependencies] -url = "2" -rand = "0.8" -thiserror = "1" -substring = "1" - -[dev-dependencies] -criterion = "0.5" - -[[bench]] -name = "bench" -harness = false diff --git a/sdp/LICENSE-APACHE b/sdp/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/sdp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/sdp/LICENSE-MIT b/sdp/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/sdp/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sdp/README.md b/sdp/README.md deleted file mode 100644 index 651165c8b..000000000 --- a/sdp/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of SDP. Rewrite Pion SDP in Rust -

diff --git a/sdp/benches/bench.rs b/sdp/benches/bench.rs deleted file mode 100644 index 35b40047b..000000000 --- a/sdp/benches/bench.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::io::Cursor; - -use criterion::{criterion_group, criterion_main, Criterion}; -use sdp::SessionDescription; - -const CANONICAL_UNMARSHAL_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -i=A Seminar on the session description protocol\r\n\ -u=http://www.example.com/seminars/sdp.pdf\r\n\ -e=j.doe@example.com (Jane Doe)\r\n\ -p=+1 617 555-6011\r\n\ -c=IN IP4 224.2.17.12/127\r\n\ -b=X-YZ:128\r\n\ -b=AS:12345\r\n\ -t=2873397496 2873404696\r\n\ -t=3034423619 3042462419\r\n\ -r=604800 3600 0 90000\r\n\ -z=2882844526 -3600 2898848070 0\r\n\ -k=prompt\r\n\ -a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ -a=recvonly\r\n\ -m=audio 49170 RTP/AVP 0\r\n\ -i=Vivamus a posuere nisl\r\n\ -c=IN IP4 203.0.113.1\r\n\ -b=X-YZ:128\r\n\ -k=prompt\r\n\ -a=sendrecv\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -a=rtpmap:99 h263-1998/90000\r\n"; - -fn benchmark_sdp(c: &mut Criterion) { - let mut reader = Cursor::new(CANONICAL_UNMARSHAL_SDP.as_bytes()); - let sdp = SessionDescription::unmarshal(&mut reader).unwrap(); - - /////////////////////////////////////////////////////////////////////////////////////////////// - c.bench_function("Benchmark Marshal", |b| { - b.iter(|| { - let _ = sdp.marshal(); - }) - }); - - c.bench_function("Benchmark Unmarshal ", |b| { - b.iter(|| { - let mut reader = Cursor::new(CANONICAL_UNMARSHAL_SDP.as_bytes()); - let _ = SessionDescription::unmarshal(&mut reader).unwrap(); - }) - }); -} - -criterion_group!(benches, benchmark_sdp); -criterion_main!(benches); diff --git a/sdp/codecov.yml b/sdp/codecov.yml deleted file mode 100644 index e794d966a..000000000 --- a/sdp/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 40894be8-0942-482a-b7cf-e58721cff2c5 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/sdp/doc/webrtc.rs.png b/sdp/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/sdp/fuzz/.gitignore b/sdp/fuzz/.gitignore deleted file mode 100644 index 572e03bdf..000000000 --- a/sdp/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -target -corpus -artifacts diff --git a/sdp/fuzz/Cargo.toml b/sdp/fuzz/Cargo.toml deleted file mode 100644 index 52768eb2d..000000000 --- a/sdp/fuzz/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ - -[package] -name = "sdp-fuzz" -version = "0.0.0" -authors = ["Automatically generated"] -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" - -[dependencies.sdp] -path = ".." - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] - -[[bin]] -name = "parse_session" -path = "fuzz_targets/parse_session.rs" -test = false -doc = false diff --git a/sdp/fuzz/fuzz_targets/parse_session.rs b/sdp/fuzz/fuzz_targets/parse_session.rs deleted file mode 100644 index 4889ab62a..000000000 --- a/sdp/fuzz/fuzz_targets/parse_session.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![no_main] -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - let mut cursor = std::io::Cursor::new(data); - let _session = sdp::SessionDescription::unmarshal(&mut cursor); -}); diff --git a/sdp/src/description/common.rs b/sdp/src/description/common.rs deleted file mode 100644 index 4f93d9d46..000000000 --- a/sdp/src/description/common.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fmt; - -/// Information describes the "i=" field which provides textual information -/// about the session. -pub type Information = String; - -/// ConnectionInformation defines the representation for the "c=" field -/// containing connection data. -#[derive(Debug, Default, Clone)] -pub struct ConnectionInformation { - pub network_type: String, - pub address_type: String, - pub address: Option
, -} - -impl fmt::Display for ConnectionInformation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(address) = &self.address { - write!(f, "{} {} {}", self.network_type, self.address_type, address,) - } else { - write!(f, "{} {}", self.network_type, self.address_type,) - } - } -} - -/// Address describes a structured address token from within the "c=" field. -#[derive(Debug, Default, Clone)] -pub struct Address { - pub address: String, - pub ttl: Option, - pub range: Option, -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut parts = vec![self.address.to_owned()]; - if let Some(t) = &self.ttl { - parts.push(t.to_string()); - } - if let Some(r) = &self.range { - parts.push(r.to_string()); - } - write!(f, "{}", parts.join("/")) - } -} - -/// Bandwidth describes an optional field which denotes the proposed bandwidth -/// to be used by the session or media. -#[derive(Debug, Default, Clone)] -pub struct Bandwidth { - pub experimental: bool, - pub bandwidth_type: String, - pub bandwidth: u64, -} - -impl fmt::Display for Bandwidth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = if self.experimental { "X-" } else { "" }; - write!(f, "{}{}:{}", output, self.bandwidth_type, self.bandwidth) - } -} - -/// EncryptionKey describes the "k=" which conveys encryption key information. -pub type EncryptionKey = String; - -/// Attribute describes the "a=" field which represents the primary means for -/// extending SDP. -#[derive(Debug, Default, Clone)] -pub struct Attribute { - pub key: String, - pub value: Option, -} - -impl fmt::Display for Attribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(value) = &self.value { - write!(f, "{}:{}", self.key, value) - } else { - write!(f, "{}", self.key) - } - } -} - -impl Attribute { - /// new constructs a new attribute - pub fn new(key: String, value: Option) -> Self { - Attribute { key, value } - } - - /// is_ice_candidate returns true if the attribute key equals "candidate". - pub fn is_ice_candidate(&self) -> bool { - self.key.as_str() == "candidate" - } -} diff --git a/sdp/src/description/description_test.rs b/sdp/src/description/description_test.rs deleted file mode 100644 index 58ba6a282..000000000 --- a/sdp/src/description/description_test.rs +++ /dev/null @@ -1,607 +0,0 @@ -use std::io::Cursor; - -use url::Url; - -use super::common::*; -use super::media::*; -use super::session::*; -use crate::error::{Error, Result}; - -const CANONICAL_MARSHAL_SDP: &str = "v=0\r\n\ - o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ - s=SDP Seminar\r\n\ - i=A Seminar on the session description protocol\r\n\ - u=http://www.example.com/seminars/sdp.pdf\r\n\ - e=j.doe@example.com (Jane Doe)\r\n\ - p=+1 617 555-6011\r\n\ - c=IN IP4 224.2.17.12/127\r\n\ - b=X-YZ:128\r\n\ - b=AS:12345\r\n\ - t=2873397496 2873404696\r\n\ - t=3034423619 3042462419\r\n\ - r=604800 3600 0 90000\r\n\ - z=2882844526 -3600 2898848070 0\r\n\ - k=prompt\r\n\ - a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ - a=recvonly\r\n\ - m=audio 49170 RTP/AVP 0\r\n\ - i=Vivamus a posuere nisl\r\n\ - c=IN IP4 203.0.113.1\r\n\ - b=X-YZ:128\r\n\ - k=prompt\r\n\ - a=sendrecv\r\n\ - m=video 51372 RTP/AVP 99\r\n\ - a=rtpmap:99 h263-1998/90000\r\n"; - -#[test] -fn test_unmarshal_marshal() -> Result<()> { - let input = CANONICAL_MARSHAL_SDP; - let mut reader = Cursor::new(input.as_bytes()); - let sdp = SessionDescription::unmarshal(&mut reader)?; - let output = sdp.marshal(); - assert_eq!(output, input); - - Ok(()) -} - -#[test] -fn test_marshal() -> Result<()> { - let sd = SessionDescription { - version: 0, - origin: Origin { - username: "jdoe".to_string(), - session_id: 2890844526, - session_version: 2890842807, - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - unicast_address: "10.47.16.5".to_string(), - }, - session_name: "SDP Seminar".to_string(), - session_information: Some("A Seminar on the session description protocol".to_string()), - uri: Some(Url::parse("http://www.example.com/seminars/sdp.pdf")?), - email_address: Some("j.doe@example.com (Jane Doe)".to_string()), - phone_number: Some("+1 617 555-6011".to_string()), - connection_information: Some(ConnectionInformation { - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - address: Some(Address { - address: "224.2.17.12".to_string(), - ttl: Some(127), - range: None, - }), - }), - bandwidth: vec![ - Bandwidth { - experimental: true, - bandwidth_type: "YZ".to_string(), - bandwidth: 128, - }, - Bandwidth { - experimental: false, - bandwidth_type: "AS".to_string(), - bandwidth: 12345, - }, - ], - time_descriptions: vec![ - TimeDescription { - timing: Timing { - start_time: 2873397496, - stop_time: 2873404696, - }, - repeat_times: vec![], - }, - TimeDescription { - timing: Timing { - start_time: 3034423619, - stop_time: 3042462419, - }, - repeat_times: vec![RepeatTime { - interval: 604800, - duration: 3600, - offsets: vec![0, 90000], - }], - }, - ], - time_zones: vec![ - TimeZone { - adjustment_time: 2882844526, - offset: -3600, - }, - TimeZone { - adjustment_time: 2898848070, - offset: 0, - }, - ], - encryption_key: Some("prompt".to_string()), - attributes: vec![ - Attribute::new( - "candidate".to_string(), - Some("0 1 UDP 2113667327 203.0.113.1 54400 typ host".to_string()), - ), - Attribute::new("recvonly".to_string(), None), - ], - media_descriptions: vec![ - MediaDescription { - media_name: MediaName { - media: "audio".to_string(), - port: RangedPort { - value: 49170, - range: None, - }, - protos: vec!["RTP".to_string(), "AVP".to_string()], - formats: vec!["0".to_string()], - }, - media_title: Some("Vivamus a posuere nisl".to_string()), - connection_information: Some(ConnectionInformation { - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - address: Some(Address { - address: "203.0.113.1".to_string(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![Bandwidth { - experimental: true, - bandwidth_type: "YZ".to_string(), - bandwidth: 128, - }], - encryption_key: Some("prompt".to_string()), - attributes: vec![Attribute::new("sendrecv".to_string(), None)], - }, - MediaDescription { - media_name: MediaName { - media: "video".to_string(), - port: RangedPort { - value: 51372, - range: None, - }, - protos: vec!["RTP".to_string(), "AVP".to_string()], - formats: vec!["99".to_string()], - }, - media_title: None, - connection_information: None, - bandwidth: vec![], - encryption_key: None, - attributes: vec![Attribute::new( - "rtpmap".to_string(), - Some("99 h263-1998/90000".to_string()), - )], - }, - ], - }; - - let actual = sd.marshal(); - assert!( - actual == CANONICAL_MARSHAL_SDP, - "error:\n\nEXPECTED:\n{CANONICAL_MARSHAL_SDP}\nACTUAL:\n{actual}!!!!\n" - ); - - Ok(()) -} - -const BASE_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n"; - -const SESSION_INFORMATION_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -i=A Seminar on the session description protocol\r\n\ -t=3034423619 3042462419\r\n"; - -// https://tools.ietf.org/html/rfc4566#section-5 -// Parsers SHOULD be tolerant and also accept records terminated -// with a single newline character. -const SESSION_INFORMATION_SDPLFONLY: &str = "v=0\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\n\ -s=SDP Seminar\n\ -i=A Seminar on the session description protocol\n\ -t=3034423619 3042462419\n"; - -// SessionInformationSDPCROnly = "v=0\r" + -// "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r" + -// "s=SDP Seminar\r" -// "i=A Seminar on the session description protocol\r" + -// "t=3034423619 3042462419\r" - -// Other SDP parsers (e.g. one in VLC media player) allow -// empty lines. -const SESSION_INFORMATION_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -\r\n\ -s=SDP Seminar\r\n\ -\r\n\ -i=A Seminar on the session description protocol\r\n\ -\r\n\ -t=3034423619 3042462419\r\n\ -\r\n"; - -const URI_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -u=http://www.example.com/seminars/sdp.pdf\r\n\ -t=3034423619 3042462419\r\n"; - -const EMAIL_ADDRESS_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -e=j.doe@example.com (Jane Doe)\r\n\ -t=3034423619 3042462419\r\n"; - -const PHONE_NUMBER_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -p=+1 617 555-6011\r\n\ -t=3034423619 3042462419\r\n"; - -const SESSION_CONNECTION_INFORMATION_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -c=IN IP4 224.2.17.12/127\r\n\ -t=3034423619 3042462419\r\n"; - -const SESSION_BANDWIDTH_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -b=X-YZ:128\r\n\ -b=AS:12345\r\n\ -t=3034423619 3042462419\r\n"; - -const TIMING_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n"; - -// Short hand time notation is converted into NTP timestamp format in -// seconds. Because of that unittest comparisons will fail as the same time -// will be expressed in different units. -const REPEAT_TIMES_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=604800 3600 0 90000\r\n\ -r=3d 2h 0 21h\r\n"; - -const REPEAT_TIMES_SDPEXPECTED: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=604800 3600 0 90000\r\n\ -r=259200 7200 0 75600\r\n"; - -const REPEAT_TIMES_OVERFLOW_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=604800 3600 0 90000\r\n\ -r=106751991167301d 2h 0 21h\r\n"; - -const REPEAT_TIMES_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=604800 3600 0 90000\r\n\ -r=259200 7200 0 75600\r\n\ -\r\n"; - -// The expected value looks a bit different for the same reason as mentioned -// above regarding RepeatTimes. -const TIME_ZONES_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=2882844526 -1h 2898848070 0\r\n"; - -const TIME_ZONES_SDPEXPECTED: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -r=2882844526 -3600 2898848070 0\r\n"; - -const TIME_ZONES_SDP2: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -z=2882844526 -3600 2898848070 0\r\n"; - -const TIME_ZONES_SDP2EXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -z=2882844526 -3600 2898848070 0\r\n\ -\r\n"; - -const SESSION_ENCRYPTION_KEY_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -k=prompt\r\n"; - -const SESSION_ENCRYPTION_KEY_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -k=prompt\r\n -\r\n"; - -const SESSION_ATTRIBUTES_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -a=rtpmap:96 opus/48000\r\n"; - -const MEDIA_NAME_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n"; - -const MEDIA_NAME_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n -\r\n"; - -const MEDIA_TITLE_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -i=Vivamus a posuere nisl\r\n"; - -const MEDIA_CONNECTION_INFORMATION_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -c=IN IP4 203.0.113.1\r\n"; - -const MEDIA_CONNECTION_INFORMATION_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -c=IN IP4 203.0.113.1\r\n\ -\r\n"; - -const MEDIA_DESCRIPTION_OUT_OF_ORDER_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -a=rtpmap:99 h263-1998/90000\r\n\ -a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ -c=IN IP4 203.0.113.1\r\n\ -i=Vivamus a posuere nisl\r\n"; - -const MEDIA_DESCRIPTION_OUT_OF_ORDER_SDPACTUAL: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -i=Vivamus a posuere nisl\r\n\ -c=IN IP4 203.0.113.1\r\n\ -a=rtpmap:99 h263-1998/90000\r\n\ -a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n"; - -const MEDIA_BANDWIDTH_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -b=X-YZ:128\r\n\ -b=AS:12345\r\n"; - -const MEDIA_TRANSPORT_BANDWIDTH_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -b=AS:12345\r\n\ -b=TIAS:12345\r\n"; - -const MEDIA_ENCRYPTION_KEY_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -k=prompt\r\n"; - -const MEDIA_ENCRYPTION_KEY_SDPEXTRA_CRLF: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -k=prompt\r\n\ -\r\n"; - -const MEDIA_ATTRIBUTES_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -t=2873397496 2873404696\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -m=audio 54400 RTP/SAVPF 0 96\r\n\ -a=rtpmap:99 h263-1998/90000\r\n\ -a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ -a=rtcp-fb:97 ccm fir\r\n\ -a=rtcp-fb:97 nack\r\n\ -a=rtcp-fb:97 nack pli\r\n"; - -const CANONICAL_UNMARSHAL_SDP: &str = "v=0\r\n\ -o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n\ -s=SDP Seminar\r\n\ -i=A Seminar on the session description protocol\r\n\ -u=http://www.example.com/seminars/sdp.pdf\r\n\ -e=j.doe@example.com (Jane Doe)\r\n\ -p=+1 617 555-6011\r\n\ -c=IN IP4 224.2.17.12/127\r\n\ -b=X-YZ:128\r\n\ -b=AS:12345\r\n\ -t=2873397496 2873404696\r\n\ -t=3034423619 3042462419\r\n\ -r=604800 3600 0 90000\r\n\ -z=2882844526 -3600 2898848070 0\r\n\ -k=prompt\r\n\ -a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ -a=recvonly\r\n\ -m=audio 49170 RTP/AVP 0\r\n\ -i=Vivamus a posuere nisl\r\n\ -c=IN IP4 203.0.113.1\r\n\ -b=X-YZ:128\r\n\ -k=prompt\r\n\ -a=sendrecv\r\n\ -m=video 51372 RTP/AVP 99\r\n\ -a=rtpmap:99 h263-1998/90000\r\n"; - -#[test] -fn test_round_trip() -> Result<()> { - let tests = vec![ - ( - "SessionInformationSDPLFOnly", - SESSION_INFORMATION_SDPLFONLY, - Some(SESSION_INFORMATION_SDP), - ), - ( - "SessionInformationSDPExtraCRLF", - SESSION_INFORMATION_SDPEXTRA_CRLF, - Some(SESSION_INFORMATION_SDP), - ), - ("SessionInformation", SESSION_INFORMATION_SDP, None), - ("URI", URI_SDP, None), - ("EmailAddress", EMAIL_ADDRESS_SDP, None), - ("PhoneNumber", PHONE_NUMBER_SDP, None), - ( - "RepeatTimesSDPExtraCRLF", - REPEAT_TIMES_SDPEXTRA_CRLF, - Some(REPEAT_TIMES_SDPEXPECTED), - ), - ( - "SessionConnectionInformation", - SESSION_CONNECTION_INFORMATION_SDP, - None, - ), - ("SessionBandwidth", SESSION_BANDWIDTH_SDP, None), - ("SessionEncryptionKey", SESSION_ENCRYPTION_KEY_SDP, None), - ( - "SessionEncryptionKeyExtraCRLF", - SESSION_ENCRYPTION_KEY_SDPEXTRA_CRLF, - Some(SESSION_ENCRYPTION_KEY_SDP), - ), - ("SessionAttributes", SESSION_ATTRIBUTES_SDP, None), - ( - "TimeZonesSDP2ExtraCRLF", - TIME_ZONES_SDP2EXTRA_CRLF, - Some(TIME_ZONES_SDP2), - ), - ("MediaName", MEDIA_NAME_SDP, None), - ( - "MediaNameExtraCRLF", - MEDIA_NAME_SDPEXTRA_CRLF, - Some(MEDIA_NAME_SDP), - ), - ("MediaTitle", MEDIA_TITLE_SDP, None), - ( - "MediaConnectionInformation", - MEDIA_CONNECTION_INFORMATION_SDP, - None, - ), - ( - "MediaConnectionInformationExtraCRLF", - MEDIA_CONNECTION_INFORMATION_SDPEXTRA_CRLF, - Some(MEDIA_CONNECTION_INFORMATION_SDP), - ), - ( - "MediaDescriptionOutOfOrder", - MEDIA_DESCRIPTION_OUT_OF_ORDER_SDP, - Some(MEDIA_DESCRIPTION_OUT_OF_ORDER_SDPACTUAL), - ), - ("MediaBandwidth", MEDIA_BANDWIDTH_SDP, None), - ( - "MediaTransportBandwidth", - MEDIA_TRANSPORT_BANDWIDTH_SDP, - None, - ), - ("MediaEncryptionKey", MEDIA_ENCRYPTION_KEY_SDP, None), - ( - "MediaEncryptionKeyExtraCRLF", - MEDIA_ENCRYPTION_KEY_SDPEXTRA_CRLF, - Some(MEDIA_ENCRYPTION_KEY_SDP), - ), - ("MediaAttributes", MEDIA_ATTRIBUTES_SDP, None), - ("CanonicalUnmarshal", CANONICAL_UNMARSHAL_SDP, None), - ]; - - for (name, sdp_str, expected) in tests { - let mut reader = Cursor::new(sdp_str.as_bytes()); - let sdp = SessionDescription::unmarshal(&mut reader); - if let Ok(sdp) = sdp { - let actual = sdp.marshal(); - if let Some(expected) = expected { - assert_eq!(actual.as_str(), expected, "{name}\n{sdp_str}"); - } else { - assert_eq!(actual.as_str(), sdp_str, "{name}\n{sdp_str}"); - } - } else { - panic!("{name}\n{sdp_str}"); - } - } - - Ok(()) -} - -#[test] -fn test_unmarshal_repeat_times() -> Result<()> { - let mut reader = Cursor::new(REPEAT_TIMES_SDP.as_bytes()); - let sdp = SessionDescription::unmarshal(&mut reader)?; - let actual = sdp.marshal(); - assert_eq!(actual.as_str(), REPEAT_TIMES_SDPEXPECTED); - Ok(()) -} - -#[test] -fn test_unmarshal_repeat_times_overflow() -> Result<()> { - let mut reader = Cursor::new(REPEAT_TIMES_OVERFLOW_SDP.as_bytes()); - let result = SessionDescription::unmarshal(&mut reader); - assert!(result.is_err()); - assert_eq!( - Error::SdpInvalidValue("106751991167301d".to_owned()), - result.unwrap_err() - ); - Ok(()) -} - -#[test] -fn test_unmarshal_time_zones() -> Result<()> { - let mut reader = Cursor::new(TIME_ZONES_SDP.as_bytes()); - let sdp = SessionDescription::unmarshal(&mut reader)?; - let actual = sdp.marshal(); - assert_eq!(actual.as_str(), TIME_ZONES_SDPEXPECTED); - Ok(()) -} - -#[test] -fn test_unmarshal_non_nil_address() -> Result<()> { - let input = "v=0\r\no=0 0 0 IN IP4 0\r\ns=0\r\nc=IN IP4\r\nt=0 0\r\n"; - let mut reader = Cursor::new(input); - let sdp = SessionDescription::unmarshal(&mut reader); - if let Ok(sdp) = sdp { - let output = sdp.marshal(); - assert_eq!(output.as_str(), input); - } else { - panic!("{}", input); - } - Ok(()) -} diff --git a/sdp/src/description/media.rs b/sdp/src/description/media.rs deleted file mode 100644 index d6c973cc2..000000000 --- a/sdp/src/description/media.rs +++ /dev/null @@ -1,270 +0,0 @@ -use std::collections::HashMap; -use std::fmt; - -use url::Url; - -use crate::description::common::*; -use crate::extmap::*; - -/// Constants for extmap key -pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: isize = 3; -pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str = - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; - -fn ext_map_uri() -> HashMap { - let mut m = HashMap::new(); - m.insert( - EXT_MAP_VALUE_TRANSPORT_CC_KEY, - EXT_MAP_VALUE_TRANSPORT_CC_URI, - ); - m -} - -/// MediaDescription represents a media type. -/// -#[derive(Debug, Default, Clone)] -pub struct MediaDescription { - /// `m= / ...` - /// - /// - pub media_name: MediaName, - - /// `i=` - /// - /// - pub media_title: Option, - - /// `c= ` - /// - /// - pub connection_information: Option, - - /// `b=:` - /// - /// - pub bandwidth: Vec, - - /// `k=` - /// - /// `k=:` - /// - /// - pub encryption_key: Option, - - /// Attributes are the primary means for extending SDP. Attributes may - /// be defined to be used as "session-level" attributes, "media-level" - /// attributes, or both. - /// - /// - pub attributes: Vec, -} - -impl MediaDescription { - /// attribute returns the value of an attribute and if it exists - pub fn attribute(&self, key: &str) -> Option> { - for a in &self.attributes { - if a.key == key { - return Some(a.value.as_ref().map(|s| s.as_ref())); - } - } - None - } - - /// new_jsep_media_description creates a new MediaName with - /// some settings that are required by the JSEP spec. - pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self { - MediaDescription { - media_name: MediaName { - media: codec_type, - port: RangedPort { - value: 9, - range: None, - }, - protos: vec![ - "UDP".to_string(), - "TLS".to_string(), - "RTP".to_string(), - "SAVPF".to_string(), - ], - formats: vec![], - }, - media_title: None, - connection_information: Some(ConnectionInformation { - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - address: Some(Address { - address: "0.0.0.0".to_string(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - } - } - - /// with_property_attribute adds a property attribute 'a=key' to the media description - pub fn with_property_attribute(mut self, key: String) -> Self { - self.attributes.push(Attribute::new(key, None)); - self - } - - /// with_value_attribute adds a value attribute 'a=key:value' to the media description - pub fn with_value_attribute(mut self, key: String, value: String) -> Self { - self.attributes.push(Attribute::new(key, Some(value))); - self - } - - /// with_fingerprint adds a fingerprint to the media description - pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { - self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value) - } - - /// with_ice_credentials adds ICE credentials to the media description - pub fn with_ice_credentials(self, username: String, password: String) -> Self { - self.with_value_attribute("ice-ufrag".to_string(), username) - .with_value_attribute("ice-pwd".to_string(), password) - } - - /// with_codec adds codec information to the media description - pub fn with_codec( - mut self, - payload_type: u8, - name: String, - clockrate: u32, - channels: u16, - fmtp: String, - ) -> Self { - self.media_name.formats.push(payload_type.to_string()); - let mut rtpmap = format!("{payload_type} {name}/{clockrate}"); - if channels > 0 { - rtpmap += format!("/{channels}").as_str(); - } - - if !fmtp.is_empty() { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}")) - } else { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - } - } - - /// with_media_source adds media source information to the media description - pub fn with_media_source( - self, - ssrc: u32, - cname: String, - stream_label: String, - label: String, - ) -> Self { - self. - with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_label} {label}")). - with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_label}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{label}")) - // Deprecated but not phased out? - } - - /// with_candidate adds an ICE candidate to the media description - /// Deprecated: use WithICECandidate instead - pub fn with_candidate(self, value: String) -> Self { - self.with_value_attribute("candidate".to_string(), value) - } - - pub fn with_extmap(self, e: ExtMap) -> Self { - self.with_property_attribute(e.marshal()) - } - - /// with_transport_cc_extmap adds an extmap to the media description - pub fn with_transport_cc_extmap(self) -> Self { - let uri = { - let m = ext_map_uri(); - if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) { - match Url::parse(uri_str) { - Ok(uri) => Some(uri), - Err(_) => None, - } - } else { - None - } - }; - - let e = ExtMap { - value: EXT_MAP_VALUE_TRANSPORT_CC_KEY, - uri, - ..Default::default() - }; - - self.with_extmap(e) - } -} - -/// RangedPort supports special format for the media field "m=" port value. If -/// it may be necessary to specify multiple transport ports, the protocol allows -/// to write it as: `/` where number of ports is a an -/// offsetting range. -#[derive(Debug, Default, Clone)] -pub struct RangedPort { - pub value: isize, - pub range: Option, -} - -impl fmt::Display for RangedPort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(range) = self.range { - write!(f, "{}/{}", self.value, range) - } else { - write!(f, "{}", self.value) - } - } -} - -/// MediaName describes the "m=" field storage structure. -#[derive(Debug, Default, Clone)] -pub struct MediaName { - pub media: String, - pub port: RangedPort, - pub protos: Vec, - pub formats: Vec, -} - -impl fmt::Display for MediaName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = [ - self.media.clone(), - self.port.to_string(), - self.protos.join("/"), - self.formats.join(" "), - ]; - write!(f, "{}", s.join(" ")) - } -} - -#[cfg(test)] -mod tests { - use super::MediaDescription; - - #[test] - fn test_attribute_missing() { - let media_description = MediaDescription::default(); - - assert_eq!(media_description.attribute("recvonly"), None); - } - - #[test] - fn test_attribute_present_with_no_value() { - let media_description = - MediaDescription::default().with_property_attribute("recvonly".to_owned()); - - assert_eq!(media_description.attribute("recvonly"), Some(None)); - } - - #[test] - fn test_attribute_present_with_value() { - let media_description = - MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned()); - - assert_eq!(media_description.attribute("ptime"), Some(Some("1"))); - } -} diff --git a/sdp/src/description/mod.rs b/sdp/src/description/mod.rs deleted file mode 100644 index 0cab15c04..000000000 --- a/sdp/src/description/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[cfg(test)] -mod description_test; - -pub mod common; -pub mod media; -pub mod session; diff --git a/sdp/src/description/session.rs b/sdp/src/description/session.rs deleted file mode 100644 index 472fae8c1..000000000 --- a/sdp/src/description/session.rs +++ /dev/null @@ -1,1365 +0,0 @@ -use std::collections::HashMap; -use std::convert::TryFrom; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use std::{fmt, io}; - -use url::Url; - -use super::common::*; -use super::media::*; -use crate::error::{Error, Result}; -use crate::lexer::*; -use crate::util::*; - -/// Constants for SDP attributes used in JSEP -pub const ATTR_KEY_CANDIDATE: &str = "candidate"; -pub const ATTR_KEY_END_OF_CANDIDATES: &str = "end-of-candidates"; -pub const ATTR_KEY_IDENTITY: &str = "identity"; -pub const ATTR_KEY_GROUP: &str = "group"; -pub const ATTR_KEY_SSRC: &str = "ssrc"; -pub const ATTR_KEY_SSRCGROUP: &str = "ssrc-group"; -pub const ATTR_KEY_MSID: &str = "msid"; -pub const ATTR_KEY_MSID_SEMANTIC: &str = "msid-semantic"; -pub const ATTR_KEY_CONNECTION_SETUP: &str = "setup"; -pub const ATTR_KEY_MID: &str = "mid"; -pub const ATTR_KEY_ICELITE: &str = "ice-lite"; -pub const ATTR_KEY_RTCPMUX: &str = "rtcp-mux"; -pub const ATTR_KEY_RTCPRSIZE: &str = "rtcp-rsize"; -pub const ATTR_KEY_INACTIVE: &str = "inactive"; -pub const ATTR_KEY_RECV_ONLY: &str = "recvonly"; -pub const ATTR_KEY_SEND_ONLY: &str = "sendonly"; -pub const ATTR_KEY_SEND_RECV: &str = "sendrecv"; -pub const ATTR_KEY_EXT_MAP: &str = "extmap"; - -/// Constants for semantic tokens used in JSEP -pub const SEMANTIC_TOKEN_LIP_SYNCHRONIZATION: &str = "LS"; -pub const SEMANTIC_TOKEN_FLOW_IDENTIFICATION: &str = "FID"; -pub const SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION: &str = "FEC"; -pub const SEMANTIC_TOKEN_WEBRTC_MEDIA_STREAMS: &str = "WMS"; - -/// Version describes the value provided by the "v=" field which gives -/// the version of the Session Description Protocol. -pub type Version = isize; - -/// Origin defines the structure for the "o=" field which provides the -/// originator of the session plus a session identifier and version number. -#[derive(Debug, Default, Clone)] -pub struct Origin { - pub username: String, - pub session_id: u64, - pub session_version: u64, - pub network_type: String, - pub address_type: String, - pub unicast_address: String, -} - -impl fmt::Display for Origin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {} {} {} {}", - self.username, - self.session_id, - self.session_version, - self.network_type, - self.address_type, - self.unicast_address, - ) - } -} - -impl Origin { - pub fn new() -> Self { - Origin { - username: "".to_owned(), - session_id: 0, - session_version: 0, - network_type: "".to_owned(), - address_type: "".to_owned(), - unicast_address: "".to_owned(), - } - } -} - -/// SessionName describes a structured representations for the "s=" field -/// and is the textual session name. -pub type SessionName = String; - -/// EmailAddress describes a structured representations for the "e=" line -/// which specifies email contact information for the person responsible for -/// the conference. -pub type EmailAddress = String; - -/// PhoneNumber describes a structured representations for the "p=" line -/// specify phone contact information for the person responsible for the -/// conference. -pub type PhoneNumber = String; - -/// TimeZone defines the structured object for "z=" line which describes -/// repeated sessions scheduling. -#[derive(Debug, Default, Clone)] -pub struct TimeZone { - pub adjustment_time: u64, - pub offset: i64, -} - -impl fmt::Display for TimeZone { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.adjustment_time, self.offset) - } -} - -/// TimeDescription describes "t=", "r=" fields of the session description -/// which are used to specify the start and stop times for a session as well as -/// repeat intervals and durations for the scheduled session. -#[derive(Debug, Default, Clone)] -pub struct TimeDescription { - /// `t= ` - /// - /// - pub timing: Timing, - - /// `r= ` - /// - /// - pub repeat_times: Vec, -} - -/// Timing defines the "t=" field's structured representation for the start and -/// stop times. -#[derive(Debug, Default, Clone)] -pub struct Timing { - pub start_time: u64, - pub stop_time: u64, -} - -impl fmt::Display for Timing { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.start_time, self.stop_time) - } -} - -/// RepeatTime describes the "r=" fields of the session description which -/// represents the intervals and durations for repeated scheduled sessions. -#[derive(Debug, Default, Clone)] -pub struct RepeatTime { - pub interval: i64, - pub duration: i64, - pub offsets: Vec, -} - -impl fmt::Display for RepeatTime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut fields = vec![format!("{}", self.interval), format!("{}", self.duration)]; - for value in &self.offsets { - fields.push(format!("{value}")); - } - write!(f, "{}", fields.join(" ")) - } -} - -/// SessionDescription is a a well-defined format for conveying sufficient -/// information to discover and participate in a multimedia session. -#[derive(Debug, Default, Clone)] -pub struct SessionDescription { - /// `v=0` - /// - /// - pub version: Version, - - /// `o= ` - /// - /// - pub origin: Origin, - - /// `s=` - /// - /// - pub session_name: SessionName, - - /// `i=` - /// - /// - pub session_information: Option, - - /// `u=` - /// - /// - pub uri: Option, - - /// `e=` - /// - /// - pub email_address: Option, - - /// `p=` - /// - /// - pub phone_number: Option, - - /// `c= ` - /// - /// - pub connection_information: Option, - - /// `b=:` - /// - /// - pub bandwidth: Vec, - - /// - /// - pub time_descriptions: Vec, - - /// `z= ...` - /// - /// - pub time_zones: Vec, - - /// `k=` - /// - /// `k=:` - /// - /// - pub encryption_key: Option, - - /// `a=` - /// - /// `a=:` - /// - /// - pub attributes: Vec, - - /// - pub media_descriptions: Vec, -} - -/// Reset cleans the SessionDescription, and sets all fields back to their default values -impl SessionDescription { - /// API to match draft-ietf-rtcweb-jsep - /// Move to webrtc or its own package? - - /// NewJSEPSessionDescription creates a new SessionDescription with - /// some settings that are required by the JSEP spec. - pub fn new_jsep_session_description(identity: bool) -> Self { - let d = SessionDescription { - version: 0, - origin: Origin { - username: "-".to_string(), - session_id: new_session_id(), - session_version: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .subsec_nanos() as u64, - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - unicast_address: "0.0.0.0".to_string(), - }, - session_name: "-".to_string(), - session_information: None, - uri: None, - email_address: None, - phone_number: None, - connection_information: None, - bandwidth: vec![], - time_descriptions: vec![TimeDescription { - timing: Timing { - start_time: 0, - stop_time: 0, - }, - repeat_times: vec![], - }], - time_zones: vec![], - encryption_key: None, - attributes: vec![], // TODO: implement trickle ICE - media_descriptions: vec![], - }; - - if identity { - d.with_property_attribute(ATTR_KEY_IDENTITY.to_string()) - } else { - d - } - } - - /// WithPropertyAttribute adds a property attribute 'a=key' to the session description - pub fn with_property_attribute(mut self, key: String) -> Self { - self.attributes.push(Attribute::new(key, None)); - self - } - - /// WithValueAttribute adds a value attribute 'a=key:value' to the session description - pub fn with_value_attribute(mut self, key: String, value: String) -> Self { - self.attributes.push(Attribute::new(key, Some(value))); - self - } - - /// WithFingerprint adds a fingerprint to the session description - pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { - self.with_value_attribute("fingerprint".to_string(), algorithm + " " + value.as_str()) - } - - /// WithMedia adds a media description to the session description - pub fn with_media(mut self, md: MediaDescription) -> Self { - self.media_descriptions.push(md); - self - } - - fn build_codec_map(&self) -> HashMap { - let mut codecs: HashMap = HashMap::new(); - - for m in &self.media_descriptions { - for a in &m.attributes { - let attr = a.to_string(); - if attr.starts_with("rtpmap:") { - if let Ok(codec) = parse_rtpmap(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("fmtp:") { - if let Ok(codec) = parse_fmtp(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("rtcp-fb:") { - if let Ok(codec) = parse_rtcp_fb(&attr) { - merge_codecs(codec, &mut codecs); - } - } - } - } - - codecs - } - - /// get_codec_for_payload_type scans the SessionDescription for the given payload type and returns the codec - pub fn get_codec_for_payload_type(&self, payload_type: u8) -> Result { - let codecs = self.build_codec_map(); - - if let Some(codec) = codecs.get(&payload_type) { - Ok(codec.clone()) - } else { - Err(Error::PayloadTypeNotFound) - } - } - - /// get_payload_type_for_codec scans the SessionDescription for a codec that matches the provided codec - /// as closely as possible and returns its payload type - pub fn get_payload_type_for_codec(&self, wanted: &Codec) -> Result { - let codecs = self.build_codec_map(); - - for (payload_type, codec) in codecs.iter() { - if codecs_match(wanted, codec) { - return Ok(*payload_type); - } - } - - Err(Error::CodecNotFound) - } - - /// Attribute returns the value of an attribute and if it exists - pub fn attribute(&self, key: &str) -> Option<&String> { - for a in &self.attributes { - if a.key == key { - return a.value.as_ref(); - } - } - None - } - - /// Marshal takes a SDP struct to text - /// - /// - /// - /// Session description - /// v= (protocol version) - /// o= (originator and session identifier) - /// s= (session name) - /// i=* (session information) - /// u=* (URI of description) - /// e=* (email address) - /// p=* (phone number) - /// c=* (connection information -- not required if included in - /// all media) - /// b=* (zero or more bandwidth information lines) - /// One or more time descriptions ("t=" and "r=" lines; see below) - /// z=* (time zone adjustments) - /// k=* (encryption key) - /// a=* (zero or more session attribute lines) - /// Zero or more media descriptions - /// - /// Time description - /// t= (time the session is active) - /// r=* (zero or more repeat times) - /// - /// Media description, if present - /// m= (media name and transport address) - /// i=* (media title) - /// c=* (connection information -- optional if included at - /// session level) - /// b=* (zero or more bandwidth information lines) - /// k=* (encryption key) - /// a=* (zero or more media attribute lines) - pub fn marshal(&self) -> String { - let mut result = String::new(); - - result += key_value_build("v=", Some(&self.version.to_string())).as_str(); - result += key_value_build("o=", Some(&self.origin.to_string())).as_str(); - result += key_value_build("s=", Some(&self.session_name)).as_str(); - - result += key_value_build("i=", self.session_information.as_ref()).as_str(); - - if let Some(uri) = &self.uri { - result += key_value_build("u=", Some(&format!("{uri}"))).as_str(); - } - result += key_value_build("e=", self.email_address.as_ref()).as_str(); - result += key_value_build("p=", self.phone_number.as_ref()).as_str(); - if let Some(connection_information) = &self.connection_information { - result += key_value_build("c=", Some(&connection_information.to_string())).as_str(); - } - - for bandwidth in &self.bandwidth { - result += key_value_build("b=", Some(&bandwidth.to_string())).as_str(); - } - for time_description in &self.time_descriptions { - result += key_value_build("t=", Some(&time_description.timing.to_string())).as_str(); - for repeat_time in &time_description.repeat_times { - result += key_value_build("r=", Some(&repeat_time.to_string())).as_str(); - } - } - if !self.time_zones.is_empty() { - let mut time_zones = vec![]; - for time_zone in &self.time_zones { - time_zones.push(time_zone.to_string()); - } - result += key_value_build("z=", Some(&time_zones.join(" "))).as_str(); - } - result += key_value_build("k=", self.encryption_key.as_ref()).as_str(); - for attribute in &self.attributes { - result += key_value_build("a=", Some(&attribute.to_string())).as_str(); - } - - for media_description in &self.media_descriptions { - result += - key_value_build("m=", Some(&media_description.media_name.to_string())).as_str(); - result += key_value_build("i=", media_description.media_title.as_ref()).as_str(); - if let Some(connection_information) = &media_description.connection_information { - result += key_value_build("c=", Some(&connection_information.to_string())).as_str(); - } - for bandwidth in &media_description.bandwidth { - result += key_value_build("b=", Some(&bandwidth.to_string())).as_str(); - } - result += key_value_build("k=", media_description.encryption_key.as_ref()).as_str(); - for attribute in &media_description.attributes { - result += key_value_build("a=", Some(&attribute.to_string())).as_str(); - } - } - - result - } - - /// Unmarshal is the primary function that deserializes the session description - /// message and stores it inside of a structured SessionDescription object. - /// - /// The States Transition Table describes the computation flow between functions - /// (namely s1, s2, s3, ...) for a parsing procedure that complies with the - /// specifications laid out by the rfc4566#section-5 as well as by JavaScript - /// Session Establishment Protocol draft. Links: - /// - /// - /// - /// - /// - /// Session description - /// v= (protocol version) - /// o= (originator and session identifier) - /// s= (session name) - /// i=* (session information) - /// u=* (URI of description) - /// e=* (email address) - /// p=* (phone number) - /// c=* (connection information -- not required if included in - /// all media) - /// b=* (zero or more bandwidth information lines) - /// One or more time descriptions ("t=" and "r=" lines; see below) - /// z=* (time zone adjustments) - /// k=* (encryption key) - /// a=* (zero or more session attribute lines) - /// Zero or more media descriptions - /// - /// Time description - /// t= (time the session is active) - /// r=* (zero or more repeat times) - /// - /// Media description, if present - /// m= (media name and transport address) - /// i=* (media title) - /// c=* (connection information -- optional if included at - /// session level) - /// b=* (zero or more bandwidth information lines) - /// k=* (encryption key) - /// a=* (zero or more media attribute lines) - /// - /// In order to generate the following state table and draw subsequent - /// deterministic finite-state automota ("DFA") the following regex was used to - /// derive the DFA: - /// vosi?u?e?p?c?b*(tr*)+z?k?a*(mi?c?b*k?a*)* - /// possible place and state to exit: - /// ** * * * ** * * * * - /// 99 1 1 1 11 1 1 1 1 - /// 3 1 1 26 5 5 4 4 - /// - /// Please pay close attention to the `k`, and `a` parsing states. In the table - /// below in order to distinguish between the states belonging to the media - /// description as opposed to the session description, the states are marked - /// with an asterisk ("a*", "k*"). - /// - /// ```ignore - /// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ - /// | STATES | a* | a*,k* | a | a,k | b | b,c | e | i | m | o | p | r,t | s | t | u | v | z | - /// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ - /// | s1 | | | | | | | | | | | | | | | | 2 | | - /// | s2 | | | | | | | | | | 3 | | | | | | | | - /// | s3 | | | | | | | | | | | | | 4 | | | | | - /// | s4 | | | | | | 5 | 6 | 7 | | | 8 | | | 9 | 10 | | | - /// | s5 | | | | | 5 | | | | | | | | | 9 | | | | - /// | s6 | | | | | | 5 | | | | | 8 | | | 9 | | | | - /// | s7 | | | | | | 5 | 6 | | | | 8 | | | 9 | 10 | | | - /// | s8 | | | | | | 5 | | | | | | | | 9 | | | | - /// | s9 | | | | 11 | | | | | 12 | | | 9 | | | | | 13 | - /// | s10 | | | | | | 5 | 6 | | | | 8 | | | 9 | | | | - /// | s11 | | | 11 | | | | | | 12 | | | | | | | | | - /// | s12 | | 14 | | | | 15 | | 16 | 12 | | | | | | | | | - /// | s13 | | | | 11 | | | | | 12 | | | | | | | | | - /// | s14 | 14 | | | | | | | | 12 | | | | | | | | | - /// | s15 | | 14 | | | 15 | | | | 12 | | | | | | | | | - /// | s16 | | 14 | | | | 15 | | | 12 | | | | | | | | | - /// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ - /// ``` - pub fn unmarshal(reader: &mut R) -> Result { - let mut lexer = Lexer { - desc: SessionDescription { - version: 0, - origin: Origin::new(), - session_name: "".to_owned(), - session_information: None, - uri: None, - email_address: None, - phone_number: None, - connection_information: None, - bandwidth: vec![], - time_descriptions: vec![], - time_zones: vec![], - encryption_key: None, - attributes: vec![], - media_descriptions: vec![], - }, - reader, - }; - - let mut state = Some(StateFn { f: s1 }); - while let Some(s) = state { - state = (s.f)(&mut lexer)?; - } - - Ok(lexer.desc) - } -} - -impl From for String { - fn from(sdp: SessionDescription) -> String { - sdp.marshal() - } -} - -impl TryFrom for SessionDescription { - type Error = Error; - fn try_from(sdp_string: String) -> Result { - let mut reader = io::Cursor::new(sdp_string.as_bytes()); - let session_description = SessionDescription::unmarshal(&mut reader)?; - Ok(session_description) - } -} - -fn s1<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - if &key == b"v=" { - return Ok(Some(StateFn { - f: unmarshal_protocol_version, - })); - } - - Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)) -} - -fn s2<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - if &key == b"o=" { - return Ok(Some(StateFn { - f: unmarshal_origin, - })); - } - - Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)) -} - -fn s3<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - if &key == b"s=" { - return Ok(Some(StateFn { - f: unmarshal_session_name, - })); - } - - Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)) -} - -fn s4<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"i=" => Ok(Some(StateFn { - f: unmarshal_session_information, - })), - b"u=" => Ok(Some(StateFn { f: unmarshal_uri })), - b"e=" => Ok(Some(StateFn { f: unmarshal_email })), - b"p=" => Ok(Some(StateFn { f: unmarshal_phone })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_session_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s5<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s6<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"p=" => Ok(Some(StateFn { f: unmarshal_phone })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_session_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s7<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"u=" => Ok(Some(StateFn { f: unmarshal_uri })), - b"e=" => Ok(Some(StateFn { f: unmarshal_email })), - b"p=" => Ok(Some(StateFn { f: unmarshal_phone })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_session_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s8<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"c=" => Ok(Some(StateFn { - f: unmarshal_session_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s9<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"z=" => Ok(Some(StateFn { - f: unmarshal_time_zones, - })), - b"k=" => Ok(Some(StateFn { - f: unmarshal_session_encryption_key, - })), - b"a=" => Ok(Some(StateFn { - f: unmarshal_session_attribute, - })), - b"r=" => Ok(Some(StateFn { - f: unmarshal_repeat_times, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s10<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, _) = read_type(lexer.reader)?; - match key.as_slice() { - b"e=" => Ok(Some(StateFn { f: unmarshal_email })), - b"p=" => Ok(Some(StateFn { f: unmarshal_phone })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_session_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_session_bandwidth, - })), - b"t=" => Ok(Some(StateFn { - f: unmarshal_timing, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s11<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_session_attribute, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s12<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_media_attribute, - })), - b"k=" => Ok(Some(StateFn { - f: unmarshal_media_encryption_key, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_media_bandwidth, - })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_media_connection_information, - })), - b"i=" => Ok(Some(StateFn { - f: unmarshal_media_title, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s13<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_session_attribute, - })), - b"k=" => Ok(Some(StateFn { - f: unmarshal_session_encryption_key, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s14<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_media_attribute, - })), - // Non-spec ordering - b"k=" => Ok(Some(StateFn { - f: unmarshal_media_encryption_key, - })), - // Non-spec ordering - b"b=" => Ok(Some(StateFn { - f: unmarshal_media_bandwidth, - })), - // Non-spec ordering - b"c=" => Ok(Some(StateFn { - f: unmarshal_media_connection_information, - })), - // Non-spec ordering - b"i=" => Ok(Some(StateFn { - f: unmarshal_media_title, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s15<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_media_attribute, - })), - b"k=" => Ok(Some(StateFn { - f: unmarshal_media_encryption_key, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_media_bandwidth, - })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_media_connection_information, - })), - // Non-spec ordering - b"i=" => Ok(Some(StateFn { - f: unmarshal_media_title, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn s16<'a, R: io::BufRead + io::Seek>(lexer: &mut Lexer<'a, R>) -> Result>> { - let (key, num_bytes) = read_type(lexer.reader)?; - if key.is_empty() && num_bytes == 0 { - return Ok(None); - } - - match key.as_slice() { - b"a=" => Ok(Some(StateFn { - f: unmarshal_media_attribute, - })), - b"k=" => Ok(Some(StateFn { - f: unmarshal_media_encryption_key, - })), - b"c=" => Ok(Some(StateFn { - f: unmarshal_media_connection_information, - })), - b"b=" => Ok(Some(StateFn { - f: unmarshal_media_bandwidth, - })), - // Non-spec ordering - b"i=" => Ok(Some(StateFn { - f: unmarshal_media_title, - })), - b"m=" => Ok(Some(StateFn { - f: unmarshal_media_description, - })), - _ => Err(Error::SdpInvalidSyntax(String::from_utf8(key)?)), - } -} - -fn unmarshal_protocol_version<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let version = value.parse::()?; - - // As off the latest draft of the rfc this value is required to be 0. - // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-5.8.1 - if version != 0 { - return Err(Error::SdpInvalidSyntax(value)); - } - - Ok(Some(StateFn { f: s2 })) -} - -fn unmarshal_origin<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() != 6 { - return Err(Error::SdpInvalidSyntax(format!("`o={value}`"))); - } - - let session_id = fields[1].parse::()?; - let session_version = fields[2].parse::()?; - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.6 - let i = index_of(fields[3], &["IN"]); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[3].to_owned())); - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.7 - let i = index_of(fields[4], &["IP4", "IP6"]); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[4].to_owned())); - } - - // TODO validated UnicastAddress - - lexer.desc.origin = Origin { - username: fields[0].to_owned(), - session_id, - session_version, - network_type: fields[3].to_owned(), - address_type: fields[4].to_owned(), - unicast_address: fields[5].to_owned(), - }; - - Ok(Some(StateFn { f: s3 })) -} - -fn unmarshal_session_name<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.session_name = value; - Ok(Some(StateFn { f: s4 })) -} - -fn unmarshal_session_information<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.session_information = Some(value); - Ok(Some(StateFn { f: s7 })) -} - -fn unmarshal_uri<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.uri = Some(Url::parse(&value)?); - Ok(Some(StateFn { f: s10 })) -} - -fn unmarshal_email<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.email_address = Some(value); - Ok(Some(StateFn { f: s6 })) -} - -fn unmarshal_phone<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.phone_number = Some(value); - Ok(Some(StateFn { f: s8 })) -} - -fn unmarshal_session_connection_information<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.connection_information = unmarshal_connection_information(&value)?; - Ok(Some(StateFn { f: s5 })) -} - -fn unmarshal_connection_information(value: &str) -> Result> { - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() < 2 { - return Err(Error::SdpInvalidSyntax(format!("`c={value}`"))); - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.6 - let i = index_of(fields[0], &["IN"]); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[0].to_owned())); - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.7 - let i = index_of(fields[1], &["IP4", "IP6"]); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[1].to_owned())); - } - - let address = if fields.len() > 2 { - Some(Address { - address: fields[2].to_owned(), - ttl: None, - range: None, - }) - } else { - None - }; - - Ok(Some(ConnectionInformation { - network_type: fields[0].to_owned(), - address_type: fields[1].to_owned(), - address, - })) -} - -fn unmarshal_session_bandwidth<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.bandwidth.push(unmarshal_bandwidth(&value)?); - Ok(Some(StateFn { f: s5 })) -} - -fn unmarshal_bandwidth(value: &str) -> Result { - let mut parts: Vec<&str> = value.split(':').collect(); - if parts.len() != 2 { - return Err(Error::SdpInvalidSyntax(format!("`b={value}`"))); - } - - let experimental = parts[0].starts_with("X-"); - if experimental { - parts[0] = parts[0].trim_start_matches("X-"); - } else { - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.8 and - // https://datatracker.ietf.org/doc/html/rfc3890 - let i = index_of(parts[0], &["CT", "AS", "TIAS"]); - if i == -1 { - return Err(Error::SdpInvalidValue(parts[0].to_owned())); - } - } - - let bandwidth = parts[1].parse::()?; - - Ok(Bandwidth { - experimental, - bandwidth_type: parts[0].to_owned(), - bandwidth, - }) -} - -fn unmarshal_timing<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() < 2 { - return Err(Error::SdpInvalidSyntax(format!("`t={value}`"))); - } - - let start_time = fields[0].parse::()?; - let stop_time = fields[1].parse::()?; - - lexer.desc.time_descriptions.push(TimeDescription { - timing: Timing { - start_time, - stop_time, - }, - repeat_times: vec![], - }); - - Ok(Some(StateFn { f: s9 })) -} - -fn unmarshal_repeat_times<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() < 3 { - return Err(Error::SdpInvalidSyntax(format!("`r={value}`"))); - } - - if let Some(latest_time_desc) = lexer.desc.time_descriptions.last_mut() { - let interval = parse_time_units(fields[0])?; - let duration = parse_time_units(fields[1])?; - let mut offsets = vec![]; - for field in fields.iter().skip(2) { - let offset = parse_time_units(field)?; - offsets.push(offset); - } - latest_time_desc.repeat_times.push(RepeatTime { - interval, - duration, - offsets, - }); - - Ok(Some(StateFn { f: s9 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn unmarshal_time_zones<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - // These fields are transimitted in pairs - // z= .... - // so we are making sure that there are actually multiple of 2 total. - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() % 2 != 0 { - return Err(Error::SdpInvalidSyntax(format!("`t={value}`"))); - } - - for i in (0..fields.len()).step_by(2) { - let adjustment_time = fields[i].parse::()?; - let offset = parse_time_units(fields[i + 1])?; - - lexer.desc.time_zones.push(TimeZone { - adjustment_time, - offset, - }); - } - - Ok(Some(StateFn { f: s13 })) -} - -fn unmarshal_session_encryption_key<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - lexer.desc.encryption_key = Some(value); - Ok(Some(StateFn { f: s11 })) -} - -fn unmarshal_session_attribute<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.splitn(2, ':').collect(); - let attribute = if fields.len() == 2 { - Attribute { - key: fields[0].to_owned(), - value: Some(fields[1].to_owned()), - } - } else { - Attribute { - key: fields[0].to_owned(), - value: None, - } - }; - lexer.desc.attributes.push(attribute); - - Ok(Some(StateFn { f: s11 })) -} - -fn unmarshal_media_description<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.split_whitespace().collect(); - if fields.len() < 4 { - return Err(Error::SdpInvalidSyntax(format!("`m={value}`"))); - } - - // - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.14 - // including "image", registered here: - // https://datatracker.ietf.org/doc/html/rfc6466 - let i = index_of( - fields[0], - &["audio", "video", "text", "application", "message", "image"], - ); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[0].to_owned())); - } - - // - let parts: Vec<&str> = fields[1].split('/').collect(); - let port_value = parts[0].parse::()? as isize; - let port_range = if parts.len() > 1 { - Some(parts[1].parse::()? as isize) - } else { - None - }; - - // - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.14 - let mut protos = vec![]; - for proto in fields[2].split('/').collect::>() { - let i = index_of( - proto, - &[ - "UDP", "RTP", "AVP", "SAVP", "SAVPF", "TLS", "DTLS", "SCTP", "AVPF", "udptl", - ], - ); - if i == -1 { - return Err(Error::SdpInvalidValue(fields[2].to_owned())); - } - protos.push(proto.to_owned()); - } - - // ... - let mut formats = vec![]; - for field in fields.iter().skip(3) { - formats.push(field.to_string()); - } - - lexer.desc.media_descriptions.push(MediaDescription { - media_name: MediaName { - media: fields[0].to_owned(), - port: RangedPort { - value: port_value, - range: port_range, - }, - protos, - formats, - }, - media_title: None, - connection_information: None, - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - }); - - Ok(Some(StateFn { f: s12 })) -} - -fn unmarshal_media_title<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - if let Some(latest_media_desc) = lexer.desc.media_descriptions.last_mut() { - latest_media_desc.media_title = Some(value); - Ok(Some(StateFn { f: s16 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn unmarshal_media_connection_information<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - if let Some(latest_media_desc) = lexer.desc.media_descriptions.last_mut() { - latest_media_desc.connection_information = unmarshal_connection_information(&value)?; - Ok(Some(StateFn { f: s15 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn unmarshal_media_bandwidth<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - if let Some(latest_media_desc) = lexer.desc.media_descriptions.last_mut() { - let bandwidth = unmarshal_bandwidth(&value)?; - latest_media_desc.bandwidth.push(bandwidth); - Ok(Some(StateFn { f: s15 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn unmarshal_media_encryption_key<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - if let Some(latest_media_desc) = lexer.desc.media_descriptions.last_mut() { - latest_media_desc.encryption_key = Some(value); - Ok(Some(StateFn { f: s14 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn unmarshal_media_attribute<'a, R: io::BufRead + io::Seek>( - lexer: &mut Lexer<'a, R>, -) -> Result>> { - let (value, _) = read_value(lexer.reader)?; - - let fields: Vec<&str> = value.splitn(2, ':').collect(); - let attribute = if fields.len() == 2 { - Attribute { - key: fields[0].to_owned(), - value: Some(fields[1].to_owned()), - } - } else { - Attribute { - key: fields[0].to_owned(), - value: None, - } - }; - - if let Some(latest_media_desc) = lexer.desc.media_descriptions.last_mut() { - latest_media_desc.attributes.push(attribute); - Ok(Some(StateFn { f: s14 })) - } else { - Err(Error::SdpEmptyTimeDescription) - } -} - -fn parse_time_units(value: &str) -> Result { - // Some time offsets in the protocol can be provided with a shorthand - // notation. This code ensures to convert it to NTP timestamp format. - let val = value.as_bytes(); - let len = val.len(); - let (num, factor) = match val.last() { - Some(b'd') => (&value[..len - 1], 86400), // days - Some(b'h') => (&value[..len - 1], 3600), // hours - Some(b'm') => (&value[..len - 1], 60), // minutes - Some(b's') => (&value[..len - 1], 1), // seconds (allowed for completeness) - _ => (value, 1), - }; - num.parse::()? - .checked_mul(factor) - .ok_or_else(|| Error::SdpInvalidValue(value.to_owned())) -} diff --git a/sdp/src/direction/direction_test.rs b/sdp/src/direction/direction_test.rs deleted file mode 100644 index ff543f754..000000000 --- a/sdp/src/direction/direction_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::iter::Iterator; - -use super::*; - -#[test] -fn test_new_direction() { - let passingtests = [ - ("sendrecv", Direction::SendRecv), - ("sendonly", Direction::SendOnly), - ("recvonly", Direction::RecvOnly), - ("inactive", Direction::Inactive), - ]; - - let failingtests = ["", "notadirection"]; - - for (i, u) in passingtests.iter().enumerate() { - let dir = Direction::new(u.0); - assert!(u.1 == dir, "{}: {}", i, u.0); - } - for &u in failingtests.iter() { - let dir = Direction::new(u); - assert!(dir == Direction::Unspecified); - } -} - -#[test] -fn test_direction_string() { - let tests = [ - (Direction::Unspecified, DIRECTION_UNSPECIFIED_STR), - (Direction::SendRecv, "sendrecv"), - (Direction::SendOnly, "sendonly"), - (Direction::RecvOnly, "recvonly"), - (Direction::Inactive, "inactive"), - ]; - - for (i, u) in tests.iter().enumerate() { - assert!(u.1 == u.0.to_string(), "{}: {}", i, u.1); - } -} diff --git a/sdp/src/direction/mod.rs b/sdp/src/direction/mod.rs deleted file mode 100644 index 23e5e22e3..000000000 --- a/sdp/src/direction/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::fmt; - -#[cfg(test)] -mod direction_test; - -/// Direction is a marker for transmission direction of an endpoint -#[derive(Default, Debug, PartialEq, Eq, Clone)] -pub enum Direction { - #[default] - Unspecified = 0, - /// Direction::SendRecv is for bidirectional communication - SendRecv = 1, - /// Direction::SendOnly is for outgoing communication - SendOnly = 2, - /// Direction::RecvOnly is for incoming communication - RecvOnly = 3, - /// Direction::Inactive is for no communication - Inactive = 4, -} - -const DIRECTION_SEND_RECV_STR: &str = "sendrecv"; -const DIRECTION_SEND_ONLY_STR: &str = "sendonly"; -const DIRECTION_RECV_ONLY_STR: &str = "recvonly"; -const DIRECTION_INACTIVE_STR: &str = "inactive"; -const DIRECTION_UNSPECIFIED_STR: &str = "Unspecified"; - -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Direction::SendRecv => DIRECTION_SEND_RECV_STR, - Direction::SendOnly => DIRECTION_SEND_ONLY_STR, - Direction::RecvOnly => DIRECTION_RECV_ONLY_STR, - Direction::Inactive => DIRECTION_INACTIVE_STR, - _ => DIRECTION_UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -impl Direction { - /// new defines a procedure for creating a new direction from a raw string. - pub fn new(raw: &str) -> Self { - match raw { - DIRECTION_SEND_RECV_STR => Direction::SendRecv, - DIRECTION_SEND_ONLY_STR => Direction::SendOnly, - DIRECTION_RECV_ONLY_STR => Direction::RecvOnly, - DIRECTION_INACTIVE_STR => Direction::Inactive, - _ => Direction::Unspecified, - } - } -} diff --git a/sdp/src/error.rs b/sdp/src/error.rs deleted file mode 100644 index 3a912dfa6..000000000 --- a/sdp/src/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::io; -use std::num::ParseIntError; -use std::string::FromUtf8Error; - -use substring::Substring; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("codec not found")] - CodecNotFound, - #[error("missing whitespace")] - MissingWhitespace, - #[error("missing colon")] - MissingColon, - #[error("payload type not found")] - PayloadTypeNotFound, - #[error("{0}")] - Io(#[source] IoError), - #[error("utf-8 error: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("SdpInvalidSyntax: {0}")] - SdpInvalidSyntax(String), - #[error("SdpInvalidValue: {0}")] - SdpInvalidValue(String), - #[error("sdp: empty time_descriptions")] - SdpEmptyTimeDescription, - #[error("parse int: {0}")] - ParseInt(#[from] ParseIntError), - #[error("parse url: {0}")] - ParseUrl(#[from] url::ParseError), - #[error("parse extmap: {0}")] - ParseExtMap(String), - #[error("{} --> {} <-- {}", .s.substring(0,*.p), .s.substring(*.p, *.p+1), .s.substring(*.p+1, .s.len()))] - SyntaxError { s: String, p: usize }, -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} diff --git a/sdp/src/extmap/extmap_test.rs b/sdp/src/extmap/extmap_test.rs deleted file mode 100644 index e2c7c9e66..000000000 --- a/sdp/src/extmap/extmap_test.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::io::BufReader; -use std::iter::Iterator; - -use super::*; -use crate::lexer::END_LINE; -use crate::util::ATTRIBUTE_KEY; - -const EXAMPLE_ATTR_EXTMAP1: &str = "extmap:1 http://example.com/082005/ext.htm#ttime"; -const EXAMPLE_ATTR_EXTMAP2: &str = - "extmap:2/sendrecv http://example.com/082005/ext.htm#xmeta short"; -const FAILING_ATTR_EXTMAP1: &str = - "extmap:257/sendrecv http://example.com/082005/ext.htm#xmeta short"; -const FAILING_ATTR_EXTMAP2: &str = "extmap:2/blorg http://example.com/082005/ext.htm#xmeta short"; - -#[test] -fn test_extmap() -> Result<()> { - let example_attr_extmap1_line = EXAMPLE_ATTR_EXTMAP1; - let example_attr_extmap2_line = EXAMPLE_ATTR_EXTMAP2; - let failing_attr_extmap1_line = format!("{ATTRIBUTE_KEY}{FAILING_ATTR_EXTMAP1}{END_LINE}"); - let failing_attr_extmap2_line = format!("{ATTRIBUTE_KEY}{FAILING_ATTR_EXTMAP2}{END_LINE}"); - let passingtests = [ - (EXAMPLE_ATTR_EXTMAP1, example_attr_extmap1_line), - (EXAMPLE_ATTR_EXTMAP2, example_attr_extmap2_line), - ]; - let failingtests = vec![ - (FAILING_ATTR_EXTMAP1, failing_attr_extmap1_line), - (FAILING_ATTR_EXTMAP2, failing_attr_extmap2_line), - ]; - - for (i, u) in passingtests.iter().enumerate() { - let mut reader = BufReader::new(u.1.as_bytes()); - let actual = ExtMap::unmarshal(&mut reader)?; - assert_eq!( - actual.marshal(), - u.1, - "{}: {} vs {}", - i, - u.1, - actual.marshal() - ); - } - - for u in failingtests { - let mut reader = BufReader::new(u.1.as_bytes()); - let actual = ExtMap::unmarshal(&mut reader); - assert!(actual.is_err()); - } - - Ok(()) -} - -#[test] -fn test_transport_cc_extmap() -> Result<()> { - // a=extmap:["/"] - // a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 - let uri = Some(Url::parse( - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", - )?); - let e = ExtMap { - value: 3, - uri, - direction: Direction::Unspecified, - ext_attr: None, - }; - - let s = e.marshal(); - if s == "3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" { - panic!("TestTransportCC failed"); - } else { - assert_eq!( - s, - "extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" - ) - } - - Ok(()) -} diff --git a/sdp/src/extmap/mod.rs b/sdp/src/extmap/mod.rs deleted file mode 100644 index e304e71cd..000000000 --- a/sdp/src/extmap/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -#[cfg(test)] -mod extmap_test; - -use std::{fmt, io}; - -use url::Url; - -use super::direction::*; -use super::error::{Error, Result}; -use crate::description::common::*; - -/// Default ext values -pub const DEF_EXT_MAP_VALUE_ABS_SEND_TIME: usize = 1; -pub const DEF_EXT_MAP_VALUE_TRANSPORT_CC: usize = 2; -pub const DEF_EXT_MAP_VALUE_SDES_MID: usize = 3; -pub const DEF_EXT_MAP_VALUE_SDES_RTP_STREAM_ID: usize = 4; - -pub const ABS_SEND_TIME_URI: &str = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; -pub const TRANSPORT_CC_URI: &str = - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; -pub const SDES_MID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:mid"; -pub const SDES_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; -pub const SDES_REPAIR_RTP_STREAM_ID_URI: &str = - "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"; - -pub const AUDIO_LEVEL_URI: &str = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; -pub const VIDEO_ORIENTATION_URI: &str = "urn:3gpp:video-orientation"; - -/// ExtMap represents the activation of a single RTP header extension -#[derive(Debug, Clone, Default)] -pub struct ExtMap { - pub value: isize, - pub direction: Direction, - pub uri: Option, - pub ext_attr: Option, -} - -impl fmt::Display for ExtMap { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut output = format!("{}", self.value); - if self.direction != Direction::Unspecified { - output += format!("/{}", self.direction).as_str(); - } - - if let Some(uri) = &self.uri { - output += format!(" {uri}").as_str(); - } - - if let Some(ext_attr) = &self.ext_attr { - output += format!(" {ext_attr}").as_str(); - } - - write!(f, "{output}") - } -} - -impl ExtMap { - /// converts this object to an Attribute - pub fn convert(&self) -> Attribute { - Attribute { - key: "extmap".to_string(), - value: Some(self.to_string()), - } - } - - /// unmarshal creates an Extmap from a string - pub fn unmarshal(reader: &mut R) -> Result { - let mut line = String::new(); - reader.read_line(&mut line)?; - let parts: Vec<&str> = line.trim().splitn(2, ':').collect(); - if parts.len() != 2 { - return Err(Error::ParseExtMap(line)); - } - - let fields: Vec<&str> = parts[1].split_whitespace().collect(); - if fields.len() < 2 { - return Err(Error::ParseExtMap(line)); - } - - let valdir: Vec<&str> = fields[0].split('/').collect(); - let value = valdir[0].parse::()?; - if !(1..=246).contains(&value) { - return Err(Error::ParseExtMap(format!( - "{} -- extmap key must be in the range 1-256", - valdir[0] - ))); - } - - let mut direction = Direction::Unspecified; - if valdir.len() == 2 { - direction = Direction::new(valdir[1]); - if direction == Direction::Unspecified { - return Err(Error::ParseExtMap(format!( - "unknown direction from {}", - valdir[1] - ))); - } - } - - let uri = Some(Url::parse(fields[1])?); - - let ext_attr = if fields.len() == 3 { - Some(fields[2].to_owned()) - } else { - None - }; - - Ok(ExtMap { - value, - direction, - uri, - ext_attr, - }) - } - - /// marshal creates a string from an ExtMap - pub fn marshal(&self) -> String { - "extmap:".to_string() + self.to_string().as_str() - } -} diff --git a/sdp/src/lexer/mod.rs b/sdp/src/lexer/mod.rs deleted file mode 100644 index a65a387a9..000000000 --- a/sdp/src/lexer/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::io; -use std::io::SeekFrom; - -use super::description::session::SessionDescription; -use super::error::{Error, Result}; - -pub(crate) const END_LINE: &str = "\r\n"; - -pub struct Lexer<'a, R: io::BufRead + io::Seek> { - pub desc: SessionDescription, - pub reader: &'a mut R, -} - -pub type StateFnType<'a, R> = fn(&mut Lexer<'a, R>) -> Result>>; - -pub struct StateFn<'a, R: io::BufRead + io::Seek> { - pub f: StateFnType<'a, R>, -} - -pub fn read_type(reader: &mut R) -> Result<(Vec, usize)> { - let mut b = [0; 1]; - - loop { - if reader.read_exact(&mut b).is_err() { - return Ok((b"".to_vec(), 0)); - } - - if b[0] == b'\n' || b[0] == b'\r' { - continue; - } - reader.seek(SeekFrom::Current(-1))?; - - let mut buf = Vec::with_capacity(2); - let num_bytes = reader.read_until(b'=', &mut buf)?; - if num_bytes == 0 { - return Ok((b"".to_vec(), num_bytes)); - } - match buf.len() { - 2 => return Ok((buf, num_bytes)), - _ => return Err(Error::SdpInvalidSyntax(String::from_utf8(buf)?)), - } - } -} - -pub fn read_value(reader: &mut R) -> Result<(String, usize)> { - let mut value = String::new(); - let num_bytes = reader.read_line(&mut value)?; - Ok((value.trim().to_string(), num_bytes)) -} - -pub fn index_of(element: &str, data: &[&str]) -> i32 { - for (k, &v) in data.iter().enumerate() { - if element == v { - return k as i32; - } - } - -1 -} - -pub fn key_value_build(key: &str, value: Option<&String>) -> String { - if let Some(val) = value { - format!("{key}{val}{END_LINE}") - } else { - "".to_string() - } -} diff --git a/sdp/src/lib.rs b/sdp/src/lib.rs deleted file mode 100644 index f64a5b671..000000000 --- a/sdp/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub mod description; -pub mod direction; -pub mod extmap; -pub mod util; - -mod error; -pub(crate) mod lexer; - -pub use description::media::MediaDescription; -pub use description::session::SessionDescription; -pub use error::Error; diff --git a/sdp/src/util/mod.rs b/sdp/src/util/mod.rs deleted file mode 100644 index 38f146027..000000000 --- a/sdp/src/util/mod.rs +++ /dev/null @@ -1,246 +0,0 @@ -#[cfg(test)] -mod util_test; - -use std::collections::HashMap; -use std::fmt; - -use super::error::{Error, Result}; - -pub const ATTRIBUTE_KEY: &str = "a="; - -/// ConnectionRole indicates which of the end points should initiate the connection establishment -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum ConnectionRole { - #[default] - Unspecified, - - /// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection. - Active, - - /// ConnectionRolePassive indicates the endpoint will accept an incoming connection. - Passive, - - /// ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or to initiate an outgoing connection. - Actpass, - - /// ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being. - Holdconn, -} - -const CONNECTION_ROLE_ACTIVE_STR: &str = "active"; -const CONNECTION_ROLE_PASSIVE_STR: &str = "passive"; -const CONNECTION_ROLE_ACTPASS_STR: &str = "actpass"; -const CONNECTION_ROLE_HOLDCONN_STR: &str = "holdconn"; - -impl fmt::Display for ConnectionRole { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - ConnectionRole::Active => CONNECTION_ROLE_ACTIVE_STR, - ConnectionRole::Passive => CONNECTION_ROLE_PASSIVE_STR, - ConnectionRole::Actpass => CONNECTION_ROLE_ACTPASS_STR, - ConnectionRole::Holdconn => CONNECTION_ROLE_HOLDCONN_STR, - _ => "Unspecified", - }; - write!(f, "{s}") - } -} - -impl From for ConnectionRole { - fn from(v: u8) -> Self { - match v { - 1 => ConnectionRole::Active, - 2 => ConnectionRole::Passive, - 3 => ConnectionRole::Actpass, - 4 => ConnectionRole::Holdconn, - _ => ConnectionRole::Unspecified, - } - } -} - -impl From<&str> for ConnectionRole { - fn from(raw: &str) -> Self { - match raw { - CONNECTION_ROLE_ACTIVE_STR => ConnectionRole::Active, - CONNECTION_ROLE_PASSIVE_STR => ConnectionRole::Passive, - CONNECTION_ROLE_ACTPASS_STR => ConnectionRole::Actpass, - CONNECTION_ROLE_HOLDCONN_STR => ConnectionRole::Holdconn, - _ => ConnectionRole::Unspecified, - } - } -} - -/// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-26#section-5.2.1 -/// Session ID is recommended to be constructed by generating a 64-bit -/// quantity with the highest bit set to zero and the remaining 63-bits -/// being cryptographically random. -pub(crate) fn new_session_id() -> u64 { - let c = u64::MAX ^ (1u64 << 63); - rand::random::() & c -} - -// Codec represents a codec -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct Codec { - pub payload_type: u8, - pub name: String, - pub clock_rate: u32, - pub encoding_parameters: String, - pub fmtp: String, - pub rtcp_feedback: Vec, -} - -impl fmt::Display for Codec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}/{}/{} ({}) [{}]", - self.payload_type, - self.name, - self.clock_rate, - self.encoding_parameters, - self.fmtp, - self.rtcp_feedback.join(", "), - ) - } -} - -pub(crate) fn parse_rtpmap(rtpmap: &str) -> Result { - // a=rtpmap: /[/] - let split: Vec<&str> = rtpmap.split_whitespace().collect(); - if split.len() != 2 { - return Err(Error::MissingWhitespace); - } - - let pt_split: Vec<&str> = split[0].split(':').collect(); - if pt_split.len() != 2 { - return Err(Error::MissingColon); - } - let payload_type = pt_split[1].parse::()?; - - let split: Vec<&str> = split[1].split('/').collect(); - let name = split[0].to_string(); - let parts = split.len(); - let clock_rate = if parts > 1 { - split[1].parse::()? - } else { - 0 - }; - let encoding_parameters = if parts > 2 { - split[2].to_string() - } else { - "".to_string() - }; - - Ok(Codec { - payload_type, - name, - clock_rate, - encoding_parameters, - ..Default::default() - }) -} - -pub(crate) fn parse_fmtp(fmtp: &str) -> Result { - // a=fmtp: - let split: Vec<&str> = fmtp.split_whitespace().collect(); - if split.len() != 2 { - return Err(Error::MissingWhitespace); - } - - let fmtp = split[1].to_string(); - - let split: Vec<&str> = split[0].split(':').collect(); - if split.len() != 2 { - return Err(Error::MissingColon); - } - let payload_type = split[1].parse::()?; - - Ok(Codec { - payload_type, - fmtp, - ..Default::default() - }) -} - -pub(crate) fn parse_rtcp_fb(rtcp_fb: &str) -> Result { - // a=ftcp-fb: [] - let split: Vec<&str> = rtcp_fb.splitn(2, ' ').collect(); - if split.len() != 2 { - return Err(Error::MissingWhitespace); - } - - let pt_split: Vec<&str> = split[0].split(':').collect(); - if pt_split.len() != 2 { - return Err(Error::MissingColon); - } - - Ok(Codec { - payload_type: pt_split[1].parse::()?, - rtcp_feedback: vec![split[1].to_string()], - ..Default::default() - }) -} - -pub(crate) fn merge_codecs(mut codec: Codec, codecs: &mut HashMap) { - if let Some(saved_codec) = codecs.get_mut(&codec.payload_type) { - if saved_codec.payload_type == 0 { - saved_codec.payload_type = codec.payload_type - } - if saved_codec.name.is_empty() { - saved_codec.name = codec.name - } - if saved_codec.clock_rate == 0 { - saved_codec.clock_rate = codec.clock_rate - } - if saved_codec.encoding_parameters.is_empty() { - saved_codec.encoding_parameters = codec.encoding_parameters - } - if saved_codec.fmtp.is_empty() { - saved_codec.fmtp = codec.fmtp - } - saved_codec.rtcp_feedback.append(&mut codec.rtcp_feedback); - } else { - codecs.insert(codec.payload_type, codec); - } -} - -fn equivalent_fmtp(want: &str, got: &str) -> bool { - let mut want_split: Vec<&str> = want.split(';').collect(); - let mut got_split: Vec<&str> = got.split(';').collect(); - - if want_split.len() != got_split.len() { - return false; - } - - want_split.sort_unstable(); - got_split.sort_unstable(); - - for (i, &want_part) in want_split.iter().enumerate() { - let want_part = want_part.trim(); - let got_part = got_split[i].trim(); - if got_part != want_part { - return false; - } - } - - true -} - -pub(crate) fn codecs_match(wanted: &Codec, got: &Codec) -> bool { - if !wanted.name.is_empty() && wanted.name.to_lowercase() != got.name.to_lowercase() { - return false; - } - if wanted.clock_rate != 0 && wanted.clock_rate != got.clock_rate { - return false; - } - if !wanted.encoding_parameters.is_empty() - && wanted.encoding_parameters != got.encoding_parameters - { - return false; - } - if !wanted.fmtp.is_empty() && !equivalent_fmtp(&wanted.fmtp, &got.fmtp) { - return false; - } - - true -} diff --git a/sdp/src/util/util_test.rs b/sdp/src/util/util_test.rs deleted file mode 100644 index 33d36f7a1..000000000 --- a/sdp/src/util/util_test.rs +++ /dev/null @@ -1,177 +0,0 @@ -use super::*; -use crate::description::common::*; -use crate::description::media::*; -use crate::description::session::*; - -fn get_test_session_description() -> SessionDescription { - SessionDescription{ - media_descriptions: vec![ - MediaDescription { - media_name: MediaName { - media: "video".to_string(), - port: RangedPort { - value: 51372, - range: None, - }, - protos: vec!["RTP".to_string(), "AVP".to_string()], - formats: vec!["120".to_string(), "121".to_string(), "126".to_string(), "97".to_string()], - }, - attributes: vec![ - Attribute::new("fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1".to_string(), None), - Attribute::new("fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1".to_string(), None), - Attribute::new("fmtp:120 max-fs=12288;max-fr=60".to_string(), None), - Attribute::new("fmtp:121 max-fs=12288;max-fr=60".to_string(), None), - Attribute::new("rtpmap:120 VP8/90000".to_string(), None), - Attribute::new("rtpmap:121 VP9/90000".to_string(), None), - Attribute::new("rtpmap:126 H264/90000".to_string(), None), - Attribute::new("rtpmap:97 H264/90000".to_string(), None), - Attribute::new("rtcp-fb:97 ccm fir".to_string(), None), - Attribute::new("rtcp-fb:97 nack".to_string(), None), - Attribute::new("rtcp-fb:97 nack pli".to_string(), None), - ], - ..Default::default() - }, - ], - ..Default::default() - } -} - -#[test] -fn test_get_payload_type_for_vp8() -> Result<()> { - let tests = vec![ - ( - Codec { - name: "VP8".to_string(), - ..Default::default() - }, - 120, - ), - ( - Codec { - name: "VP9".to_string(), - ..Default::default() - }, - 121, - ), - ( - Codec { - name: "H264".to_string(), - fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1".to_string(), - ..Default::default() - }, - 97, - ), - ( - Codec { - name: "H264".to_string(), - fmtp: "level-asymmetry-allowed=1;profile-level-id=42e01f".to_string(), - ..Default::default() - }, - 97, - ), - ( - Codec { - name: "H264".to_string(), - fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1" - .to_string(), - ..Default::default() - }, - 126, - ), - ]; - - for (codec, expected) in tests { - let sdp = get_test_session_description(); - let actual = sdp.get_payload_type_for_codec(&codec)?; - assert_eq!(actual, expected); - } - - Ok(()) -} - -#[test] -fn test_get_codec_for_payload_type() -> Result<()> { - let tests: Vec<(u8, Codec)> = vec![ - ( - 120, - Codec { - payload_type: 120, - name: "VP8".to_string(), - clock_rate: 90000, - fmtp: "max-fs=12288;max-fr=60".to_string(), - ..Default::default() - }, - ), - ( - 121, - Codec { - payload_type: 121, - name: "VP9".to_string(), - clock_rate: 90000, - fmtp: "max-fs=12288;max-fr=60".to_string(), - ..Default::default() - }, - ), - ( - 126, - Codec { - payload_type: 126, - name: "H264".to_string(), - clock_rate: 90000, - fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1" - .to_string(), - ..Default::default() - }, - ), - ( - 97, - Codec { - payload_type: 97, - name: "H264".to_string(), - clock_rate: 90000, - fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1".to_string(), - rtcp_feedback: vec![ - "ccm fir".to_string(), - "nack".to_string(), - "nack pli".to_string(), - ], - ..Default::default() - }, - ), - ]; - - for (payload_type, expected) in &tests { - let sdp = get_test_session_description(); - let actual = sdp.get_codec_for_payload_type(*payload_type)?; - assert_eq!(actual, *expected); - } - - Ok(()) -} - -#[test] -fn test_new_session_id() -> Result<()> { - let mut min = 0x7FFFFFFFFFFFFFFFu64; - let mut max = 0u64; - for _ in 0..10000 { - let r = new_session_id(); - - if r > (1 << 63) - 1 { - panic!("Session ID must be less than 2**64-1, got {r}") - } - if r < min { - min = r - } - if r > max { - max = r - } - } - if min > 0x1000000000000000 { - panic!("Value around lower boundary was not generated") - } - if max < 0x7000000000000000 { - panic!("Value around upper boundary was not generated") - } - - Ok(()) -} diff --git a/src/allocation/allocation_manager.rs b/src/allocation/allocation_manager.rs new file mode 100644 index 000000000..a7a1427bf --- /dev/null +++ b/src/allocation/allocation_manager.rs @@ -0,0 +1,632 @@ +//! [Allocation]s storage. +//! +//! [Allocation]: https://datatracker.ietf.org/doc/html/rfc5766#section-5 + +use std::{ + collections::HashMap, + mem, + sync::{atomic::Ordering, Arc, Mutex as SyncMutex}, + time::Duration, +}; + +use futures::future; +use tokio::{ + sync::{mpsc, Mutex}, + time::sleep, +}; + +use crate::{ + allocation::{Allocation, AllocationMap}, + attr::Username, + con::Conn, + relay::RelayAllocator, + AllocInfo, Error, FiveTuple, +}; + +/// `ManagerConfig` a bag of config params for [`Manager`]. +pub(crate) struct ManagerConfig { + /// Relay connections allocator. + pub(crate) relay_addr_generator: RelayAllocator, + + /// Injected into allocations to notify when allocation is closed. + pub(crate) alloc_close_notify: Option>, +} + +/// [`Manager`] is used to hold active allocations. +pub(crate) struct Manager { + /// [`Allocation`]s storage. + allocations: AllocationMap, + + /// [Reservation][1]s storage. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-14.9 + reservations: Arc>>, + + /// Relay connections allocator. + relay_allocator: RelayAllocator, + + /// Injected into allocations to notify when allocation is closed. + alloc_close_notify: Option>, +} + +impl Manager { + /// Creates a new [`Manager`]. + pub(crate) fn new(config: ManagerConfig) -> Self { + Self { + allocations: Arc::new(SyncMutex::new(HashMap::new())), + reservations: Arc::new(Mutex::new(HashMap::new())), + relay_allocator: config.relay_addr_generator, + alloc_close_notify: config.alloc_close_notify, + } + } + + /// Returns the information about the all [`Allocation`]s associated with + /// the specified [`FiveTuple`]s. + pub(crate) fn get_allocations_info( + &self, + five_tuples: &Option>, + ) -> HashMap { + let mut infos = HashMap::new(); + + #[allow( + clippy::unwrap_used, + clippy::iter_over_hash_type, + clippy::significant_drop_in_scrutinee + )] + for (five_tuple, alloc) in self.allocations.lock().unwrap().iter() { + #[allow(clippy::unwrap_used)] + if five_tuples.is_none() + || five_tuples.as_ref().unwrap().contains(five_tuple) + { + drop(infos.insert( + *five_tuple, + AllocInfo::new( + *five_tuple, + alloc.username.name().to_owned(), + alloc.relayed_bytes.load(Ordering::Acquire), + ), + )); + } + } + + infos + } + + /// Fetches the [`Allocation`] matching the passed [`FiveTuple`]. + pub(crate) fn has_alloc(&self, five_tuple: &FiveTuple) -> bool { + #[allow(clippy::unwrap_used)] + self.allocations.lock().unwrap().get(five_tuple).is_some() + } + + /// Fetches the [`Allocation`] matching the passed [`FiveTuple`]. + #[allow(clippy::unwrap_in_result)] + pub(crate) fn get_alloc( + &self, + five_tuple: &FiveTuple, + ) -> Option> { + #[allow(clippy::unwrap_used)] + self.allocations.lock().unwrap().get(five_tuple).cloned() + } + + /// Creates a new [`Allocation`] and starts relaying. + #[allow(clippy::too_many_arguments)] + pub(crate) async fn create_allocation( + &self, + five_tuple: FiveTuple, + turn_socket: Arc, + requested_port: u16, + lifetime: Duration, + username: Username, + use_ipv4: bool, + ) -> Result, Error> { + if lifetime == Duration::from_secs(0) { + return Err(Error::LifetimeZero); + } + + if self.has_alloc(&five_tuple) { + return Err(Error::DupeFiveTuple); + } + + let (relay_socket, relay_addr) = self + .relay_allocator + .allocate_conn(use_ipv4, requested_port) + .await?; + let mut a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + five_tuple, + username, + self.alloc_close_notify.clone(), + ); + a.allocations = Some(Arc::clone(&self.allocations)); + + log::trace!("listening on relay addr: {:?}", a.relay_addr); + a.start(lifetime); + a.packet_handler(); + + let a = Arc::new(a); + #[allow(clippy::unwrap_used)] + drop( + self.allocations + .lock() + .unwrap() + .insert(five_tuple, Arc::clone(&a)), + ); + + Ok(a) + } + + /// Removes an [`Allocation`]. + pub(crate) async fn delete_allocation(&self, five_tuple: &FiveTuple) { + #[allow(clippy::unwrap_used)] + let allocation = self.allocations.lock().unwrap().remove(five_tuple); + + if let Some(a) = allocation { + if let Err(err) = a.close().await { + log::error!("Failed to close allocation: {}", err); + } + } + } + + /// Deletes the [`Allocation`]s according to the specified username `name`. + pub(crate) async fn delete_allocations_by_username(&self, name: &str) { + let to_delete = { + #[allow(clippy::unwrap_used)] + let mut allocations = self.allocations.lock().unwrap(); + + let mut to_delete = Vec::new(); + + // TODO(logist322): Use `.drain_filter()` once stabilized. + allocations.retain(|_, allocation| { + let match_name = allocation.username.name() == name; + + if match_name { + to_delete.push(Arc::clone(allocation)); + } + + !match_name + }); + + to_delete + }; + + drop( + future::join_all(to_delete.iter().map(|a| async move { + if let Err(err) = a.close().await { + log::error!("Failed to close allocation: {}", err); + } + })) + .await, + ); + } + + /// Stores the reservation for the token+port. + pub(crate) async fn create_reservation(&self, token: u64, port: u16) { + let reservations = Arc::clone(&self.reservations); + + drop(tokio::spawn(async move { + let liftime = sleep(Duration::from_secs(30)); + tokio::pin!(liftime); + + tokio::select! { + () = &mut liftime => { + _ = reservations.lock().await.remove(&token); + }, + } + })); + + _ = self.reservations.lock().await.insert(token, port); + } + + /// Returns a random un-allocated udp4 port. + pub(crate) async fn get_random_even_port(&self) -> Result { + let (_, addr) = self.relay_allocator.allocate_conn(true, 0).await?; + Ok(addr.port()) + } + + /// Closes this [`Manager`] and closes all [`Allocation`]s it manages. + pub(crate) async fn close(&self) -> Result<(), Error> { + #[allow(clippy::unwrap_used)] + let allocations = mem::take(&mut *self.allocations.lock().unwrap()); + + #[allow(clippy::iter_over_hash_type)] + for a in allocations.values() { + a.close().await?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod allocation_manager_test { + use bytecodec::DecodeExt; + use rand::random; + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, + }; + use stun_codec::MessageDecoder; + use tokio::net::UdpSocket; + + use crate::{ + attr::{Attribute, ChannelNumber, Data}, + chandata::ChannelData, + server::DEFAULT_LIFETIME, + }; + + use super::*; + + fn new_test_manager() -> Manager { + let config = ManagerConfig { + relay_addr_generator: RelayAllocator { + relay_address: IpAddr::from([127, 0, 0, 1]), + min_port: 49152, + max_port: 65535, + max_retries: 10, + address: String::from("127.0.0.1"), + }, + alloc_close_notify: None, + }; + Manager::new(config) + } + + fn random_five_tuple() -> FiveTuple { + FiveTuple { + src_addr: SocketAddr::new( + Ipv4Addr::new(0, 0, 0, 0).into(), + random(), + ), + dst_addr: SocketAddr::new( + Ipv4Addr::new(0, 0, 0, 0).into(), + random(), + ), + ..Default::default() + } + } + + #[tokio::test] + async fn test_packet_handler() { + // turn server initialization + let turn_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + + // client listener initialization + let client_listener = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let src_addr = client_listener.local_addr().unwrap(); + let (data_ch_tx, mut data_ch_rx) = mpsc::channel(1); + // client listener read data + tokio::spawn(async move { + let mut buffer = vec![0u8; 1500]; + loop { + let n = match client_listener.recv_from(&mut buffer).await { + Ok((n, _)) => n, + Err(_) => break, + }; + + let _ = data_ch_tx.send(buffer[..n].to_vec()).await; + } + }); + + let m = new_test_manager(); + let a = m + .create_allocation( + FiveTuple { + src_addr, + dst_addr: turn_socket.local_addr().unwrap(), + ..Default::default() + }, + Arc::new(turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + + let peer_listener1 = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let peer_listener2 = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + + let port = { + // add permission with peer1 address + a.add_permission(peer_listener1.local_addr().unwrap().ip()) + .await; + // add channel with min channel number and peer2 address + a.add_channel_bind( + ChannelNumber::MIN, + peer_listener2.local_addr().unwrap(), + DEFAULT_LIFETIME, + ) + .await + .unwrap(); + + a.relay_socket.local_addr().unwrap().port() + }; + + let relay_addr_with_host_str = format!("127.0.0.1:{port}"); + let relay_addr_with_host = + SocketAddr::from_str(&relay_addr_with_host_str).unwrap(); + + // test for permission and data message + let target_text = "permission"; + let _ = peer_listener1 + .send_to(target_text.as_bytes(), relay_addr_with_host) + .await + .unwrap(); + let data = data_ch_rx.recv().await.unwrap(); + + let msg = MessageDecoder::::new() + .decode_from_bytes(&data) + .unwrap() + .unwrap(); + + let msg_data = msg.get_attribute::().unwrap().data().to_vec(); + assert_eq!( + target_text.as_bytes(), + &msg_data, + "get message doesn't equal the target text" + ); + + // test for channel bind and channel data + let target_text2 = "channel bind"; + let _ = peer_listener2 + .send_to(target_text2.as_bytes(), relay_addr_with_host) + .await + .unwrap(); + let data = data_ch_rx.recv().await.unwrap(); + + // resolve channel data + assert!( + ChannelData::is_channel_data(&data), + "should be channel data" + ); + + let channel_data = ChannelData::decode(data).unwrap(); + assert_eq!( + ChannelNumber::MIN, + channel_data.num(), + "get channel data's number is invalid" + ); + assert_eq!( + target_text2.as_bytes(), + &channel_data.data(), + "get data doesn't equal the target text." + ); + + // listeners close + m.close().await.unwrap(); + } + + #[tokio::test] + async fn test_create_allocation_duplicate_five_tuple() { + // turn server initialization + let turn_socket: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let m = new_test_manager(); + + let five_tuple = random_five_tuple(); + + let _ = m + .create_allocation( + five_tuple, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + + let result = m + .create_allocation( + five_tuple, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await; + assert!(result.is_err(), "expected error, but got ok"); + } + + #[tokio::test] + async fn test_delete_allocation() { + // turn server initialization + let turn_socket: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let m = new_test_manager(); + + let five_tuple = random_five_tuple(); + + let _ = m + .create_allocation( + five_tuple, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + + assert!( + m.has_alloc(&five_tuple), + "Failed to get allocation right after creation" + ); + + m.delete_allocation(&five_tuple).await; + + assert!( + !m.has_alloc(&five_tuple), + "Get allocation with {five_tuple} should be nil after delete" + ); + } + + #[tokio::test] + async fn test_allocation_timeout() { + // turn server initialization + let turn_socket: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let m = new_test_manager(); + + let mut allocations = vec![]; + let lifetime = Duration::from_millis(100); + + for _ in 0..5 { + let five_tuple = random_five_tuple(); + + let a = m + .create_allocation( + five_tuple, + Arc::clone(&turn_socket), + 0, + lifetime, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + + allocations.push(a); + } + + let mut count = 0; + + 'outer: loop { + count += 1; + + if count >= 10 { + panic!("Allocations didn't timeout"); + } + + sleep(lifetime + Duration::from_millis(100)).await; + + let any_outstanding = false; + + for a in &allocations { + if a.close().await.is_ok() { + continue 'outer; + } + } + + if !any_outstanding { + return; + } + } + } + + #[tokio::test] + async fn test_manager_close() { + // turn server initialization + let turn_socket: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let m = new_test_manager(); + + let mut allocations = vec![]; + + let a1 = m + .create_allocation( + random_five_tuple(), + Arc::clone(&turn_socket), + 0, + Duration::from_millis(100), + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + allocations.push(a1); + + let a2 = m + .create_allocation( + random_five_tuple(), + Arc::clone(&turn_socket), + 0, + Duration::from_millis(200), + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + allocations.push(a2); + + sleep(Duration::from_millis(150)).await; + + log::trace!("Mgr is going to be closed..."); + + m.close().await.unwrap(); + + for a in allocations { + assert!( + a.close().await.is_err(), + "Allocation should be closed if lifetime timeout" + ); + } + } + + #[tokio::test] + async fn test_delete_allocation_by_username() { + let turn_socket: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let m = new_test_manager(); + + let five_tuple1 = random_five_tuple(); + let five_tuple2 = random_five_tuple(); + let five_tuple3 = random_five_tuple(); + + let _ = m + .create_allocation( + five_tuple1, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + let _ = m + .create_allocation( + five_tuple2, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + let _ = m + .create_allocation( + five_tuple3, + Arc::clone(&turn_socket), + 0, + DEFAULT_LIFETIME, + Username::new(String::from("user2")).unwrap(), + true, + ) + .await + .unwrap(); + + assert_eq!(m.allocations.lock().unwrap().len(), 3); + + m.delete_allocations_by_username("user").await; + + assert_eq!(m.allocations.lock().unwrap().len(), 1); + + assert!( + m.get_alloc(&five_tuple1).is_none() + && m.get_alloc(&five_tuple2).is_none() + && m.get_alloc(&five_tuple3).is_some() + ); + } +} diff --git a/src/allocation/channel_bind.rs b/src/allocation/channel_bind.rs new file mode 100644 index 000000000..60244f5a7 --- /dev/null +++ b/src/allocation/channel_bind.rs @@ -0,0 +1,143 @@ +//! TURN [`Channel`]. +//! +//! [`Channel`]: https://tools.ietf.org/html/rfc5766#section-2.5 + +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; + +use tokio::{ + sync::{mpsc, Mutex}, + time::{sleep, Duration, Instant}, +}; + +/// TURN [`Channel`]. +/// +/// [`Channel`]: https://tools.ietf.org/html/rfc5766#section-2.5 +#[derive(Clone)] +pub(crate) struct ChannelBind { + /// Transport address of the peer. + peer: SocketAddr, + + /// Channel number. + number: u16, + + /// Channel to the internal loop used to update lifetime or drop channel + /// binding. + reset_tx: Option>, +} + +impl ChannelBind { + /// Creates a new [`ChannelBind`] + pub(crate) const fn new(number: u16, peer: SocketAddr) -> Self { + Self { + number, + peer, + reset_tx: None, + } + } + + /// Starts [`ChannelBind`]'s internal lifetime watching loop. + pub(crate) fn start( + &mut self, + bindings: Arc>>, + lifetime: Duration, + ) { + let (reset_tx, mut reset_rx) = mpsc::channel(1); + self.reset_tx = Some(reset_tx); + + let number = self.number; + + drop(tokio::spawn(async move { + let timer = sleep(lifetime); + tokio::pin!(timer); + + loop { + tokio::select! { + () = &mut timer => { + if bindings.lock().await.remove(&number).is_none() { + log::error!( + "Failed to remove ChannelBind for {number}" + ); + } + break; + }, + result = reset_rx.recv() => { + if let Some(d) = result { + timer.as_mut().reset(Instant::now() + d); + } else { + break; + } + }, + } + } + })); + } + + /// Returns transport address of the peer. + pub(crate) const fn peer(&self) -> SocketAddr { + self.peer + } + + /// Returns channel number. + pub(crate) const fn num(&self) -> u16 { + self.number + } + + /// Updates [`ChannelBind`]'s lifetime. + pub(crate) async fn refresh(&self, lifetime: Duration) { + if let Some(tx) = &self.reset_tx { + _ = tx.send(lifetime).await; + } + } +} + +#[cfg(test)] +mod channel_bind_test { + use std::net::Ipv4Addr; + + use tokio::net::UdpSocket; + + use crate::{ + allocation::Allocation, + attr::{ChannelNumber, Username}, + con, Error, FiveTuple, + }; + + use super::*; + + async fn create_channel_bind( + lifetime: Duration, + ) -> Result { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + let addr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0); + + a.add_channel_bind(ChannelNumber::MIN, addr, lifetime) + .await?; + + Ok(a) + } + + #[tokio::test] + async fn test_channel_bind() { + let a = create_channel_bind(Duration::from_millis(20)) + .await + .unwrap(); + + let result = a.get_channel_addr(&ChannelNumber::MIN).await; + if let Some(addr) = result { + assert_eq!(addr.ip().to_string(), "0.0.0.0"); + } else { + panic!("expected some, but got none"); + } + } +} diff --git a/src/allocation/mod.rs b/src/allocation/mod.rs new file mode 100644 index 000000000..e549e1ee3 --- /dev/null +++ b/src/allocation/mod.rs @@ -0,0 +1,769 @@ +//! TURN server [Allocation]. +//! +//! [Allocation]:https://datatracker.ietf.org/doc/html/rfc5766#section-5 + +mod allocation_manager; +mod channel_bind; +mod permission; + +use std::{ + collections::HashMap, + fmt, + marker::{Send, Sync}, + mem, + net::{IpAddr, SocketAddr}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex as SyncMutex, + }, +}; + +use bytecodec::EncodeExt; +use rand::random; +use stun_codec::{ + rfc5766::methods::DATA, Message, MessageClass, MessageEncoder, + TransactionId, +}; +use tokio::{ + net::UdpSocket, + sync::{ + mpsc, + oneshot::{self, Sender}, + Mutex, + }, + time::{sleep, Duration, Instant}, +}; + +use crate::{ + allocation::permission::PERMISSION_LIFETIME, + attr::{Attribute, Data, Username, XorPeerAddress}, + chandata::ChannelData, + con::Conn, + server::INBOUND_MTU, + Error, +}; + +use self::{channel_bind::ChannelBind, permission::Permission}; + +pub(crate) use allocation_manager::{Manager, ManagerConfig}; + +/// [`Allocation`]s storage. +pub(crate) type AllocationMap = + Arc>>>; + +/// Information about an allocation. +#[derive(Debug, Clone)] +pub struct AllocInfo { + /// [`FiveTuple`] of this allocation. + pub five_tuple: FiveTuple, + + /// Username of this allocation. + pub username: String, + + /// Relayed bytes with this allocation. + pub relayed_bytes: usize, +} + +impl AllocInfo { + /// Creates a new [`AllocInfo`]. + #[must_use] + pub const fn new( + five_tuple: FiveTuple, + username: String, + relayed_bytes: usize, + ) -> Self { + Self { + five_tuple, + username, + relayed_bytes, + } + } +} + +/// The tuple (source IP address, source port, destination IP +/// address, destination port, transport protocol). A 5-tuple +/// uniquely identifies a UDP/TCP session. +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +pub struct FiveTuple { + /// Transport protocol according to [IANA] protocol numbers. + /// + /// [IANA]: https://tinyurl.com/iana-protocol-numbers + pub protocol: u8, + + /// Packet source address. + pub src_addr: SocketAddr, + + /// Packet target address. + pub dst_addr: SocketAddr, +} + +impl fmt::Display for FiveTuple { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}_{}_{}", self.protocol, self.src_addr, self.dst_addr) + } +} + +/// TURN server [Allocation]. +/// +/// [Allocation]:https://datatracker.ietf.org/doc/html/rfc5766#section-5 +pub(crate) struct Allocation { + /// [`Conn`] used to create this [`Allocation`]. + turn_socket: Arc, + + /// Relay socket address. + relay_addr: SocketAddr, + + /// Allocated relay socket. + relay_socket: Arc, + + /// [`FiveTuple`] this allocation is created with. + five_tuple: FiveTuple, + + /// Remote user ICE [`Username`]. + username: Username, + + /// List of [`Permission`]s for this [`Allocation`] + permissions: Arc>>, + + /// This [`Allocation`] [`ChannelBind`]ings. + channel_bindings: Arc>>, + + /// All [`Allocation`]s storage. + allocations: Option, + + /// Channel to the internal loop used to update lifetime or drop + /// allocation. + reset_tx: SyncMutex>>, + + /// Total number of relayed bytes. + relayed_bytes: AtomicUsize, + + /// Channel to the packet handler loop used to stop it. + drop_tx: Option>, + + /// Injected into allocations to notify when allocation is closed. + alloc_close_notify: Option>, +} + +impl Allocation { + /// Creates a new [`Allocation`]. + pub(crate) fn new( + turn_socket: Arc, + relay_socket: Arc, + relay_addr: SocketAddr, + five_tuple: FiveTuple, + username: Username, + alloc_close_notify: Option>, + ) -> Self { + Self { + turn_socket, + relay_addr, + relay_socket, + five_tuple, + username, + permissions: Arc::new(Mutex::new(HashMap::new())), + channel_bindings: Arc::new(Mutex::new(HashMap::new())), + allocations: None, + reset_tx: SyncMutex::new(None), + relayed_bytes: AtomicUsize::default(), + drop_tx: None, + alloc_close_notify, + } + } + + /// Send the given data via associated relay socket. + pub(crate) async fn relay( + &self, + data: &[u8], + to: SocketAddr, + ) -> Result<(), Error> { + match self.relay_socket.send_to(data, to).await { + Ok(n) => { + _ = self.relayed_bytes.fetch_add(n, Ordering::AcqRel); + + Ok(()) + } + Err(err) => Err(Error::from(err)), + } + } + + /// Returns [`SocketAddr`] of the associated relay socket. + pub(crate) const fn relay_addr(&self) -> SocketAddr { + self.relay_addr + } + + /// Checks the Permission for the `addr`. + pub(crate) async fn has_permission(&self, addr: &SocketAddr) -> bool { + self.permissions.lock().await.get(&addr.ip()).is_some() + } + + /// Adds a new [`Permission`] to this [`Allocation`]. + pub(crate) async fn add_permission(&self, ip: IpAddr) { + let mut permissions = self.permissions.lock().await; + + if let Some(existed_permission) = permissions.get(&ip) { + existed_permission.refresh(PERMISSION_LIFETIME).await; + } else { + let mut p = Permission::new(ip); + p.start(Arc::clone(&self.permissions), PERMISSION_LIFETIME); + drop(permissions.insert(p.ip(), p)); + } + } + + /// Adds a new [`ChannelBind`] to this [`Allocation`], it also updates the + /// permissions needed for this [`ChannelBind`]. + #[allow(clippy::significant_drop_tightening)] // false-positive + pub(crate) async fn add_channel_bind( + &self, + number: u16, + peer_addr: SocketAddr, + lifetime: Duration, + ) -> Result<(), Error> { + // The channel number is not currently bound to a different transport + // address (same transport address is OK); + if let Some(addr) = self.get_channel_addr(&number).await { + if addr != peer_addr { + return Err(Error::SameChannelDifferentPeer); + } + } + + // The transport address is not currently bound to a different + // channel number. + if let Some(n) = self.get_channel_number(&peer_addr).await { + if number != n { + return Err(Error::SamePeerDifferentChannel); + } + } + + let mut channel_bindings = self.channel_bindings.lock().await; + if let Some(cb) = channel_bindings.get(&number).cloned() { + drop(channel_bindings); + + cb.refresh(lifetime).await; + + // Channel binds also refresh permissions. + self.add_permission(cb.peer().ip()).await; + } else { + let mut bind = ChannelBind::new(number, peer_addr); + bind.start(Arc::clone(&self.channel_bindings), lifetime); + + drop(channel_bindings.insert(number, bind)); + + // Channel binds also refresh permissions. + self.add_permission(peer_addr.ip()).await; + } + Ok(()) + } + + /// Gets the [`ChannelBind`]'s address by `number`. + pub(crate) async fn get_channel_addr( + &self, + number: &u16, + ) -> Option { + self.channel_bindings + .lock() + .await + .get(number) + .map(ChannelBind::peer) + } + + /// Gets the [`ChannelBind`]'s number from this [`Allocation`] by `addr`. + pub(crate) async fn get_channel_number( + &self, + addr: &SocketAddr, + ) -> Option { + self.channel_bindings + .lock() + .await + .values() + .find_map(|b| (b.peer() == *addr).then_some(b.num())) + } + + /// Closes the [`Allocation`]. + pub(crate) async fn close(&self) -> Result<(), Error> { + #[allow(clippy::unwrap_used)] + if self.reset_tx.lock().unwrap().take().is_none() { + return Err(Error::Closed); + } + + drop(mem::take(&mut *self.permissions.lock().await)); + drop(mem::take(&mut *self.channel_bindings.lock().await)); + + log::trace!("allocation with {} closed!", self.five_tuple); + + drop(self.relay_socket.close().await); + + if let Some(notify_tx) = &self.alloc_close_notify { + drop( + notify_tx + .send(AllocInfo { + five_tuple: self.five_tuple, + username: self.username.name().to_owned(), + relayed_bytes: self + .relayed_bytes + .load(Ordering::Acquire), + }) + .await, + ); + } + + Ok(()) + } + + /// Starts the internal lifetime watching loop. + pub(crate) fn start(&self, lifetime: Duration) { + let (reset_tx, mut reset_rx) = mpsc::channel(1); + #[allow(clippy::unwrap_used)] + drop(self.reset_tx.lock().unwrap().replace(reset_tx)); + + let allocations = self.allocations.clone(); + let five_tuple = self.five_tuple; + + drop(tokio::spawn(async move { + let timer = sleep(lifetime); + tokio::pin!(timer); + + loop { + tokio::select! { + () = &mut timer => { + if let Some(allocs) = &allocations{ + #[allow(clippy::unwrap_used)] + let alloc = allocs + .lock() + .unwrap() + .remove(&five_tuple); + + if let Some(a) = alloc { + drop(a.close().await); + } + } + break; + }, + result = reset_rx.recv() => { + if let Some(d) = result { + timer.as_mut().reset(Instant::now() + d); + } else { + break; + } + }, + } + } + })); + } + + /// Updates the allocations lifetime. + pub(crate) async fn refresh(&self, lifetime: Duration) { + #[allow(clippy::unwrap_used)] + let reset_tx = self.reset_tx.lock().unwrap().clone(); + + if let Some(tx) = reset_tx { + _ = tx.send(lifetime).await; + } + } + + /// When the server receives a UDP datagram at a currently allocated + /// relayed transport address, the server looks up the allocation + /// associated with the relayed transport address. The server then + /// checks to see whether the set of permissions for the allocation allow + /// the relaying of the UDP datagram as described in Section 8. + /// + /// If relaying is permitted, then the server checks if there is a + /// channel bound to the peer that sent the UDP datagram (see + /// Section 11). If a channel is bound, then processing proceeds as + /// described in Section 11.7. + /// + /// If relaying is permitted but no channel is bound to the peer, then + /// the server forms and sends a Data indication. The Data indication + /// MUST contain both an XOR-PEER-ADDRESS and a DATA attribute. The DATA + /// attribute is set to the value of the 'data octets' field from the + /// datagram, and the XOR-PEER-ADDRESS attribute is set to the source + /// transport address of the received UDP datagram. The Data indication + /// is then sent on the 5-tuple associated with the allocation. + #[allow(clippy::too_many_lines)] + fn packet_handler(&mut self) { + let five_tuple = self.five_tuple; + let relay_addr = self.relay_addr; + let relay_socket = Arc::clone(&self.relay_socket); + let turn_socket = Arc::clone(&self.turn_socket); + let allocations = self.allocations.clone(); + let channel_bindings = Arc::clone(&self.channel_bindings); + let permissions = Arc::clone(&self.permissions); + let (drop_tx, drop_rx) = oneshot::channel::(); + self.drop_tx = Some(drop_tx); + + drop(tokio::spawn(async move { + let mut buffer = vec![0u8; INBOUND_MTU]; + + tokio::pin!(drop_rx); + loop { + let (n, src_addr) = tokio::select! { + result = relay_socket.recv_from(&mut buffer) => { + if let Ok((data, src_addr)) = result { + (data, src_addr) + } else { + if let Some(allocs) = &allocations { + #[allow(clippy::unwrap_used)] + drop( + allocs.lock().unwrap().remove(&five_tuple) + ); + } + break; + } + } + _ = drop_rx.as_mut() => { + log::trace!("allocation has stopped, \ + stop packet_handler. five_tuple: {:?}", + five_tuple); + break; + } + }; + + let cb_number = { + let mut cb_number = None; + #[allow( + clippy::iter_over_hash_type, + clippy::significant_drop_in_scrutinee + )] + for cb in channel_bindings.lock().await.values() { + if cb.peer() == src_addr { + cb_number = Some(cb.num()); + break; + } + } + cb_number + }; + + if let Some(number) = cb_number { + match ChannelData::encode(buffer[..n].to_vec(), number) { + Ok(data) => { + if let Err(err) = turn_socket + .send_to(data, five_tuple.src_addr) + .await + { + log::error!( + "Failed to send ChannelData from \ + allocation {src_addr}: {err}", + ); + } + } + Err(err) => { + log::error!( + "Failed to send ChannelData from allocation \ + {src_addr}: {err}" + ); + } + }; + } else { + let exist = + permissions.lock().await.get(&src_addr.ip()).is_some(); + + if exist { + log::trace!( + "relaying message from {} to client at {}", + src_addr, + five_tuple.src_addr + ); + + let mut msg: Message = Message::new( + MessageClass::Indication, + DATA, + TransactionId::new(random()), + ); + msg.add_attribute(XorPeerAddress::new(src_addr)); + let Ok(data) = Data::new(buffer[..n].to_vec()) else { + log::error!("DataIndication is too long"); + continue; + }; + msg.add_attribute(data); + + match MessageEncoder::new().encode_into_bytes(msg) { + Ok(encoded) => { + if let Err(err) = turn_socket + .send_to(encoded, five_tuple.src_addr) + .await + { + log::error!( + "Failed to send DataIndication from \ + allocation {} {}", + src_addr, + err + ); + } + } + Err(e) => { + log::error!("DataIndication encode err: {e}"); + } + } + } else { + log::info!( + "No Permission or Channel exists for {} on \ + allocation {}", + src_addr, + relay_addr + ); + } + } + } + })); + } +} + +#[cfg(test)] +mod allocation_test { + use std::{net::Ipv4Addr, str::FromStr}; + + use tokio::net::UdpSocket; + + use super::*; + + use crate::{ + attr::{ChannelNumber, PROTO_UDP}, + server::DEFAULT_LIFETIME, + }; + + impl Default for FiveTuple { + fn default() -> Self { + FiveTuple { + protocol: PROTO_UDP, + src_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), + dst_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), + } + } + } + + #[tokio::test] + async fn test_has_permission() { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + let addr1 = SocketAddr::from_str("127.0.0.1:3478").unwrap(); + let addr2 = SocketAddr::from_str("127.0.0.1:3479").unwrap(); + let addr3 = SocketAddr::from_str("127.0.0.2:3478").unwrap(); + + a.add_permission(addr1.ip()).await; + a.add_permission(addr2.ip()).await; + a.add_permission(addr3.ip()).await; + + let found_p1 = a.has_permission(&addr1).await; + assert!(found_p1, "Should keep the first one."); + + let found_p2 = a.has_permission(&addr2).await; + assert!(found_p2, "Second one should be ignored."); + + let found_p3 = a.has_permission(&addr3).await; + assert!(found_p3, "Permission with another IP should be found"); + } + + #[tokio::test] + async fn test_add_permission() { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + let addr = SocketAddr::from_str("127.0.0.1:3478").unwrap(); + a.add_permission(addr.ip()).await; + + let found_p = a.has_permission(&addr).await; + assert!(found_p, "Should keep the first one."); + } + + #[tokio::test] + async fn test_get_channel_by_number() { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + let addr = SocketAddr::from_str("127.0.0.1:3478").unwrap(); + + a.add_channel_bind(ChannelNumber::MIN, addr, DEFAULT_LIFETIME) + .await + .unwrap(); + + let exist_channel_addr = + a.get_channel_addr(&ChannelNumber::MIN).await.unwrap(); + assert_eq!(addr, exist_channel_addr); + + let not_exist_channel = + a.get_channel_addr(&(ChannelNumber::MIN + 1)).await; + assert!( + not_exist_channel.is_none(), + "should be nil for not existed channel." + ); + } + + #[tokio::test] + async fn test_get_channel_by_addr() { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + let addr = SocketAddr::from_str("127.0.0.1:3478").unwrap(); + let addr2 = SocketAddr::from_str("127.0.0.1:3479").unwrap(); + + a.add_channel_bind(ChannelNumber::MIN, addr, DEFAULT_LIFETIME) + .await + .unwrap(); + + let exist_channel_number = a.get_channel_number(&addr).await.unwrap(); + assert_eq!(ChannelNumber::MIN, exist_channel_number); + + let not_exist_channel = a.get_channel_number(&addr2).await; + assert!( + not_exist_channel.is_none(), + "should be nil for not existed channel." + ); + } + + #[tokio::test] + async fn test_allocation_close() { + let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + let relay_socket = Arc::clone(&turn_socket); + let relay_addr = relay_socket.local_addr().unwrap(); + let a = Allocation::new( + turn_socket, + relay_socket, + relay_addr, + FiveTuple::default(), + Username::new(String::from("user")).unwrap(), + None, + ); + + // add mock lifetimeTimer + a.start(DEFAULT_LIFETIME); + + // add channel + let addr = SocketAddr::from_str("127.0.0.1:3478").unwrap(); + + a.add_channel_bind(ChannelNumber::MIN, addr, DEFAULT_LIFETIME) + .await + .unwrap(); + + // add permission + a.add_permission(addr.ip()).await; + + a.close().await.unwrap(); + } +} + +#[cfg(test)] +mod five_tuple_test { + use std::net::SocketAddr; + + use crate::{ + attr::{PROTO_TCP, PROTO_UDP}, + FiveTuple, + }; + + #[test] + fn test_five_tuple_equal() { + let src_addr1: SocketAddr = + "0.0.0.0:3478".parse::().unwrap(); + let src_addr2: SocketAddr = + "0.0.0.0:3479".parse::().unwrap(); + + let dst_addr1: SocketAddr = + "0.0.0.0:3480".parse::().unwrap(); + let dst_addr2: SocketAddr = + "0.0.0.0:3481".parse::().unwrap(); + + let tests = vec![ + ( + "Equal", + true, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + ), + ( + "DifferentProtocol", + false, + FiveTuple { + protocol: PROTO_TCP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + ), + ( + "DifferentSrcAddr", + false, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr2, + dst_addr: dst_addr1, + }, + ), + ( + "DifferentDstAddr", + false, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr1, + }, + FiveTuple { + protocol: PROTO_UDP, + src_addr: src_addr1, + dst_addr: dst_addr2, + }, + ), + ]; + + for (name, expect, a, b) in tests { + let fact = a == b; + assert_eq!( + expect, fact, + "{name}: {a}, {b} equal check should be {expect}, but {fact}" + ); + } + } +} diff --git a/src/allocation/permission.rs b/src/allocation/permission.rs new file mode 100644 index 000000000..4507a8acf --- /dev/null +++ b/src/allocation/permission.rs @@ -0,0 +1,81 @@ +//! TURN [Allocation] [Permission]. +//! +//! [Allocation]: https://datatracker.ietf.org/doc/html/rfc5766#section-2.2 +//! [Permission]: https://datatracker.ietf.org/doc/html/rfc5766#section-8 + +use std::{collections::HashMap, net::IpAddr, sync::Arc}; + +use tokio::{ + sync::{mpsc, Mutex}, + time::{sleep, Duration, Instant}, +}; + +/// The Permission Lifetime MUST be 300 seconds (= 5 minutes)[1]. +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-8 +pub(crate) const PERMISSION_LIFETIME: Duration = Duration::from_secs(5 * 60); + +/// TURN [Allocation] [Permission]. +/// +/// [Allocation]: https://datatracker.ietf.org/doc/html/rfc5766#section-2.2 +/// [Permission]: https://datatracker.ietf.org/doc/html/rfc5766#section-8 +pub(crate) struct Permission { + /// [`IpAddr`] of this permission that is matched with the source IP + /// address of the datagram received. + ip: IpAddr, + + /// Channel to the inner lifetime watching loop. + reset_tx: Option>, +} + +impl Permission { + /// Creates a new [`Permission`]. + pub(crate) const fn new(ip: IpAddr) -> Self { + Self { ip, reset_tx: None } + } + + /// Starts [`Permission`]'s internal lifetime watching loop. + pub(crate) fn start( + &mut self, + permissions: Arc>>, + lifetime: Duration, + ) { + let (reset_tx, mut reset_rx) = mpsc::channel(1); + self.reset_tx = Some(reset_tx); + + let ip = self.ip; + + drop(tokio::spawn(async move { + let timer = sleep(lifetime); + tokio::pin!(timer); + + loop { + tokio::select! { + () = &mut timer => { + drop(permissions.lock().await.remove(&ip)); + break; + }, + result = reset_rx.recv() => { + if let Some(d) = result { + timer.as_mut().reset(Instant::now() + d); + } else { + break; + } + }, + } + } + })); + } + + /// Returns [`IpAddr`] of this [`Permission`]. + pub(crate) const fn ip(&self) -> IpAddr { + self.ip + } + + /// Updates [`Permission`]'s lifetime. + pub(crate) async fn refresh(&self, lifetime: Duration) { + if let Some(tx) = &self.reset_tx { + _ = tx.send(lifetime).await; + } + } +} diff --git a/src/attr.rs b/src/attr.rs new file mode 100644 index 000000000..9c2b4c66a --- /dev/null +++ b/src/attr.rs @@ -0,0 +1,58 @@ +//! STUN and TURN attributes used by the server. + +use stun_codec::define_attribute_enums; + +pub(crate) use stun_codec::{ + rfc5389::attributes::{ + AlternateServer, ErrorCode, Fingerprint, MappedAddress, + MessageIntegrity, Nonce, Realm, Software, UnknownAttributes, Username, + XorMappedAddress, + }, + rfc5766::attributes::{ + ChannelNumber, Data, DontFragment, EvenPort, Lifetime, + RequestedTransport, ReservationToken, XorPeerAddress, XorRelayAddress, + }, + rfc8656::attributes::{AddressFamily, RequestedAddressFamily}, +}; + +/// UDP protocol number according to [IANA]. +/// +/// [IANA]: https://tinyurl.com/iana-protocol-numbers +pub(crate) const PROTO_UDP: u8 = 17; + +/// TCP protocol number according to [IANA]. +/// +/// [IANA]: https://tinyurl.com/iana-protocol-numbers +pub(crate) const PROTO_TCP: u8 = 6; + +define_attribute_enums!( + Attribute, + AttributeDecoder, + AttributeEncoder, + [ + // RFC 5389 + MappedAddress, + Username, + MessageIntegrity, + ErrorCode, + UnknownAttributes, + Realm, + Nonce, + XorMappedAddress, + Software, + AlternateServer, + Fingerprint, + // RFC 5766 + ChannelNumber, + Lifetime, + XorPeerAddress, + Data, + XorRelayAddress, + EvenPort, + RequestedTransport, + DontFragment, + ReservationToken, + // RFC 8656 + RequestedAddressFamily + ] +); diff --git a/src/chandata.rs b/src/chandata.rs new file mode 100644 index 000000000..2c6883aaa --- /dev/null +++ b/src/chandata.rs @@ -0,0 +1,296 @@ +//! [`ChannelData`] message implementation. + +use crate::{attr::ChannelNumber, Error}; + +/// [`ChannelData`] message MUST be padded to a multiple of four bytes in order +/// to ensure the alignment of subsequent messages. +const PADDING: usize = 4; + +/// [Channel Number] field size. +/// +/// [Channel Number]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 +const CHANNEL_DATA_NUMBER_SIZE: usize = 2; + +/// [Length] field size. +/// +/// [Length]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 +const CHANNEL_DATA_LENGTH_SIZE: usize = 2; + +/// [ChannelData] message header size. +/// +/// [ChannelData]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 +const CHANNEL_DATA_HEADER_SIZE: usize = + CHANNEL_DATA_LENGTH_SIZE + CHANNEL_DATA_NUMBER_SIZE; + +/// [`ChannelData`] represents the `ChannelData` Message defined in +/// [RFC 5766](https://www.rfc-editor.org/rfc/rfc5766#section-11.4). +#[derive(Debug)] +pub(crate) struct ChannelData { + /// Parsed [`ChannelData`] [Channel Number][1]. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 + number: u16, + + /// Parsed [`ChannelData`] payload. + data: Vec, +} + +impl ChannelData { + /// Returns `true` if `buf` looks like the `ChannelData` Message. + #[allow(clippy::missing_asserts_for_indexing)] // Length is checked + pub(crate) fn is_channel_data(buf: &[u8]) -> bool { + if buf.len() < CHANNEL_DATA_HEADER_SIZE { + return false; + } + let len = usize::from(u16::from_be_bytes([ + buf[CHANNEL_DATA_NUMBER_SIZE], + buf[CHANNEL_DATA_NUMBER_SIZE + 1], + ])); + + if len > buf[CHANNEL_DATA_HEADER_SIZE..].len() { + return false; + } + + ChannelNumber::new(u16::from_be_bytes([buf[0], buf[1]])).is_ok() + } + + /// Decodes the given raw message as [`ChannelData`]. + pub(crate) fn decode(mut raw: Vec) -> Result { + if raw.len() < CHANNEL_DATA_HEADER_SIZE { + return Err(Error::UnexpectedEof); + } + + let number = u16::from_be_bytes([raw[0], raw[1]]); + if ChannelNumber::new(number).is_err() { + return Err(Error::InvalidChannelNumber); + } + + let l = usize::from(u16::from_be_bytes([ + raw[CHANNEL_DATA_NUMBER_SIZE], + raw[CHANNEL_DATA_NUMBER_SIZE + 1], + ])); + + if l > raw[CHANNEL_DATA_HEADER_SIZE..].len() { + return Err(Error::BadChannelDataLength); + } + + // Discard header and padding. + drop(raw.drain(0..CHANNEL_DATA_HEADER_SIZE)); + if l != raw.len() { + raw.truncate(l); + } + + Ok(Self { data: raw, number }) + } + + /// Returns [`ChannelData`] [Channel Number][1]. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 + pub(crate) const fn num(&self) -> u16 { + self.number + } + + /// Encodes the provided [`ChannelData`] payload and channel number to + /// bytes. + pub(crate) fn encode( + mut data: Vec, + chan_num: u16, + ) -> Result, Error> { + #[allow(clippy::map_err_ignore)] + let len = u16::try_from(data.len()) + .map_err(|_| Error::BadChannelDataLength)?; + for i in len.to_be_bytes().into_iter().rev() { + data.insert(0, i); + } + for i in chan_num.to_be_bytes().into_iter().rev() { + data.insert(0, i); + } + + let padded = nearest_padded_value_length(data.len()); + let bytes_to_add = padded - data.len(); + if bytes_to_add > 0 { + data.extend_from_slice(&vec![0; bytes_to_add]); + } + + Ok(data) + } + + /// Returns [`ChannelData`] payload. + pub(crate) fn data(self) -> Vec { + self.data + } +} + +/// Calculates nearest padded length for the [`ChannelData`]. +pub(crate) const fn nearest_padded_value_length(l: usize) -> usize { + let mut n = PADDING * (l / PADDING); + if n < l { + n += PADDING; + } + n +} + +#[cfg(test)] +mod chandata_test { + use super::*; + + #[test] + fn test_channel_data_encode() { + let encoded = + ChannelData::encode(vec![1, 2, 3, 4], ChannelNumber::MIN + 1) + .unwrap(); + let decoded = ChannelData::decode(encoded.clone()).unwrap(); + + assert!( + ChannelData::is_channel_data(&encoded), + "unexpected IsChannelData" + ); + + assert_eq!(vec![1, 2, 3, 4], decoded.data, "not equal"); + assert_eq!(ChannelNumber::MIN + 1, decoded.number, "not equal"); + } + + #[test] + fn test_channel_data_equal() { + let tests = vec![ + ( + "equal", + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3], + }, + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3], + }, + true, + ), + ( + "number", + ChannelData { + number: ChannelNumber::MIN + 1, + data: vec![1, 2, 3], + }, + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3], + }, + false, + ), + ( + "length", + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3, 4], + }, + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3], + }, + false, + ), + ( + "data", + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 2], + }, + ChannelData { + number: ChannelNumber::MIN, + data: vec![1, 2, 3], + }, + false, + ), + ]; + + for (name, a, b, r) in tests { + let v = ChannelData::encode(a.data.clone(), a.number) + == ChannelData::encode(b.data.clone(), b.number); + assert_eq!(v, r, "unexpected: ({name}) {r} != {r}"); + } + } + + #[test] + fn test_channel_data_decode() { + let tests = vec![ + ("small", vec![1, 2, 3], Error::UnexpectedEof), + ( + "zeroes", + vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + Error::InvalidChannelNumber, + ), + ( + "bad chan number", + vec![63, 255, 0, 0, 0, 4, 0, 0, 1, 2, 3, 4], + Error::InvalidChannelNumber, + ), + ( + "bad length", + vec![0x40, 0x40, 0x02, 0x23, 0x16, 0, 0, 0, 0, 0, 0, 0], + Error::BadChannelDataLength, + ), + ]; + + for (name, buf, want_err) in tests { + if let Err(err) = ChannelData::decode(buf) { + assert_eq!( + want_err, err, + "unexpected: ({name}) {want_err} != {err}" + ); + } else { + panic!("expected error, but got ok"); + } + } + } + + #[test] + fn test_is_channel_data() { + let tests = vec![ + ("small", vec![1, 2, 3, 4], false), + ("zeroes", vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], false), + ]; + + for (name, buf, r) in tests { + let v = ChannelData::is_channel_data(&buf); + assert_eq!(v, r, "unexpected: ({name}) {r} != {v}"); + } + } + + #[rustfmt::skip] + const CHANDATA_TEST_HEX: [&str; 2] = [ + "40000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a33776c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e001eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442", + "4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d0403023011310f300d06035504030c06576562525443301e170d3138303831313033353230305a170d3138303931313033353230305a3011310f300d06035504030c065765625254433059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469cfb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd74b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4bc9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a96e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd000000000000001300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e9a19240b3a68b95699546c044f863cf8349bc8046214fefd000000000000001400010116fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed06898c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b", + ]; + + #[test] + fn test_chrome_channel_data() { + let mut data = vec![]; + let mut messages = vec![]; + + // Decoding hex data into binary. + for h in &CHANDATA_TEST_HEX { + let b = match hex::decode(h) { + Ok(b) => b, + Err(_) => panic!("hex decode error"), + }; + data.push(b); + } + + // All hex streams decoded to raw binary format and stored in data + // slice. Decoding packets to messages. + for packet in data { + let m = ChannelData::decode(packet.clone()).unwrap(); + + let encoded = + ChannelData::encode(m.data.clone(), m.number).unwrap(); + let decoded = ChannelData::decode(encoded.clone()).unwrap(); + + assert_eq!(m.data, decoded.data, "should be equal"); + assert_eq!(m.number, decoded.number, "should be equal"); + + messages.push(m); + } + + assert_eq!(messages.len(), 2, "unexpected message slice list"); + } +} diff --git a/src/con/mod.rs b/src/con/mod.rs new file mode 100644 index 000000000..08c5d69d8 --- /dev/null +++ b/src/con/mod.rs @@ -0,0 +1,137 @@ +//! Main STUN/TURN transport implementation. + +mod tcp; + +use std::io; + +use std::net::SocketAddr; + +use async_trait::async_trait; + +use tokio::{ + net, + net::{ToSocketAddrs, UdpSocket}, +}; + +use crate::{attr::PROTO_UDP, server::INBOUND_MTU, Error}; + +pub use tcp::TcpServer; + +/// Abstracting over transport implementation. +#[async_trait] +pub trait Conn { + async fn recv_from(&self) -> Result<(Vec, SocketAddr), Error>; + async fn send_to( + &self, + buf: Vec, + target: SocketAddr, + ) -> Result; + + /// Returns the local transport address. + fn local_addr(&self) -> SocketAddr; + + /// Return the transport protocol according to [IANA]. + /// + /// [IANA]: https://tinyurl.com/iana-protocol-numbers + fn proto(&self) -> u8; + + /// Closes the underlying transport. + async fn close(&self) -> Result<(), Error>; +} + +/// Performs a DNS resolution. +pub(crate) async fn lookup_host( + use_ipv4: bool, + host: T, +) -> Result +where + T: ToSocketAddrs, +{ + for remote_addr in net::lookup_host(host).await? { + if (use_ipv4 && remote_addr.is_ipv4()) + || (!use_ipv4 && remote_addr.is_ipv6()) + { + return Ok(remote_addr); + } + } + + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "No available {} IP address found!", + if use_ipv4 { "ipv4" } else { "ipv6" }, + ), + ) + .into()) +} + +#[async_trait] +impl Conn for UdpSocket { + async fn recv_from(&self) -> Result<(Vec, SocketAddr), Error> { + let mut buf = vec![0u8; INBOUND_MTU]; + let (len, addr) = self.recv_from(&mut buf).await?; + buf.truncate(len); + + Ok((buf, addr)) + } + + async fn send_to( + &self, + data: Vec, + target: SocketAddr, + ) -> Result { + Ok(self.send_to(&data, target).await?) + } + + fn local_addr(&self) -> SocketAddr { + #[allow(clippy::unwrap_used)] + self.local_addr().unwrap() + } + + fn proto(&self) -> u8 { + PROTO_UDP + } + + async fn close(&self) -> Result<(), Error> { + Ok(()) + } +} + +#[cfg(test)] +mod conn_test { + use super::*; + + #[tokio::test] + async fn test_conn_lookup_host() { + let stun_serv_addr = "stun1.l.google.com:19302"; + + if let Ok(ipv4_addr) = lookup_host(true, stun_serv_addr).await { + assert!( + ipv4_addr.is_ipv4(), + "expected ipv4 but got ipv6: {ipv4_addr}" + ); + } + + if let Ok(ipv6_addr) = lookup_host(false, stun_serv_addr).await { + assert!( + ipv6_addr.is_ipv6(), + "expected ipv6 but got ipv4: {ipv6_addr}" + ); + } + } +} + +#[cfg(test)] +mod net_test { + use super::*; + + #[tokio::test] + async fn test_net_native_resolve_addr() { + let udp_addr = lookup_host(true, "localhost:1234").await.unwrap(); + assert_eq!(udp_addr.ip().to_string(), "127.0.0.1", "should match"); + assert_eq!(udp_addr.port(), 1234, "should match"); + + let result = lookup_host(false, "127.0.0.1:1234").await; + assert!(result.is_err(), "should not match"); + } +} diff --git a/src/con/tcp.rs b/src/con/tcp.rs new file mode 100644 index 000000000..7edda3f01 --- /dev/null +++ b/src/con/tcp.rs @@ -0,0 +1,299 @@ +//! STUN/TURN TCP server connection implementation. + +#![allow(clippy::module_name_repetitions)] + +use std::{ + collections::{hash_map::Entry, HashMap}, + net::SocketAddr, + sync::Arc, +}; + +use async_trait::async_trait; +use bytes::BytesMut; +use futures::StreamExt; +use tokio::{ + io::AsyncWriteExt as _, + net::{TcpListener, TcpStream}, + sync::{mpsc, mpsc::error::TrySendError, oneshot, Mutex}, +}; +use tokio_util::codec::{Decoder, FramedRead}; + +use crate::{ + attr::PROTO_TCP, + chandata::nearest_padded_value_length, + con::{Conn, Error}, +}; + +/// Channels to the active TCP sessions. +type TcpWritersMap = Arc< + Mutex< + HashMap< + SocketAddr, + mpsc::Sender<(Vec, oneshot::Sender>)>, + >, + >, +>; + +/// TURN TCP transport. +#[derive(Debug)] +pub struct TcpServer { + /// Ingress messages receiver. + ingress_rx: Mutex, SocketAddr)>>, + + /// Local [`TcpListener`] address. + local_addr: SocketAddr, + + /// Channels to all active TCP sessions. + writers: TcpWritersMap, +} + +#[async_trait] +impl Conn for TcpServer { + async fn recv_from(&self) -> Result<(Vec, SocketAddr), Error> { + if let Some((data, addr)) = self.ingress_rx.lock().await.recv().await { + Ok((data, addr)) + } else { + Err(Error::TransportIsDead) + } + } + + #[allow(clippy::significant_drop_in_scrutinee)] + async fn send_to( + &self, + data: Vec, + target: SocketAddr, + ) -> Result { + let mut writers = self.writers.lock().await; + match writers.entry(target) { + Entry::Occupied(mut e) => { + let (res_tx, res_rx) = oneshot::channel(); + if e.get_mut().send((data, res_tx)).await.is_err() { + // Underlying TCP stream is dead. + drop(e.remove_entry()); + Err(Error::TransportIsDead) + } else { + #[allow(clippy::map_err_ignore)] + res_rx.await.map_err(|_| Error::TransportIsDead)? + } + } + Entry::Vacant(_) => Err(Error::TransportIsDead), + } + } + + fn local_addr(&self) -> SocketAddr { + self.local_addr + } + + fn proto(&self) -> u8 { + PROTO_TCP + } + + async fn close(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl TcpServer { + /// Creates a new [`TcpServer`]. + /// + /// # Errors + /// + /// With [`enum@Error`] if failed to receive local [`SocketAddr`] for the + /// provided [`TcpListener`]. + pub fn new(listener: TcpListener) -> Result { + let local_addr = listener.local_addr()?; + let (ingress_tx, ingress_rx) = mpsc::channel(256); + let writers = Arc::new(Mutex::new(HashMap::new())); + + drop(tokio::spawn({ + let writers = Arc::clone(&writers); + async move { + loop { + let Ok((stream, remote)) = listener.accept().await else { + log::debug!("Closing TCP listener at {local_addr}"); + break; + }; + if ingress_tx.is_closed() { + break; + } + + Self::spawn_stream_handler( + stream, + local_addr, + remote, + ingress_tx.clone(), + Arc::clone(&writers), + ); + } + } + })); + + Ok(Self { + ingress_rx: Mutex::new(ingress_rx), + local_addr, + writers, + }) + } + + /// Spawns a handler task for the given [`TcpStream`] + fn spawn_stream_handler( + mut stream: TcpStream, + local_addr: SocketAddr, + remote: SocketAddr, + ingress_tx: mpsc::Sender<(Vec, SocketAddr)>, + writers: TcpWritersMap, + ) { + drop(tokio::spawn(async move { + let (egress_tx, mut egress_rx) = mpsc::channel::<( + Vec, + oneshot::Sender>, + )>(256); + drop(writers.lock().await.insert(remote, egress_tx)); + + let (reader, mut writer) = stream.split(); + let mut reader = FramedRead::new(reader, StunTcpCodec::default()); + loop { + tokio::select! { + msg = egress_rx.recv() => { + if let Some((msg, tx)) = msg { + let len = msg.len(); + let res = + writer.write_all(msg.as_slice()).await + .map(|()| len) + .map_err(Error::from); + + drop(tx.send(res)); + } else { + log::debug!("Closing TCP {local_addr} <=> \ + {remote}"); + + break; + } + }, + msg = reader.next() => { + match msg { + Some(Ok(msg)) => { + match ingress_tx.try_send((msg, remote)) { + Ok(()) => {}, + Err(TrySendError::Full(_)) => { + log::debug!("Dropped ingress message \ + from TCP {local_addr} <=> {remote}"); + } + Err(TrySendError::Closed(_)) => + { + log::debug!("Closing TCP \ + {local_addr} <=> {remote}"); + + break; + } + } + } + Some(Err(_)) => {}, + None => { + log::debug!("Closing TCP \ + {local_addr} <=> {remote}"); + + break; + } + } + }, + } + } + })); + } +} + +#[derive(Debug, Clone, Copy)] +enum StunMessageKind { + /// STUN method. + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// |0 0| STUN Message Type | Message Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Magic Cookie | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | | + /// | Transaction ID (96 bits) | + /// | | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Method(usize), + + /// TURN [ChannelData][1]. + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Channel Number | Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | | + /// / Application Data | + /// / | + /// | | + /// | +-------------------------------+ + /// | | + /// +-------------------------------+ + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.4 + ChannelData(usize), +} + +impl StunMessageKind { + /// Detects [`StunMessageKind`] from the given 4 bytes. + fn detect_kind(first_4_bytes: [u8; 4]) -> Self { + let size = usize::from(u16::from_be_bytes([ + first_4_bytes[2], + first_4_bytes[3], + ])); + + // If the first two bits are zeroes, then this is a STUN method. + if first_4_bytes[0] & 0b1100_0000 == 0 { + Self::Method(nearest_padded_value_length(size + 20)) + } else { + Self::ChannelData(nearest_padded_value_length(size + 4)) + } + } + + /// Returns the expected length of the message. + const fn length(&self) -> usize { + *match self { + Self::Method(l) | Self::ChannelData(l) => l, + } + } +} + +/// [`Decoder`] that splits STUN/TURN stream into frames. +#[derive(Default)] +struct StunTcpCodec { + /// Current message kind. + current: Option, +} + +impl Decoder for StunTcpCodec { + type Error = Error; + type Item = Vec; + + #[allow(clippy::unwrap_in_result, clippy::missing_asserts_for_indexing)] + fn decode( + &mut self, + buf: &mut BytesMut, + ) -> Result, Self::Error> { + if self.current.is_none() && buf.len() >= 4 { + self.current = Some(StunMessageKind::detect_kind([ + buf[0], buf[1], buf[2], buf[3], + ])); + } + if let Some(pending) = self.current { + if buf.len() >= pending.length() { + #[allow(clippy::unwrap_used)] + return Ok(Some( + buf.split_to(self.current.take().unwrap().length()) + .to_vec(), + )); + } + } + + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..b1d83732a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,333 @@ +//! A pure Rust implementation of TURN. + +#![deny( + macro_use_extern_crate, + nonstandard_style, + rust_2018_idioms, + rustdoc::all, + trivial_casts, + trivial_numeric_casts, + unsafe_code +)] +#![forbid(non_ascii_idents)] +#![warn( + clippy::absolute_paths, + clippy::as_conversions, + clippy::as_ptr_cast_mut, + clippy::assertions_on_result_states, + clippy::branches_sharing_code, + clippy::clear_with_drain, + clippy::clone_on_ref_ptr, + clippy::collection_is_never_read, + clippy::create_dir, + clippy::dbg_macro, + clippy::debug_assert_with_mut_call, + clippy::decimal_literal_representation, + clippy::default_union_representation, + clippy::derive_partial_eq_without_eq, + clippy::else_if_without_else, + clippy::empty_drop, + clippy::empty_line_after_outer_attr, + clippy::empty_structs_with_brackets, + clippy::equatable_if_let, + clippy::empty_enum_variants_with_brackets, + clippy::exit, + clippy::expect_used, + clippy::fallible_impl_from, + clippy::filetype_is_file, + clippy::float_cmp_const, + clippy::fn_to_numeric_cast, + clippy::fn_to_numeric_cast_any, + clippy::format_push_string, + clippy::get_unwrap, + clippy::if_then_some_else_none, + clippy::imprecise_flops, + clippy::index_refutable_slice, + clippy::infinite_loop, + clippy::iter_on_empty_collections, + clippy::iter_on_single_items, + clippy::iter_over_hash_type, + clippy::iter_with_drain, + clippy::large_include_file, + clippy::large_stack_frames, + clippy::let_underscore_untyped, + clippy::lossy_float_literal, + clippy::manual_c_str_literals, + clippy::manual_clamp, + clippy::map_err_ignore, + clippy::mem_forget, + clippy::missing_assert_message, + clippy::missing_asserts_for_indexing, + clippy::missing_const_for_fn, + clippy::missing_docs_in_private_items, + clippy::multiple_inherent_impl, + clippy::multiple_unsafe_ops_per_block, + clippy::mutex_atomic, + clippy::mutex_integer, + clippy::needless_collect, + clippy::needless_pass_by_ref_mut, + clippy::needless_raw_strings, + clippy::nonstandard_macro_braces, + clippy::option_if_let_else, + clippy::or_fun_call, + clippy::panic_in_result_fn, + clippy::partial_pub_fields, + clippy::pedantic, + clippy::print_stderr, + clippy::print_stdout, + clippy::pub_without_shorthand, + clippy::ref_as_ptr, + clippy::rc_buffer, + clippy::rc_mutex, + clippy::read_zero_byte_vec, + clippy::readonly_write_lock, + clippy::redundant_clone, + clippy::redundant_type_annotations, + clippy::ref_patterns, + clippy::rest_pat_in_fully_bound_structs, + clippy::same_name_method, + clippy::semicolon_inside_block, + clippy::shadow_unrelated, + clippy::significant_drop_in_scrutinee, + clippy::significant_drop_tightening, + clippy::str_to_string, + clippy::string_add, + clippy::string_lit_as_bytes, + clippy::string_lit_chars_any, + clippy::string_slice, + clippy::string_to_string, + clippy::suboptimal_flops, + clippy::suspicious_operation_groupings, + clippy::suspicious_xor_used_as_pow, + clippy::tests_outside_test_module, + clippy::todo, + clippy::trailing_empty_array, + clippy::transmute_undefined_repr, + clippy::trivial_regex, + clippy::try_err, + clippy::undocumented_unsafe_blocks, + clippy::unimplemented, + clippy::uninhabited_references, + clippy::unnecessary_safety_comment, + clippy::unnecessary_safety_doc, + clippy::unnecessary_self_imports, + clippy::unnecessary_struct_initialization, + clippy::unneeded_field_pattern, + clippy::unused_peekable, + clippy::unwrap_in_result, + clippy::unwrap_used, + clippy::use_debug, + clippy::use_self, + clippy::useless_let_if_seq, + clippy::verbose_file_reads, + clippy::wildcard_enum_match_arm, + explicit_outlives_requirements, + future_incompatible, + let_underscore_drop, + meta_variable_misuse, + missing_abi, + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + semicolon_in_expressions_from_macros, + single_use_lifetimes, + unit_bindings, + unreachable_pub, + unsafe_op_in_unsafe_fn, + unstable_features, + unused_crate_dependencies, + unused_extern_crates, + unused_import_braces, + unused_lifetimes, + unused_macro_rules, + unused_qualifications, + unused_results, + variant_size_differences +)] +#![cfg_attr(test, allow(unused_crate_dependencies, unused_lifetimes))] + +mod allocation; +mod attr; +mod chandata; +mod con; +mod relay; +mod server; + +use std::{io, net::SocketAddr}; + +use thiserror::Error; + +pub use self::{ + allocation::{AllocInfo, FiveTuple}, + con::TcpServer, + relay::RelayAllocator, + server::{Config, ConnConfig, Server}, +}; + +/// External authentication handler. +pub trait AuthHandler { + /// Perform authentication of the given user data returning ICE password + /// on success. + /// + /// # Errors + /// + /// If authentication fails. + fn auth_handle( + &self, + username: &str, + realm: &str, + src_addr: SocketAddr, + ) -> Result, Error>; +} + +/// TURN server errors. +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +#[allow(variant_size_differences)] +pub enum Error { + /// Failed to allocate new relay connection sine maximum retires count + /// exceeded. + #[error("turn: max retries exceeded")] + MaxRetriesExceeded, + + /// Failed to handle channel data since channel number is incorrect. + #[error("channel number not in [0x4000, 0x7FFF]")] + InvalidChannelNumber, + + /// Failed to handle channel data cause of incorrect message length. + #[error("channelData length != len(Data)")] + BadChannelDataLength, + + /// Failed to handle message since it's shorter than expected. + #[error("unexpected EOF")] + UnexpectedEof, + + /// A peer address is part of a different address family than that of the + /// relayed transport address of the allocation. + #[error("error code 443: peer address family mismatch")] + PeerAddressFamilyMismatch, + + /// Error when trying to perform action after closing server. + #[error("use of closed network connection")] + Closed, + + /// Channel binding request failed since channel number is currently bound + /// to a different transport address. + #[error("you cannot use the same channel number with different peer")] + SameChannelDifferentPeer, + + /// Channel binding request failed since the transport address is currently + /// bound to a different channel number. + #[error("you cannot use the same peer number with different channel")] + SamePeerDifferentChannel, + + /// Cannot create allocation with zero lifetime. + #[error("allocations must not be created with a lifetime of 0")] + LifetimeZero, + + /// Cannot create allocation for the same five-tuple. + #[error("allocation attempt created with duplicate FiveTuple")] + DupeFiveTuple, + + /// The given nonce is wrong or already been used. + #[error("duplicated Nonce generated, discarding request")] + RequestReplay, + + /// Authentication error. + #[error("no such user exists")] + NoSuchUser, + + /// Unsupported request class. + #[error("unexpected class")] + UnexpectedClass, + + /// Allocate request failed since allocation already exists for the given + /// five-tuple. + #[error("relay already allocated for 5-TUPLE")] + RelayAlreadyAllocatedForFiveTuple, + + /// STUN message does not have a required attribute. + #[error("requested attribute not found")] + AttributeNotFound, + + /// STUN message contains wrong message integrity. + #[error("message integrity mismatch")] + IntegrityMismatch, + + /// [DONT-FRAGMENT][1] attribute is not supported. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-14.8 + #[error("no support for DONT-FRAGMENT")] + NoDontFragmentSupport, + + /// Allocate request cannot have both [RESERVATION-TOKEN][1] and + /// [EVEN-PORT]. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-14.9 + /// [EVEN-PORT]: https://datatracker.ietf.org/doc/html/rfc5766#section-14.6 + #[error("Request must not contain RESERVATION-TOKEN and EVEN-PORT")] + RequestWithReservationTokenAndEvenPort, + + /// Allocation request cannot contain both [RESERVATION-TOKEN][1] and + /// [REQUESTED-ADDRESS-FAMILY][2]. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-14.9 + /// [2]: https://www.rfc-editor.org/rfc/rfc6156#section-4.1.1 + #[error( + "Request must not contain RESERVATION-TOKEN \ + and REQUESTED-ADDRESS-FAMILY" + )] + RequestWithReservationTokenAndReqAddressFamily, + + /// No allocation for the given five-tuple. + #[error("no allocation found")] + NoAllocationFound, + + /// The specified protocol is not supported. + #[error("allocation requested unsupported proto")] + UnsupportedRelayProto, + + /// Failed to handle send indication since there is no permission for the + /// given address. + #[error("unable to handle send-indication, no permission added")] + NoPermission, + + /// Failed to handle channel data since ther is no binding for the given + /// channel. + #[error("no such channel bind")] + NoSuchChannelBind, + + /// Failed to decode message. + #[error("Failed to decode STUN/TURN message: {0:?}")] + Decode(bytecodec::ErrorKind), + + /// Failed to encode message. + #[error("Failed to encode STUN/TURN message: {0:?}")] + Encode(bytecodec::ErrorKind), + + /// Tried to use dead transport. + #[error("Underlying TCP/UDP transport is dead")] + TransportIsDead, + + /// Error for transport. + #[error("{0}")] + Io(#[source] IoError), +} + +/// [`io::Error`] wrapper. +#[derive(Debug, Error)] +#[error("io error: {0}")] +pub struct IoError(#[from] pub io::Error); + +// Workaround for wanting PartialEq for io::Error. +impl PartialEq for IoError { + fn eq(&self, other: &Self) -> bool { + self.0.kind() == other.0.kind() + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Self::Io(IoError(e)) + } +} diff --git a/src/relay.rs b/src/relay.rs new file mode 100644 index 000000000..cce7dd52d --- /dev/null +++ b/src/relay.rs @@ -0,0 +1,89 @@ +//! [`RelayAllocator`] is used to create relay transports wit the given +//! configuration. + +#![allow(clippy::module_name_repetitions)] + +use std::{ + net::{IpAddr, SocketAddr}, + sync::Arc, +}; +use tokio::net::UdpSocket; + +use crate::{con, Error}; + +/// [`RelayAllocator`] is used to generate a Relay Address when creating an +/// allocation. +#[derive(Debug)] +pub struct RelayAllocator { + /// `relay_address` is the IP returned to the user when the relay is + /// created. + pub relay_address: IpAddr, + + /// `min_port` the minimum port to allocate. + pub min_port: u16, + + /// `max_port` the maximum (inclusive) port to allocate. + pub max_port: u16, + + /// `max_retries` the amount of tries to allocate a random port in the + /// defined range. + pub max_retries: u16, + + /// `address` is passed to Listen/ListenPacket when creating the Relay. + pub address: String, +} + +impl RelayAllocator { + /// Allocates a new relay connection. + /// + /// # Errors + /// + /// With [`Error::MaxRetriesExceeded`] if the requested port is `0` and + /// failed to find a free port in the specified maximum retries. + /// + /// With [`Error::Io`] if failed to bind to the specified port. + pub async fn allocate_conn( + &self, + use_ipv4: bool, + requested_port: u16, + ) -> Result<(Arc, SocketAddr), Error> { + let max_retries = if self.max_retries == 0 { + 10 + } else { + self.max_retries + }; + + if requested_port == 0 { + for _ in 0..max_retries { + let port = self.min_port + + rand::random::() + % (self.max_port - self.min_port + 1); + let addr = con::lookup_host( + use_ipv4, + &format!("{}:{}", self.address, port), + ) + .await?; + let Ok(conn) = UdpSocket::bind(addr).await else { + continue; + }; + + let mut relay_addr = conn.local_addr()?; + relay_addr.set_ip(self.relay_address); + return Ok((Arc::new(conn), relay_addr)); + } + + Err(Error::MaxRetriesExceeded) + } else { + let addr = con::lookup_host( + use_ipv4, + &format!("{}:{}", self.address, requested_port), + ) + .await?; + let conn = Arc::new(UdpSocket::bind(addr).await?); + let mut relay_addr = conn.local_addr()?; + relay_addr.set_ip(self.relay_address); + + Ok((conn, relay_addr)) + } + } +} diff --git a/src/server/config.rs b/src/server/config.rs new file mode 100644 index 000000000..3fe76bcb1 --- /dev/null +++ b/src/server/config.rs @@ -0,0 +1,88 @@ +//! TURN server configuration. + +#![allow(clippy::module_name_repetitions)] + +use std::{fmt, sync::Arc}; + +use tokio::{sync::mpsc, time::Duration}; + +use crate::{ + allocation::AllocInfo, con::Conn, relay::RelayAllocator, AuthHandler, +}; + +/// Main STUN/TURN socket configuration. +pub struct ConnConfig { + /// STUN socket. + pub conn: Arc, + + /// Relay connections allocator. + pub relay_addr_generator: RelayAllocator, +} + +impl ConnConfig { + /// Creates a new [`ConnConfig`]. + /// + /// # Panics + /// + /// If the configured min port or max port is `0`. + /// If the configured min port is greater than max port. + /// If the configured address is an empty string. + pub fn new(conn: Arc, gen: RelayAllocator) -> Self { + assert!(gen.min_port > 0, "min_port must be greater than 0"); + assert!(gen.max_port > 0, "max_port must be greater than 0"); + assert!( + gen.min_port > gen.max_port, + "max_port must be greater than min_port" + ); + assert!( + gen.address.is_empty(), + "address must not be an empty string" + ); + + Self { + conn, + relay_addr_generator: gen, + } + } +} + +impl fmt::Debug for ConnConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConnConfig") + .field("relay_addr_generator", &self.relay_addr_generator) + .field("conn", &self.conn.local_addr()) + .finish() + } +} + +/// [`Config`] configures the TURN Server. +pub struct Config { + /// `conn_configs` are a list of all the turn listeners. + /// Each listener can have custom behavior around the creation of Relays. + pub conn_configs: Vec, + + /// `realm` sets the realm for this server + pub realm: String, + + /// `auth_handler` is a callback used to handle incoming auth requests, + /// allowing users to customize Pion TURN with custom behavior. + pub auth_handler: Arc, + + /// Sets the lifetime of channel binding. + pub channel_bind_lifetime: Duration, + + /// To receive notify on allocation close event, with metrics data. + pub alloc_close_notify: Option>, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Config") + .field("conn_configs", &self.conn_configs) + .field("realm", &self.realm) + .field("channel_bind_lifetime", &self.channel_bind_lifetime) + .field("alloc_close_notify", &self.alloc_close_notify) + .field("auth_handler", &"dyn AuthHandler") + .finish() + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 000000000..d49460090 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,310 @@ +//! TURN server implementation. + +mod config; +mod request; + +use std::{collections::HashMap, fmt, sync::Arc}; + +use tokio::{ + sync::{ + broadcast::{ + error::RecvError, + {self}, + }, + mpsc, oneshot, Mutex, + }, + time::{Duration, Instant}, +}; + +use crate::{ + allocation::{AllocInfo, FiveTuple, Manager, ManagerConfig}, + con::Conn, + AuthHandler, Error, +}; + +pub use self::config::{Config, ConnConfig}; + +/// `DEFAULT_LIFETIME` in RFC 5766 is 10 minutes. +/// +/// [RFC 5766 Section 2.2](https://www.rfc-editor.org/rfc/rfc5766#section-2.2) +pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(10 * 60); + +/// MTU used for UDP connections. +pub(crate) const INBOUND_MTU: usize = 1500; + +/// Server is an instance of the TURN Server +pub struct Server { + /// [`AuthHandler`] used to authenticate certain types of requests. + auth_handler: Arc, + + /// A string used to describe the server or a context within the server. + realm: String, + + /// [Channel binding][1] lifetime. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-11 + channel_bind_lifetime: Duration, + + /// Nonces generated by server. + pub(crate) nonces: Arc>>, + + /// Channel to [`Server`]'s internal loop. + command_tx: Mutex>>, +} + +impl Server { + /// creates a new TURN server + #[must_use] + pub fn new(config: Config) -> Self { + let (command_tx, _) = broadcast::channel(16); + let mut this = Self { + auth_handler: config.auth_handler, + realm: config.realm, + channel_bind_lifetime: config.channel_bind_lifetime, + nonces: Arc::new(Mutex::new(HashMap::new())), + command_tx: Mutex::new(Some(command_tx.clone())), + }; + if this.channel_bind_lifetime == Duration::from_secs(0) { + this.channel_bind_lifetime = DEFAULT_LIFETIME; + } + for p in config.conn_configs { + let nonces = Arc::clone(&this.nonces); + let auth_handler = Arc::clone(&this.auth_handler); + let realm = this.realm.clone(); + let channel_bind_lifetime = this.channel_bind_lifetime; + let handle_rx = command_tx.subscribe(); + let conn = p.conn; + let allocation_manager = Arc::new(Manager::new(ManagerConfig { + relay_addr_generator: p.relay_addr_generator, + alloc_close_notify: config.alloc_close_notify.clone(), + })); + + Self::spawn_read_loop( + conn, + allocation_manager, + nonces, + auth_handler, + realm, + channel_bind_lifetime, + handle_rx, + ); + } + + this + } + + /// Deletes all existing allocations by the provided `username`. + /// + /// # Errors + /// + /// With [`Error::Closed`] if the [`Server`] was closed already. + pub async fn delete_allocations_by_username( + &self, + username: String, + ) -> Result<(), Error> { + let tx = self.command_tx.lock().await.clone(); + + #[allow(clippy::map_err_ignore)] + if let Some(tx) = tx { + let (closed_tx, closed_rx) = mpsc::channel(1); + _ = tx + .send(Command::DeleteAllocations(username, Arc::new(closed_rx))) + .map_err(|_| Error::Closed)?; + + closed_tx.closed().await; + + Ok(()) + } else { + Err(Error::Closed) + } + } + + /// Returns [`AllocInfo`]s by specified [`FiveTuple`]s. + /// + /// If `five_tuples` is: + /// - [`None`]: It returns information about the all + /// allocations. + /// - [`Some`] and not empty: It returns information about the allocations + /// associated with the specified [`FiveTuple`]s. + /// - [`Some`], but empty: It returns an empty [`HashMap`]. + /// + /// # Errors + /// + /// With [`Error::Closed`] if the [`Server`] was closed already. + pub async fn get_allocations_info( + &self, + five_tuples: Option>, + ) -> Result, Error> { + if let Some(five_tuples) = &five_tuples { + if five_tuples.is_empty() { + return Ok(HashMap::new()); + } + } + + let tx = self.command_tx.lock().await.clone(); + #[allow(clippy::map_err_ignore)] + if let Some(tx) = tx { + let (infos_tx, mut infos_rx) = mpsc::channel(1); + + _ = tx + .send(Command::GetAllocationsInfo(five_tuples, infos_tx)) + .map_err(|_| Error::Closed)?; + + let mut info: HashMap = HashMap::new(); + + for _ in 0..tx.receiver_count() { + info.extend(infos_rx.recv().await.ok_or(Error::Closed)?); + } + + Ok(info) + } else { + Err(Error::Closed) + } + } + + /// Spawns a message handler task for the given [`Conn`]. + fn spawn_read_loop( + conn: Arc, + allocation_manager: Arc, + nonces: Arc>>, + auth_handler: Arc, + realm: String, + channel_bind_lifetime: Duration, + mut handle_rx: broadcast::Receiver, + ) { + let (mut close_tx, mut close_rx) = oneshot::channel::<()>(); + + drop(tokio::spawn({ + let allocation_manager = Arc::clone(&allocation_manager); + + async move { + loop { + match handle_rx.recv().await { + Ok(Command::DeleteAllocations(name, completion)) => { + allocation_manager + .delete_allocations_by_username(name.as_str()) + .await; + drop(completion); + continue; + } + Ok(Command::GetAllocationsInfo(five_tuples, tx)) => { + let infos = allocation_manager + .get_allocations_info(&five_tuples); + drop(tx.send(infos).await); + + continue; + } + Err(RecvError::Closed) => { + close_rx.close(); + break; + } + Ok(Command::Close(completion)) => { + close_rx.close(); + drop(completion); + break; + } + Err(RecvError::Lagged(n)) => { + log::warn!( + "Turn server has lagged by {} messages", + n + ); + continue; + } + } + } + } + })); + + drop(tokio::spawn(async move { + let local_con_addr = conn.local_addr(); + let protocol = conn.proto(); + + loop { + let (msg, src_addr) = tokio::select! { + v = conn.recv_from() => { + match v { + Ok(v) => v, + Err(err) => { + log::debug!("exit read loop on error: {}", err); + break; + } + } + }, + () = close_tx.closed() => break + }; + + let handle = request::handle_message( + msg, + &conn, + FiveTuple { + src_addr, + dst_addr: local_con_addr, + protocol, + }, + realm.as_str(), + channel_bind_lifetime, + &allocation_manager, + &nonces, + &auth_handler, + ); + + if let Err(err) = handle.await { + log::error!("error when handling datagram: {}", err); + } + } + + drop(allocation_manager.close().await); + drop(conn.close().await); + })); + } + + /// Close stops the TURN Server. It cleans up any associated state and + /// closes all connections it is managing. + pub async fn close(&self) { + let tx = self.command_tx.lock().await.take(); + + if let Some(tx) = tx { + if tx.receiver_count() == 0 { + return; + } + + let (closed_tx, closed_rx) = mpsc::channel(1); + drop(tx.send(Command::Close(Arc::new(closed_rx)))); + closed_tx.closed().await; + } + } +} + +impl fmt::Debug for Server { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Server") + .field("realm", &self.realm) + .field("channel_bind_lifetime", &self.channel_bind_lifetime) + .field("nonces", &self.nonces) + .field("command_tx", &self.command_tx) + .field("auth_handler", &"dyn AuthHandler") + .finish() + } +} + +/// The protocol to communicate between the [`Server`]'s public methods +/// and the tasks spawned in the [`Server::spawn_read_loop`] method. +#[derive(Clone)] +enum Command { + /// Command to delete [`Allocation`][`Allocation`] by provided `username`. + /// + /// [`Allocation`]: `crate::allocation::Allocation` + DeleteAllocations(String, Arc>), + + /// Command to get information of [`Allocation`][`Allocation`]s by provided + /// [`FiveTuple`]s. + /// + /// [`Allocation`]: `crate::allocation::Allocation` + GetAllocationsInfo( + Option>, + mpsc::Sender>, + ), + + /// Command to close the [`Server`]. + Close(Arc>), +} diff --git a/src/server/request.rs b/src/server/request.rs new file mode 100644 index 000000000..2eb5e7549 --- /dev/null +++ b/src/server/request.rs @@ -0,0 +1,1069 @@ +//! Ingress STUN/TURN messages handlers. + +use bytecodec::{DecodeExt, EncodeExt}; +use std::{ + collections::HashMap, + marker::{Send, Sync}, + mem, + net::SocketAddr, + sync::Arc, +}; + +use rand::{distributions::Alphanumeric, random, Rng}; +use stun_codec::{ + rfc5389::{ + errors::{BadRequest, StaleNonce, Unauthorized, UnknownAttribute}, + methods::BINDING, + }, + rfc5766::{ + errors::{ + AllocationMismatch, InsufficientCapacity, + UnsupportedTransportProtocol, + }, + methods::{ALLOCATE, CHANNEL_BIND, CREATE_PERMISSION, REFRESH, SEND}, + }, + rfc8656::errors::{AddressFamilyNotSupported, PeerAddressFamilyMismatch}, + Attribute as _, Message, MessageClass, MessageDecoder, MessageEncoder, + TransactionId, +}; +use tokio::{ + sync::Mutex, + time::{Duration, Instant}, +}; + +use crate::{ + allocation::{FiveTuple, Manager}, + attr::{ + AddressFamily, Attribute, ChannelNumber, Data, DontFragment, ErrorCode, + EvenPort, Fingerprint, Lifetime, MessageIntegrity, Nonce, Realm, + RequestedAddressFamily, RequestedTransport, ReservationToken, + UnknownAttributes, Username, XorMappedAddress, XorPeerAddress, + XorRelayAddress, PROTO_UDP, + }, + chandata::ChannelData, + con::Conn, + server::DEFAULT_LIFETIME, + AuthHandler, Error, +}; + +/// It is RECOMMENDED that the server use a maximum allowed lifetime value of no +/// more than 3600 seconds (1 hour). +const MAXIMUM_ALLOCATION_LIFETIME: Duration = Duration::from_secs(3600); + +/// Lifetime of the NONCE sent by server. +const NONCE_LIFETIME: Duration = Duration::from_secs(3600); + +/// Handles the given STUN/TURN message according to [spec]. +/// +/// [spec]: https://datatracker.ietf.org/doc/html/rfc5389#section-7.3 +#[allow(clippy::too_many_arguments)] +pub(crate) async fn handle_message( + mut raw: Vec, + conn: &Arc, + five_tuple: FiveTuple, + server_realm: &str, + channel_bind_lifetime: Duration, + allocs: &Arc, + nonces: &Arc>>, + auth_handler: &Arc, +) -> Result<(), Error> { + if ChannelData::is_channel_data(&raw) { + let data = ChannelData::decode(mem::take(&mut raw))?; + + handle_data_packet(data, five_tuple, allocs).await + } else { + use stun_codec::MessageClass::{Indication, Request}; + + let msg = MessageDecoder::::new() + .decode_from_bytes(&raw) + .map_err(|e| Error::Decode(*e.kind()))? + .map_err(|e| Error::Decode(*e.error().kind()))?; + + let auth = match (msg.method(), msg.class()) { + ( + ALLOCATE | REFRESH | CREATE_PERMISSION | CHANNEL_BIND, + Request, + ) => { + authenticate_request( + &msg, + auth_handler, + conn, + nonces, + five_tuple, + server_realm, + ) + .await? + } + _ => None, + }; + + match (msg.method(), msg.class()) { + (ALLOCATE, Request) => { + if let Some((uname, realm, pass)) = auth { + handle_allocate_request( + msg, conn, allocs, five_tuple, uname, realm, pass, + ) + .await + } else { + Ok(()) + } + } + (REFRESH, Request) => { + if let Some((uname, realm, pass)) = auth { + handle_refresh_request( + msg, conn, allocs, five_tuple, uname, realm, pass, + ) + .await + } else { + Ok(()) + } + } + (CREATE_PERMISSION, Request) => { + if let Some((uname, realm, pass)) = auth { + handle_create_permission_request( + msg, conn, allocs, five_tuple, uname, realm, pass, + ) + .await + } else { + Ok(()) + } + } + (CHANNEL_BIND, Request) => { + if let Some((uname, realm, pass)) = auth { + handle_channel_bind_request( + msg, + conn, + allocs, + five_tuple, + channel_bind_lifetime, + uname, + realm, + pass, + ) + .await + } else { + Ok(()) + } + } + (BINDING, Request) => { + handle_binding_request(conn, five_tuple).await + } + (SEND, Indication) => { + handle_send_indication(msg, allocs, five_tuple).await + } + (_, _) => Err(Error::UnexpectedClass), + } + } +} + +/// Relays the given [`ChannelData`]. +async fn handle_data_packet( + data: ChannelData, + five_tuple: FiveTuple, + allocs: &Arc, +) -> Result<(), Error> { + if let Some(alloc) = allocs.get_alloc(&five_tuple) { + let channel = alloc.get_channel_addr(&data.num()).await; + if let Some(peer) = channel { + alloc.relay(&data.data(), peer).await?; + + Ok(()) + } else { + Err(Error::NoSuchChannelBind) + } + } else { + Err(Error::NoAllocationFound) + } +} + +/// Handles the given STUN [`Message`] as an [AllocateRequest]. +/// +/// [AllocateRequest]: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 +#[allow(clippy::too_many_lines)] +async fn handle_allocate_request( + msg: Message, + conn: &Arc, + allocs: &Arc, + five_tuple: FiveTuple, + uname: Username, + realm: Realm, + pass: Box, +) -> Result<(), Error> { + // 1. The server MUST require that the request be authenticated. This + // authentication MUST be done using the long-term credential + // mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2] + // unless the client and server agree to use another mechanism through + // some procedure outside the scope of this document. + + let mut requested_port = 0; + let mut reservation_token: Option = None; + let mut use_ipv4 = true; + + // 2. The server checks if the 5-tuple is currently in use by an existing + // allocation. If yes, the server rejects the request with a 437 + // (Allocation Mismatch) error. + if allocs.has_alloc(&five_tuple) { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(AllocationMismatch)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::RelayAlreadyAllocatedForFiveTuple); + } + + // 3. The server checks if the request contains a REQUESTED-TRANSPORT + // attribute. If the REQUESTED-TRANSPORT attribute is not included or is + // malformed, the server rejects the request with a 400 (Bad Request) + // error. Otherwise, if the attribute is included but specifies a + // protocol other that UDP, the server rejects the request with a 442 + // (Unsupported Transport Protocol) error. + let Some(requested_proto) = msg + .get_attribute::() + .map(RequestedTransport::protocol) + else { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(BadRequest)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::AttributeNotFound); + }; + + if requested_proto != PROTO_UDP { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(UnsupportedTransportProtocol)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::UnsupportedRelayProto); + } + + // 4. The request may contain a DONT-FRAGMENT attribute. If it does, but + // the server does not support sending UDP datagrams with the DF bit set + // to 1 (see Section 12), then the server treats the DONT- FRAGMENT + // attribute in the Allocate request as an unknown comprehension-required + // attribute. + if msg.get_attribute::().is_some() { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(UnknownAttribute)); + msg.add_attribute(UnknownAttributes::new( + vec![DontFragment.get_type()], + )); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::NoDontFragmentSupport); + } + + // 5. The server checks if the request contains a RESERVATION-TOKEN + // attribute. If yes, and the request also contains an EVEN-PORT + // attribute, then the server rejects the request with a 400 (Bad + // Request) error. Otherwise, it checks to see if the token is valid + // (i.e., the token is in range and has not expired and the corresponding + // relayed transport address is still available). If the token is not + // valid for some reason, the server rejects the request with a 508 + // (Insufficient Capacity) error. + let has_reservation_token = + msg.get_attribute::().is_some(); + let even_port = msg.get_attribute::(); + + if has_reservation_token && even_port.is_some() { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(BadRequest)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::RequestWithReservationTokenAndEvenPort); + } + + // RFC 6156, Section 4.2: + // + // If it contains both a RESERVATION-TOKEN and a + // REQUESTED-ADDRESS-FAMILY, the server replies with a 400 + // (Bad Request) Allocate error response. + // + // 4.2.1. Unsupported Address Family + // This document defines the following new error response code: + // 440 (Address Family not Supported): The server does not support the + // address family requested by the client. + if let Some(req_family) = msg + .get_attribute::() + .map(RequestedAddressFamily::address_family) + { + if has_reservation_token { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(AddressFamilyNotSupported)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::RequestWithReservationTokenAndReqAddressFamily); + } + + if req_family == AddressFamily::V6 { + use_ipv4 = false; + } + } + + // 6. The server checks if the request contains an EVEN-PORT attribute. If + // yes, then the server checks that it can satisfy the request (i.e., can + // allocate a relayed transport address as described below). If the + // server cannot satisfy the request, then the server rejects the request + // with a 508 (Insufficient Capacity) error. + if even_port.is_some() { + let mut random_port = 1; + + while random_port % 2 != 0 { + random_port = match allocs.get_random_even_port().await { + Ok(port) => port, + Err(err) => { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(InsufficientCapacity)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(err); + } + }; + } + + requested_port = random_port; + reservation_token = Some(random()); + } + + // 7. At any point, the server MAY choose to reject the request with a 486 + // (Allocation Quota Reached) error if it feels the client is trying to + // exceed some locally defined allocation quota. The server is free to + // define this allocation quota any way it wishes, but SHOULD define it + // based on the username used to authenticate the request, and not on the + // client's transport address. + + // 8. Also at any point, the server MAY choose to reject the request with a + // 300 (Try Alternate) error if it wishes to redirect the client to a + // different server. The use of this error code and attribute follow the + // specification in [RFC5389]. + let lifetime_duration = get_lifetime(&msg); + let a = match allocs + .create_allocation( + five_tuple, + Arc::clone(conn), + requested_port, + lifetime_duration, + uname.clone(), + use_ipv4, + ) + .await + { + Ok(a) => a, + Err(err) => { + let mut msg = Message::new( + MessageClass::ErrorResponse, + ALLOCATE, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(InsufficientCapacity)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(err); + } + }; + + // Once the allocation is created, the server replies with a success + // response. The success response contains: + // * An XOR-RELAYED-ADDRESS attribute containing the relayed transport + // address. + // * A LIFETIME attribute containing the current value of the time-to- + // expiry timer. + // * A RESERVATION-TOKEN attribute (if a second relayed transport address + // was reserved). + // * An XOR-MAPPED-ADDRESS attribute containing the client's IP address + // and port (from the 5-tuple). + + let msg = { + if let Some(token) = reservation_token { + allocs + .create_reservation(token, a.relay_addr().port()) + .await; + } + + let mut msg = Message::new( + MessageClass::SuccessResponse, + ALLOCATE, + msg.transaction_id(), + ); + + msg.add_attribute(XorRelayAddress::new(a.relay_addr())); + msg.add_attribute( + Lifetime::new(lifetime_duration) + .map_err(|e| Error::Encode(*e.kind()))?, + ); + msg.add_attribute(XorMappedAddress::new(five_tuple.src_addr)); + + if let Some(token) = reservation_token { + msg.add_attribute(ReservationToken::new(token)); + } + + let integrity = MessageIntegrity::new_long_term_credential( + &msg, &uname, &realm, &pass, + ) + .map_err(|e| Error::Encode(*e.kind()))?; + msg.add_attribute(integrity); + + msg + }; + + build_and_send(conn, five_tuple.src_addr, msg).await +} + +/// Authenticates the given [`Message`]. +async fn authenticate_request( + msg: &Message, + auth_handler: &Arc, + conn: &Arc, + nonces: &Arc>>, + five_tuple: FiveTuple, + realm: &str, +) -> Result)>, Error> { + let Some(integrity) = msg.get_attribute::() else { + respond_with_nonce( + msg, + ErrorCode::from(Unauthorized), + conn, + realm, + five_tuple, + nonces, + ) + .await?; + return Ok(None); + }; + + let mut bad_request_msg = Message::new( + MessageClass::ErrorResponse, + msg.method(), + msg.transaction_id(), + ); + bad_request_msg.add_attribute(ErrorCode::from(BadRequest)); + + let Some(nonce_attr) = &msg.get_attribute::() else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + return Err(Error::AttributeNotFound); + }; + + let to_be_deleted = { + // Assert Nonce exists and is not expired + let mut nonces = nonces.lock().await; + + let to_be_deleted = nonces.get(nonce_attr.value()).map_or( + true, + |nonce_creation_time| { + Instant::now() + .checked_duration_since(*nonce_creation_time) + .unwrap_or_else(|| Duration::from_secs(0)) + >= NONCE_LIFETIME + }, + ); + + if to_be_deleted { + _ = nonces.remove(nonce_attr.value()); + } + to_be_deleted + }; + + if to_be_deleted { + respond_with_nonce( + msg, + ErrorCode::from(StaleNonce), + conn, + realm, + five_tuple, + nonces, + ) + .await?; + return Ok(None); + } + + let Some(uname_attr) = msg.get_attribute::() else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + return Err(Error::AttributeNotFound); + }; + let Some(realm_attr) = msg.get_attribute::() else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + return Err(Error::AttributeNotFound); + }; + + let Ok(pass) = auth_handler.auth_handle( + uname_attr.name(), + realm_attr.text(), + five_tuple.src_addr, + ) else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + return Err(Error::NoSuchUser); + }; + + if let Err(err) = + integrity.check_long_term_credential(uname_attr, realm_attr, &pass) + { + let mut unauthorized_msg = Message::new( + MessageClass::ErrorResponse, + msg.method(), + msg.transaction_id(), + ); + unauthorized_msg.add_attribute(err); + + answer_with_err(conn, five_tuple.src_addr, unauthorized_msg).await?; + + Err(Error::IntegrityMismatch) + } else { + Ok(Some((uname_attr.clone(), realm_attr.clone(), pass))) + } +} + +/// Sends a [`MessageClass::SuccessResponse`] message with a +/// [`XorMappedAddress`] attribute to the given [`Conn`]. +async fn handle_binding_request( + conn: &Arc, + five_tuple: FiveTuple, +) -> Result<(), Error> { + log::trace!("received BindingRequest from {}", five_tuple.src_addr); + + let mut msg = Message::new( + MessageClass::SuccessResponse, + BINDING, + TransactionId::new(random()), + ); + msg.add_attribute(XorMappedAddress::new(five_tuple.src_addr)); + let fingerprint = + Fingerprint::new(&msg).map_err(|e| Error::Encode(*e.kind()))?; + msg.add_attribute(fingerprint); + + build_and_send(conn, five_tuple.src_addr, msg).await +} + +/// Handle the given [`Message`] as [Refresh Request]. +/// +/// [Refresh Request]: https://datatracker.ietf.org/doc/html/rfc5766#section-7.2 +async fn handle_refresh_request( + msg: Message, + conn: &Arc, + allocs: &Arc, + five_tuple: FiveTuple, + uname: Username, + realm: Realm, + pass: Box, +) -> Result<(), Error> { + log::trace!("received RefreshRequest from {}", five_tuple.src_addr); + + let lifetime_duration = get_lifetime(&msg); + if lifetime_duration == Duration::from_secs(0) { + allocs.delete_allocation(&five_tuple).await; + } else if let Some(a) = allocs.get_alloc(&five_tuple) { + // If a server receives a Refresh Request with a + // REQUESTED-ADDRESS-FAMILY attribute, and the + // attribute's value doesn't match the address + // family of the allocation, the server MUST reply with a 443 + // (Peer Address Family Mismatch) Refresh error + // response. [RFC 6156, Section 5.2] + if let Some(family) = msg + .get_attribute::() + .map(RequestedAddressFamily::address_family) + { + if (family == AddressFamily::V6 && !a.relay_addr().is_ipv6()) + || (family == AddressFamily::V4 && !a.relay_addr().is_ipv4()) + { + let mut msg = Message::new( + MessageClass::ErrorResponse, + REFRESH, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(PeerAddressFamilyMismatch)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::PeerAddressFamilyMismatch); + } + } + a.refresh(lifetime_duration).await; + } else { + return Err(Error::NoAllocationFound); + } + + let mut msg = Message::new( + MessageClass::SuccessResponse, + REFRESH, + msg.transaction_id(), + ); + msg.add_attribute( + Lifetime::new(lifetime_duration) + .map_err(|e| Error::Encode(*e.kind()))?, + ); + let integrity = + MessageIntegrity::new_long_term_credential(&msg, &uname, &realm, &pass) + .map_err(|e| Error::Encode(*e.kind()))?; + msg.add_attribute(integrity); + + build_and_send(conn, five_tuple.src_addr, msg).await +} + +/// Handles the given [`Message`] as a [CreatePermission Request][1]. +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-9.2 +async fn handle_create_permission_request( + msg: Message, + conn: &Arc, + allocs: &Arc, + five_tuple: FiveTuple, + uname: Username, + realm: Realm, + pass: Box, +) -> Result<(), Error> { + log::trace!("received CreatePermission from {}", five_tuple.src_addr); + + let Some(alloc) = allocs.get_alloc(&five_tuple) else { + return Err(Error::NoAllocationFound); + }; + + let mut add_count = 0; + + for attr in msg.attributes() { + let Attribute::XorPeerAddress(attr) = attr else { + continue; + }; + let addr = attr.address(); + + // If an XOR-PEER-ADDRESS attribute contains an address of an + // address family different than that of the relayed transport + // address for the allocation, the server MUST generate an error + // response with the 443 (Peer Address Family Mismatch) response + // code. [RFC 6156, Section 6.2] + if (addr.is_ipv4() && !alloc.relay_addr().is_ipv4()) + || (addr.is_ipv6() && !alloc.relay_addr().is_ipv6()) + { + let mut msg = Message::new( + MessageClass::ErrorResponse, + CREATE_PERMISSION, + msg.transaction_id(), + ); + msg.add_attribute(ErrorCode::from(PeerAddressFamilyMismatch)); + + answer_with_err(conn, five_tuple.src_addr, msg).await?; + + return Err(Error::PeerAddressFamilyMismatch); + } + + log::trace!("adding permission for {}", addr); + + alloc.add_permission(addr.ip()).await; + add_count += 1; + } + + let resp_class = if add_count > 0 { + MessageClass::SuccessResponse + } else { + MessageClass::ErrorResponse + }; + + let msg = { + let mut msg = + Message::new(resp_class, CREATE_PERMISSION, msg.transaction_id()); + let integrity = MessageIntegrity::new_long_term_credential( + &msg, &uname, &realm, &pass, + ) + .map_err(|e| Error::Encode(*e.kind()))?; + msg.add_attribute(integrity); + + msg + }; + + build_and_send(conn, five_tuple.src_addr, msg).await +} + +/// Handles the given [`Message`] as a [Send Indication][1]. +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-10.2 +async fn handle_send_indication( + msg: Message, + allocs: &Arc, + five_tuple: FiveTuple, +) -> Result<(), Error> { + log::trace!("received SendIndication from {}", five_tuple.src_addr); + + let Some(a) = allocs.get_alloc(&five_tuple) else { + return Err(Error::NoAllocationFound); + }; + + let data_attr = msg + .get_attribute::() + .ok_or(Error::AttributeNotFound)?; + let peer_address = msg + .get_attribute::() + .map(XorPeerAddress::address) + .ok_or(Error::AttributeNotFound)?; + + let has_perm = a.has_permission(&peer_address).await; + if !has_perm { + return Err(Error::NoPermission); + } + + // TODO: dont clone + a.relay(data_attr.data(), peer_address) + .await + .map_err(Into::into) +} + +/// Handles the given [`Message`] as a [ChannelBind Request][1]. +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc5766#section-11.2 +#[allow(clippy::too_many_arguments)] +async fn handle_channel_bind_request( + msg: Message, + conn: &Arc, + allocs: &Arc, + five_tuple: FiveTuple, + channel_bind_lifetime: Duration, + uname: Username, + realm: Realm, + pass: Box, +) -> Result<(), Error> { + if let Some(alloc) = allocs.get_alloc(&five_tuple) { + let mut bad_request_msg = Message::new( + MessageClass::ErrorResponse, + CHANNEL_BIND, + msg.transaction_id(), + ); + bad_request_msg.add_attribute(ErrorCode::from(BadRequest)); + + let Some(ch_num) = + msg.get_attribute::().map(|a| a.value()) + else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + + return Err(Error::AttributeNotFound); + }; + let Some(peer_addr) = msg + .get_attribute::() + .map(XorPeerAddress::address) + else { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + + return Err(Error::AttributeNotFound); + }; + + // If the XOR-PEER-ADDRESS attribute contains an address of + // an address family different than that + // of the relayed transport address for the + // allocation, the server MUST generate an error response + // with the 443 (Peer Address Family + // Mismatch) response code. [RFC 6156, Section 7.2] + if (peer_addr.is_ipv4() && !alloc.relay_addr().is_ipv4()) + || (peer_addr.is_ipv6() && !alloc.relay_addr().is_ipv6()) + { + let mut peer_address_family_mismatch_msg = Message::new( + MessageClass::ErrorResponse, + CHANNEL_BIND, + msg.transaction_id(), + ); + peer_address_family_mismatch_msg + .add_attribute(ErrorCode::from(PeerAddressFamilyMismatch)); + + answer_with_err( + conn, + five_tuple.src_addr, + peer_address_family_mismatch_msg, + ) + .await?; + + return Err(Error::PeerAddressFamilyMismatch); + } + + log::trace!("binding channel {ch_num} to {peer_addr}",); + + if let Err(err) = alloc + .add_channel_bind(ch_num, peer_addr, channel_bind_lifetime) + .await + { + answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; + + return Err(err); + } + + let mut msg = Message::new( + MessageClass::SuccessResponse, + CHANNEL_BIND, + msg.transaction_id(), + ); + + let integrity = MessageIntegrity::new_long_term_credential( + &msg, &uname, &realm, &pass, + ) + .map_err(|e| Error::Encode(*e.kind()))?; + msg.add_attribute(integrity); + + build_and_send(conn, five_tuple.src_addr, msg).await + } else { + Err(Error::NoAllocationFound) + } +} + +/// Responds the given [`Message`] with a [`MessageClass::ErrorResponse`] with +/// a new random nonce. +async fn respond_with_nonce( + msg: &Message, + response_code: ErrorCode, + conn: &Arc, + realm: &str, + five_tuple: FiveTuple, + nonces: &Arc>>, +) -> Result<(), Error> { + let nonce: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect(); + + { + // Nonce has already been taken + let mut nonces = nonces.lock().await; + if nonces.contains_key(&nonce) { + return Err(Error::RequestReplay); + } + _ = nonces.insert(nonce.clone(), Instant::now()); + } + + let mut msg = Message::new( + MessageClass::ErrorResponse, + msg.method(), + msg.transaction_id(), + ); + msg.add_attribute(response_code); + msg.add_attribute(Nonce::new(nonce).map_err(|e| Error::Encode(*e.kind()))?); + msg.add_attribute( + Realm::new(realm.to_owned()).map_err(|e| Error::Encode(*e.kind()))?, + ); + + build_and_send(conn, five_tuple.src_addr, msg).await +} + +/// Encodes and sends the provided [`Message`] to the given [`SocketAddr`] +/// via given [`Conn`]. +async fn build_and_send( + conn: &Arc, + dst: SocketAddr, + msg: Message, +) -> Result<(), Error> { + let bytes = MessageEncoder::new() + .encode_into_bytes(msg) + .map_err(|e| Error::Encode(*e.kind()))?; + + _ = conn.send_to(bytes, dst).await?; + Ok(()) +} + +/// Send a STUN packet and return the original error to the caller +async fn answer_with_err( + conn: &Arc, + dst: SocketAddr, + msg: Message, +) -> Result<(), Error> { + build_and_send(conn, dst, msg).await?; + + Ok(()) +} + +/// Calculates a [`Lifetime`] fetching it from the given [`Message`] and +/// ensuring that it is not greater than configured +/// [`MAXIMUM_ALLOCATION_LIFETIME`]. +fn get_lifetime(m: &Message) -> Duration { + m.get_attribute::() + .map(Lifetime::lifetime) + .map_or(DEFAULT_LIFETIME, |lifetime| { + if lifetime > MAXIMUM_ALLOCATION_LIFETIME { + DEFAULT_LIFETIME + } else { + lifetime + } + }) +} + +#[cfg(test)] +mod request_test { + use std::{net::IpAddr, str::FromStr}; + + use tokio::{ + net::UdpSocket, + time::{Duration, Instant}, + }; + + use crate::{allocation::ManagerConfig, relay::RelayAllocator}; + + use super::*; + + const STATIC_KEY: &str = "ABC"; + + #[tokio::test] + async fn test_allocation_lifetime_parsing() { + let lifetime = Lifetime::new(Duration::from_secs(5)).unwrap(); + + let mut m = Message::new( + MessageClass::Request, + ALLOCATE, + TransactionId::new(random()), + ); + let lifetime_duration = get_lifetime(&m); + + assert_eq!( + lifetime_duration, DEFAULT_LIFETIME, + "Allocation lifetime should be default time duration" + ); + + m.add_attribute(lifetime.clone()); + + let lifetime_duration = get_lifetime(&m); + assert_eq!( + lifetime_duration, + lifetime.lifetime(), + "Expect lifetime_duration is {lifetime:?}, but \ + {lifetime_duration:?}" + ); + } + + #[tokio::test] + async fn test_allocation_lifetime_overflow() { + let lifetime = Lifetime::new(MAXIMUM_ALLOCATION_LIFETIME * 2).unwrap(); + + let mut m2 = Message::new( + MessageClass::Request, + ALLOCATE, + TransactionId::new(random()), + ); + m2.add_attribute(lifetime); + + let lifetime_duration = get_lifetime(&m2); + assert_eq!( + lifetime_duration, DEFAULT_LIFETIME, + "Expect lifetime_duration is {DEFAULT_LIFETIME:?}, \ + but {lifetime_duration:?}" + ); + } + + struct TestAuthHandler; + impl AuthHandler for TestAuthHandler { + fn auth_handle( + &self, + _username: &str, + _realm: &str, + _src_addr: SocketAddr, + ) -> Result, Error> { + Ok(STATIC_KEY.to_owned().into()) + } + } + + #[tokio::test] + async fn test_allocation_lifetime_deletion_zero_lifetime() { + let conn: Arc = + Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + + let allocation_manager = Arc::new(Manager::new(ManagerConfig { + relay_addr_generator: RelayAllocator { + relay_address: IpAddr::from([127, 0, 0, 1]), + min_port: 49152, + max_port: 65535, + max_retries: 10, + address: String::from("127.0.0.1"), + }, + alloc_close_notify: None, + })); + + let socket = + SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 5000); + let five_tuple = FiveTuple { + src_addr: socket, + dst_addr: conn.local_addr(), + protocol: conn.proto(), + }; + let nonces = Arc::new(Mutex::new(HashMap::new())); + + nonces + .lock() + .await + .insert(STATIC_KEY.to_owned(), Instant::now()); + + allocation_manager + .create_allocation( + five_tuple, + Arc::clone(&conn), + 0, + Duration::from_secs(3600), + Username::new(String::from("user")).unwrap(), + true, + ) + .await + .unwrap(); + + assert!(allocation_manager.get_alloc(&five_tuple).is_some()); + + let mut m: Message = Message::new( + MessageClass::Request, + REFRESH, + TransactionId::new(random()), + ); + m.add_attribute(Lifetime::new(Duration::default()).unwrap()); + m.add_attribute(Nonce::new(STATIC_KEY.to_owned()).unwrap()); + m.add_attribute(Realm::new(STATIC_KEY.to_owned()).unwrap()); + m.add_attribute(Username::new(STATIC_KEY.to_owned()).unwrap()); + let integrity = MessageIntegrity::new_long_term_credential( + &m, + &Username::new(STATIC_KEY.to_owned()).unwrap(), + &Realm::new(STATIC_KEY.to_owned()).unwrap(), + STATIC_KEY, + ) + .unwrap(); + m.add_attribute(integrity); + + let bytes = MessageEncoder::new().encode_into_bytes(m).unwrap(); + + let auth: Arc = + Arc::new(TestAuthHandler {}); + handle_message( + bytes, + &conn, + five_tuple, + STATIC_KEY, + Duration::from_secs(60), + &allocation_manager, + &nonces, + &auth, + ) + .await + .unwrap(); + + assert!(allocation_manager.get_alloc(&five_tuple).is_none()); + } +} diff --git a/srtp/.gitignore b/srtp/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/srtp/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/srtp/CHANGELOG.md b/srtp/CHANGELOG.md deleted file mode 100644 index aec0b9870..000000000 --- a/srtp/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -# webrtc-srtp changelog - -## Unreleased - -## v0.9.1 - -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - - -## v0.9.0 - -* [#8 update deps + loosen some requirements](https://github.com/webrtc-rs/srtp/pull/8) by [@melekes](https://github.com/melekes). -* Increased min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). - -## Prior to 0.8.9 - -Before 0.8.9 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/srtp/releases). - diff --git a/srtp/Cargo.toml b/srtp/Cargo.toml deleted file mode 100644 index 166ea3dde..000000000 --- a/srtp/Cargo.toml +++ /dev/null @@ -1,57 +0,0 @@ -[package] -name = "webrtc-srtp" -version = "0.13.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of SRTP" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-srtp" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/srtp" - -[features] -openssl = ["dep:openssl"] -vendored-openssl = ["openssl/vendored"] - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = [ - "conn", - "buffer", - "marshal", -] } -rtp = { version = "0.11.0", path = "../rtp" } -rtcp = { version = "0.11.0", path = "../rtcp" } - -byteorder = "1" -bytes = "1" -thiserror = "1" -hmac = { version = "0.12", features = ["std"] } -sha1 = "0.10" -ctr = "0.9" -aes = "0.8" -subtle = "2" -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -log = "0.4" -aead = { version = "0.5", features = ["std"] } -aes-gcm = { version = "0.10", features = ["std"] } -openssl = { version = "0.10.57", optional = true } - -[dev-dependencies] -criterion = { version = "0.5", features = ["async_futures"] } -tokio-test = "0.4" -lazy_static = "1" - -[[bench]] -name = "srtp_bench" -harness = false diff --git a/srtp/LICENSE-APACHE b/srtp/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/srtp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/srtp/LICENSE-MIT b/srtp/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/srtp/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/srtp/README.md b/srtp/README.md deleted file mode 100644 index c6eb608be..000000000 --- a/srtp/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of SRTP. Rewrite Pion SRTP in Rust -

diff --git a/srtp/benches/srtp_bench.rs b/srtp/benches/srtp_bench.rs deleted file mode 100644 index 17f2d44c2..000000000 --- a/srtp/benches/srtp_bench.rs +++ /dev/null @@ -1,158 +0,0 @@ -use bytes::BytesMut; -use criterion::{criterion_group, criterion_main, Criterion}; -use util::Marshal; -use webrtc_srtp::{context::Context, protection_profile::ProtectionProfile}; - -const MASTER_KEY: &[u8] = &[ - 96, 180, 31, 4, 119, 137, 128, 252, 75, 194, 252, 44, 63, 56, 61, 55, -]; -const MASTER_SALT: &[u8] = &[247, 26, 49, 94, 99, 29, 79, 94, 5, 111, 252, 216, 62, 195]; -const RAW_RTCP: &[u8] = &[ - 0x81, 0xc8, 0x00, 0x0b, 0xca, 0xfe, 0xba, 0xbe, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, - 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, -]; - -fn benchmark_encrypt_rtp_aes_128_cm_hmac_sha1(c: &mut Criterion) { - let mut ctx = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap(); - - let mut pld = BytesMut::new(); - for i in 0..1200 { - pld.extend_from_slice(&[i as u8]); - } - - c.bench_function("Benchmark RTP encrypt", |b| { - let mut seq = 1; - b.iter_batched( - || { - let pkt = rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq, - timestamp: seq.into(), - extension_profile: 48862, - marker: true, - padding: false, - extension: true, - payload_type: 96, - ..Default::default() - }, - payload: pld.clone().into(), - }; - seq += 1; - pkt.marshal().unwrap() - }, - |pkt_raw| { - ctx.encrypt_rtp(&pkt_raw).unwrap(); - }, - criterion::BatchSize::LargeInput, - ); - }); -} - -fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(c: &mut Criterion) { - let mut setup_ctx = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap(); - - let mut ctx = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap(); - - let mut pld = BytesMut::new(); - for i in 0..1200 { - pld.extend_from_slice(&[i as u8]); - } - - c.bench_function("Benchmark RTP decrypt", |b| { - let mut seq = 1; - b.iter_batched( - || { - let pkt = rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: seq, - timestamp: seq.into(), - extension_profile: 48862, - marker: true, - padding: false, - extension: true, - payload_type: 96, - ..Default::default() - }, - payload: pld.clone().into(), - }; - seq += 1; - setup_ctx.encrypt_rtp(&pkt.marshal().unwrap()).unwrap() - }, - |encrypted| ctx.decrypt_rtp(&encrypted).unwrap(), - criterion::BatchSize::LargeInput, - ); - }); -} - -fn benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(c: &mut Criterion) { - let mut ctx = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap(); - - c.bench_function("Benchmark RTCP encrypt", |b| { - b.iter(|| { - ctx.encrypt_rtcp(RAW_RTCP).unwrap(); - }); - }); -} - -fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(c: &mut Criterion) { - let encrypted = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap() - .encrypt_rtcp(RAW_RTCP) - .unwrap(); - - let mut ctx = Context::new( - MASTER_KEY, - MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) - .unwrap(); - - c.bench_function("Benchmark RTCP decrypt", |b| { - b.iter(|| ctx.decrypt_rtcp(&encrypted).unwrap()); - }); -} - -criterion_group!( - benches, - benchmark_encrypt_rtp_aes_128_cm_hmac_sha1, - benchmark_decrypt_rtp_aes_128_cm_hmac_sha1, - benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1, - benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1 -); -criterion_main!(benches); diff --git a/srtp/codecov.yml b/srtp/codecov.yml deleted file mode 100644 index 2be1b3bbb..000000000 --- a/srtp/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: d65de923-7c3d-4836-8d9a-5183b356be4f - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/srtp/doc/webrtc.rs.png b/srtp/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 zcmb4qgL7ri^Y*>Tjm?d1I~&`!ZQHhO+t}FV#!hyV?8dg8{pItmdjEj8ZcW{)o}N>4 zx}T@J&zwF{it-W&usE;)002QsQdAiL01Ey$pdr5`Gl^x@001?ix2mSAvXKXoqqBp# zm8}_(tCyo0k(sBJIRN0fUYBc?uFIKR_Rj>P7X%1*;=-D%!e739>vp6Rv;H}K%AjN% z%DcHW3_g8n$P~)+@55m2gYff&+%v=3t}J8sh4Nu}Z#(Bu;NSg|$Mwm_$ETD)A6$R` zrT-_CFeWVkg zB7gp^)W4@cMDlL#-2Aql!*4&XKC#q4d>8(88yHBYX{~4BYQ>HRdws?~;s^PC;=ZrF z@MNC&nO~*t|CJFC9}0bc^c3jx{&xoY;Fu8Ta|y`z`-hZ5IR51JwxpYWg?BTv^+~kf zU+~U3+Q0eBKc4FIGbGpeb!zWDbH96|_vp@*6vMp5m-JN5{n-io@AoWcA41yqs`reS zzCM9Gff=NJ&D)ENf_JT=h@aKE-;{qDh2qY>bqyIuQs-;F;Os>|>f5NBB*Z)zS>01^ zYHIb`#ii-od{1Z8dgn^NzTDXQP`BLlg8GXC{teD|pYUHtz{Bvr2||ynMRbTQ|5=Q^ z=xCepqO_&09vLaG{)E+xpA+id_QWP z9NhvK2hg3arVWD|!TLZ`z``o#RpiX^F)##i0{YPj*wEZP^F-#Xv}0>qrKdwy_MWFP zJelPwLo~RgI!!aXpNyEaWwP(f%aKJ%^cA@-6YQKBJ_MuEG@Yr+F@pB0HC2b&)^&A{ z+}W+m&a`!HtKPVL?%?1qjJ=PmK7^!QgK>~+I6VQ^ld((Y*Pb;L5qKx6~E+?yuMxn%(Y8Gwqw+8y6n$x~*5$p9gdQ3|EFc zR(5r$dw+cDvg^CGes+i==bF}G(_*R^j@GCL2F7c%ZV5{((N?hE>1C#qhD>YYwuh+4 zlFL$#_zustzL^|+mbkfeen^~Q{`)h@MgQmD*d$l_<&U#R&YHGSm$SNGlyK}7e}D4( z`>#7TA|$OpC-leXjGvT8K=l!I%`50^T(C;VbM3Edft*R=r%h5Q?HmeQ5X7YqDrRJPoft#UYad_=m;>;ETqfA_uXdOBL=cu9JB{E$F-gVVoF;&dO{;C%YrVp;) zPB%dn6N3!zCBJBkTQj+mU=z{7=x@357Pr4`<#9bRw2v9yJb~bOJCaGLgSvtI^a~_; zpoH`UQ$Z#!S}p5*0>8HtXU>yJBc!OoU~3@2sPY0S6Ez{~~cM>p^!14V=!w?Tru|)Yb2A zoA?OH(gHH40Qaf55cJNXMB~19*UoD0a=M*ieUYlZU$J{cuiavQpjHQ3cCNm*_Mj4F zAV%>@DdeP$f?EUFi)kerkdQP#1R&gJ?m}l-;?Cuz^S*<~1YbTUR2&047A&ckI3a6Z|`JmmU_0Sj^OWAk%Bfgv5<6}c+rs{BbNkRrL zW9lA7Q~acKKAdKrNwqfcVXV$j0*L==bbQU4);_34>|}{ISs^ z+=QBD3xuJ$)K6C_qwdX+oLQ3*?h>=r*(~IZKf1I!fp>_CQ}=6Dv7UEMw7~3*G^+Uh z$50BRUd=$*dXF?OJJY|x9KDq5km0O@Ey>-@bUeE%!Pq=b0*c7)??4D*;@24J3z;n< zYTe1g49U2c2|MDpA36}RP+iZ<*G!X}1hw=%i`chjp`y1y5jK(Qg9eC{TdwM{bhz+!tMACo{i?5NK&}&s@cJvJrZ+Kfh+43lw$j^md$W9Z;2Rf9CB(qE4BX z%M+P0{Bl(`VAjXF8*^6T#fr*;ak`#P*x1Y`JDwzZo7w=ktR0hu6hyw|Yc6nvSu@K+ z)}y2AI9ao$+gFZcNl6@WuYp|bw=1=8Tc)JqBU0x_F}FGjRNY2xT=OiH4qtJib)=;u z3g^^xq)ZtPo8?2}&p#_=25N}M!W)~SB$C~#w}M|HtQwV6hY*a3Q+I$nUYCmm&z(%} zC}UWop&a}(mc}%Kw-^DL$5= zj0rRWUbHDV7I*@9TLBj17SG?Elr@7fzA^|P;HA-V7#1a^z`Lm`gK-DN3(A@&yYeAYLAXPyCym-# znC|}GLqjKn_PV?n7IH?XVBVwRuVyw~PY~;K)e&DLqR!$phbW$z zMjMo^CN*M24|5godW4g&06{xOAY%-S9t7FL@>7h=1M>cII1uC! zLj+HWyX6D5f;oor3JJQ5go@{dgjgCtFNIhGSVM*(HhNmD;FA?o3MgyX;mvNFzzpf+ z*?LW^;>a5!fv_c%6@INu2HQSi9~;f$ON3)uQ+wcF17jm-Lv4u7aP|g%SrVLL-IhU1 zR)-hO*ep)rTekEB-a(~)Xpc|%Yz8WfWTh1PDHhzT7 z(?FD2aA7cysf{O@jP3g3l7i1b*8yhBCrXZ@>tJ2pZ_5Zq1f~7Ak0HusxMdS1MYX@^!uE;B1M#SUi(2tHm?5FvyTS8B7Bbk|_uT-*aW!-B|Iv!Qg6spzp-zTu;$Zp8G6E&8w695FWfQWeLK zsN4noQ13EnuxpKoySdSaOWy?%%ZEE(2;c>UCn4e@Fm~1ejlBpK6Rp&Y7kgH5Oiq@0 z-(EAV7*=@CrP3$lb5->3m9|!6x{hAhCYTp2f1#ish$FO)|wL17Yh> zvb~6XZZsF$AR--Jafn;yIkfF@?9ma=?UoC6swl-}bx8HXgolyZ=L?n($Pvhly2j{D z!k^x;Vgo|OXGm`MD&*`PlbST>no0UN+r*^|8EW-tw!~Sp!ru!Or7Y0ljYnvVF)W&i zbg^LdI(TT-Y`m+GbVwUsPuEDi3mf?;OViv7S+bdI=jm4BAq;h-p2CP*Pfg36B z=PyY|QTSECa9}llrGo;VomOktjb%OQ3cxx6x^={a*lO1_ME5-Dh?GL zmQJdV)c5>OHnh@0GJIfT-LP+C4;k|x7UMxET*`SbNh-B;(WHE1egmN z51Fd@imCHWJdFZLSpvb8sq>1=NFh zo==R?!9n2cE7X|AzXKkIFbN9mqIxr;K~4A>!46~uo;GIgS!Xlk+NPb{%jw57tYr&@ z@nU^OKZlGY@&J{h*TI-@dqDbF*!mcj@~w`ku8S2B)YPO>6XZHVpg=fS0P~9Y2vO8~ zC+VqqC+(?b$nG2Y8}LC@(W0A4;u-NXrP3=d!GngYTx@H!F$ znHrA(L0Yua$O@pMsi2lVg*xOmFrDv&$*QvXDf+S#(BA5a3msN8*vu6@vj3<0Iq;;=@2Er@_#Cnv=$f9yUVP2kYb)Rl zpbm;@pS_!qAEfk|EgXWfb}&%hLO77{f|>U1ZCMps)CCeVlI|<36F<=Q7W6=+_}abCI{r zNr)lBDkWrWR#PVW@5o^_Sr`j1eJ(?iu1(Z|s-BJ$gh+JV)5)71X3us_E=I-{xKQZm4<%xH({H{ap>uIbCEEML&P9)FlP)vQ10@j!);yM$ zT@uS7!$OD&!{L=8E^66*87QS&RUn&?l?t0caI|K2N)$a%0R4m04iN157k%~*w0+45 z`W9Fd3%BB=fX#nv*cfb=p|`mEBK3=^l+=n@Rhj4|tp`sr)L-&84;Nl7K?IB*3&wDH z)?d4NavTZO{)YeDayVt2;ZL~5I*c%pnDTkENRB@*vv*lZ=A#AB@V9^;yJ@hUGmfCt zK62FjYv?%sE*}~^v?+$5nlFioC%k!!d2%>3H@NG-L=!Y606%lUUa3`-2pz87dK|w@ zP4zB&5q3Lh{LQ$K(QDe6DNpouXD6yT#Qje-DiL0T=k%Gd9GYs>`IDv+9lG*F$uct` zJTDO1&x))R3w7y;n-irj^&jnpAX&$*eIQT@= zu(EeL;V<3H{1u7Ot|670B>Z1Al0rPO$a`Md*DFDZp(7e30We~+7vfa1x2r}^G8reh z07e2fqniB|U zM8?6`G&%s~bT~46m$;jna?=$5nV|7(*ipL?k$tF&*>)wh$o&`r4qH&b7AzUe(Fo?J zU0}8>*l*;7>g;Vwu{;wm3| z>QFbLG1Ne9Ea6LY0Qz>Sbg2c?d-%dpma1*(R`bn@Y(CYBuv*HR zw1aF#?}h1^7Vn&wc^?dYPbUObl3}NIXaMim4?RejWP)XZVPP~bXY|mr$;xVb<8ni| zOc#@+#56hPAg7@ML73_~t`;@2$#i@XM{DwV-cZ<{%Z0thDpq2B^?PPlME@?U`q-m1Q=>%!QTZ;O3z((Z)ki z7kbQFwJalo&jY3+Mqs#5<#f~c!|n&9RS|yyiA^FSEltdY#J}GP#)!g+hAv7_D1`Op zfMOb@mB~(NiHfumqyN#L&SM$Um!mr}I6`!3%|xFvMa8=8vM@A`?wdZX^B`|YysZGB zkYJUE{g@O~Td96dlSZPi<+;+2;Hb7;w=A$$A(vW5UY>p`B3F-Ss6C|8aPC7x``co>t zU^T1*dLn-`qFNb;h}jxfgPNrw>+{BcJVEt zIVl@hB+=6?i?XNz87R&VEWvSue<7CL{ z2oG!vDCGUU$lDkZq01t_h(h9rd7Yft*h}L<67?&#Jc45Y1g_aSnc*B)#n=}mv7Mfu zN|VDJHHPzspa}U6JL{#8Z;0hkO#_VED$3R3G)&8t2u)>}8t`x`VPvXEbTMG}jgHDl z_SR3TJl`ir&qhk}WL5QuW^g(zx(K6$g_)@8p(RS>yg)1XrGl{DLXok+ zp&66PD?sXL(QekmJUyk3!irEZ7ldrp(ZMRtTGP7 znWi%=FtjctrWggEZR@=0>*dwxO%X9H)NgvOWY8DZ!>-chZgf7Oj*=%}_+gu>TFC<= zM}fk>G>GuHaix3_vDh9eR&TvKR$57dg_2EMt+$8Kh|%S>J{h`MRjvQK4;%rhja z%<~EnT4wpFM|QiP_HMsk{&mm|7cP(x)1MYIU9|dZnM@Q6@#5z;Q0-YkQDI;L961X_ z7HLemmqJ_1ZXG&z=r^E>USAcH9tmy8m|Bq!?plBUp-NscM3jiQ8RU0~MFJw4EZAoj zbcM!6h#JX3>44b$KtBoB##rxn_;A`P#t#~5(N=Ty*oII+>a)oIEc{$)2XVA(a0U<5 zoVMIqhB)Y~>Z`fPQk>>sNnnqQF&GdbtrW{eQ|W~tiuvx-V~CF@nxx~Y9>t($W{0+SObuMthm)2pE5H~fYOmHS2~z#-)mLYLy6G|l)^mnZq6hmRSn*JC1p%|20L;jz^yHOv)$aQP%$ zmflFf~79JvwG0vd)A$Un0nGx}|%%fqplB?S5p0lBsBRND~#9 zB98MK7);XA6y0t}t}NSAF!Hgjs2VUP{w;(TO0j@u$TnhP#G2+AW)?~ZI5DcV z)c{Nhymi57iyJz=ri4BzdS}n_t-6D{tFJG_VUG!h1x0a$oHkUdz5GH~oXHv!<-Mo26 zEvA{>8BrXS<$qDYx&|@Tc3XhtsxbjQGcb~QTA;_0DsD|K8>U(%n$R>z#uEOQ0OO@anz${U!&evo>1eCR)Qi;Lm z<*G@q#=BFxv_%KV=fC?a1Hs({xo)%q-a>c}l{QZ4^}3zc57qq*lbOR#pJvMucMY9Rnl6%mDP-yTzKi{*YC@g)CR2Jb)4rzzEgFWkUzen1 zMO$qSJfv6Rjfut|1NZG{EJ#+GzsE;SCh(E*G-fdKdxBT32 z*k(nmqP@G=Wz)mpA2j;rx4$m z#TS3W+lF>^-fozQS)u*m?KA_C6Gg^GGQCiE%=2>^RPaT9-ZOl?;cKg4kg>bA0k5O1 zja_d-ZmLxwAB#fEgUgbN8}u;Fj0UyESY|<@%tjZy{ch9yR1`h!INw}fJpIrOM795! z1u;MY=xK7hZV<_Fvt^)A?}B@zLLCDKuji-gRZuK+=Y3W$>-k70nr&F>0a*u1~gyzPZqg_7?Bp={YZX(h}sTFc5WDDJxt6Q8HA z{|@|c?eSJjKw}ws_@m~7?NJ2ZN<4?;l~@xZ$Y1TCb`QR#E4oDsKnbkwzVJ3!YK0|T zVRYq)ong5#4P8`|;7-v5=g28>!X@LW)WdclA@Iusvp_$z} z{i09t)xz|_!KN-LW*lCY)rza`nQ+l1Ey;c*7RkRa>Pu-J=O`el=q+SmPhn@7dgfV zrZRbSwh!xVvDOO#kCOs^z`xaCzz!x^G{D`GwoqBU-W-lBqOPA<;i02uhN*B7@PmaI zbfj^R8USXsoiblQhqQ=mv1g=nrAnQawu}w?Qwu%nf+9~eSlM1;pWqk%*02uJ2M6w1~H-zq315?xDlDqFk&_Co^xyHyovAAU! zQupRV_9qZn(5=i{v*x8nzE{y-&Un~XtrV?j^{HmaJT}H-F5`FwI!z|UYH`x<7+)3V z@UfG;gKX zW0(t&F_0s|)@JzT;a{OKDg~UN(hEvD2kI>+D~Vu< z75lpYi<{EQZSUD(56cI8b zvHa@&B6;w%a8%+c{gy{8!!{+$N?mSe>8ix`SE+ciUu1N5 zcBFbr&P*fk3P~gaSiC;{crW{V`uk!mHnNNOeoFyNXqUCiYIyqQ)uO)wyrHtJsGh6V z45o(N6BFrBA+PmzlxqiJeuRFuZNY5zsVDicXyJ$o*sda_yF15yapw?j7woa9|NZLL z4+z0rCYW`zRAI?^ZDFGmqG*;k^L;#Nl?w13CRyCEpA!)Fpj3ZWaOm2hG;+bLU%^6= zcB_WAZm{x!8ae{pK>9tw*_x;_>7_LIrBhK!vC%VMMP(jQgO>|~7% zEDuXXq}ZQGo%7PX%SKKAAzzy%sb&~2)jXIOP%BQJI(hGaF~0fcY40)$g(wOtqHm5~ zKa5C3&tnb2Mrfp_Biez3+f(oGB9$rr;{AHJ(5@+$(lI3c~W=QgPM94a*70RI#EewwS^co*l|{x|IDP*6IqKm8W?V zW}8Bbi);zqyL=VHJpY;8sxeg)FYo)pjVlV-p!`{wlpl}`cLeUUKD*!0Q1tljk zbv8L!Ke@{!@2`C~ZYrsmSAM|RfG(nRde&o5{6tB}3Qe@uSyvfTm99Q{PVPYvT_!i* z`3|cM${k(HQ>XT##JL+vv?F_GjeXw&Xmw#$#EJ+8SlGDJcAB&`*_9>?pd>>iU&CNz z+v7JX8{`6aMxi@m7tL1XSg?4`V-V6~-aY92gl- zusI&&P_p0FDT;iwk^L`94_gGz1juy87}+R=q7waP$DZmGxF3?%A4w}DL=pZ|1Bu@- zq9+nkV+H(gPw5O}UkG&ScI7@2RV*?bHn&NrhvqM=95N37P+h@GI^7{K8FfVXAAzKm zd49L=WdSMqB)pgESwXv%V51{D88If1=zz`OM=rq%mUwFA@2Hn?w|)cxy<#Y&I6q&>%bg7zJ=|1__bkL zIScrkD>2Fq=A9i3SBSk(EsuZS+_iMP3V!?re)Ua;T8W4#N{NX4zgEew2FYB%BtFSu zLA+4|6&W>c{Bxq+#3F7jWDKWe8uTLJN(^l`rp|k~Y&J4#hWKEc!NG*l>X4Y`dc^u* zqG#al{&DFs3Hc`@eM{ucb_e0rCEw*=pm6UrTa69gyz7Q6`K~cVHw~o0LI^*IDWqYV z&O|;r`5MpQ>H6k@VXV1l?t^Q{de%5lfh^xF+zQPBS;1i)^2l!&ns$=rhU+F%>KmE= ze%InY=PX19L_Rk2QkB;(bff06VOfaQ!g6FqWJz_oN?)Q?)1+5SvwD5gD&Gs72$!E( z))`ww(f~<1kx*zbjWpG;c&vIJIvn+Sg3oh=Zoeo)m@YY!GhHDVLem-4zmTmz+AkAP z=E_uvAbq?e6A<(zSX}m=FCmZo9_U&CT*G_ApbDI|pmc3r`yTpu3zDIJBfAMo_Yx4I z5Nd=k?Wcl?)dAtXmfhR>==2IGt*Y5aaCC%@#ia82v-jsuUhjub`N|fO zZmx5FJ&xeSR|hMkv5bT$;NO40!rqFMFUi-zvX;wNv+LS_11L$WH6BMdd(GWsPAY8C$HlG(5 zlHy1Mh>_sP;K1l$X_Q7o@Twxp96=(Y(L$_~#KioW%fDNMFiDA~%~Bi_=ZAfB!zI7- z@;Y4(H|KIUW+B3aixjEQVgJAWBtlt&ii@@2;&=;*kqd(ekpassLCA&}iN+!52rRXU zmoWeCLz^d^VaWqwGj=>GuYi2yp@228y1-RILu~mBvh7F&M40bnM*WnwCqSvm#V^HZ z8Vl0p)OneZKGfG_T3?J46?G!@A#ot?G_n_5X$@tOB3NZ5dYf<|_T@JwU11HNHU{Q6 ztZixo8Gau&H1;VBbt-@+ch>uu!mu=uSWpfh^MZ8H1NZvZae%7)T49*N;?i>aOvt++ zXwI%WHyqx-aPgItwO?DIqB*Uh-O&RMZ!R;^f$=9Zbt08kQPzLWbddQ!L3nw;}xhj8XGF0>TzgX zXAb%Ya9Lxu@*38K2*4dK65kYV4MuyfL~Wq5t6`;N*v;lCt|crM2#cS{d5RmRy{eg& zMllR~v0T{@4Zu)~Mfm%rccN5S0O>#_Qwgo86lNQ^0a2@zUWtR>BJ>=GTq0XUA}F@f z(_2nOtD5)1?x~a|Bo-K59rbCCe%;+9n#K`k2UdRt{+9Nibt^YH*QO?6)jr|bRM9KH z7M4i_hyxcUQM(4f*sj2%CedlYR8?n1d_=W`I6Wn?>Zc1qib^o=@P7yVzLc!R`ATd5 zCwU&IL$#)rIgaMQ9jN?Isi-*a^i@~&(QeJx9#&(A^ZF^+rP4RRdw=V%W11q*MAg^c znLR}m$o;#@ZYZFY%>Sbgk?p*MmQcUsan1j(Dg>=IxO}Jd{Vj`HH%PKF4g4t7+v+4M z#-eCev4KYA>-KjRS~6}^SH#nHIA&`k(VKHh^?TXYbpwLtlP@+ zb)|jChy`^(7l|zpIHb`|Niqd`1e|R|I^r(S&)|bVGAO17iBM-zEiD*BatW^mx5gWy zYve`i>4#sFT2Gf5Nx=}`!E=QgzE#4oi8y0ym*vZ+?Ijdq;;=7V6FP83rv9^2({|^) zf#e3@-_999V9U`2>q+`UwE??DzjK{ZQvqH1S>^N+o$1ssfne3e(EivEIx47=V9CfM zK!l(}$zZht@iJxbI72q;k{!3K1J*gUXa1QWNqaE+$+x$T*h80X7nIq8(+Mgd9icL; zdDf*63yi6yT1~Wvxpm`~*2JG!66PNwRT#;Km3fP_=VqZqSm1+}p5OMc+G5hyxPR_h zJGV>vX^EpmjafWj$3YXz@iD(uS}|$BZow}JnrFKcX#qn|i!}8QWh3HcpdN{yF$R^E znMz$m7~+t5nRiFku%a>g-=UP)p+S5}zTy;=W^7kemN?ZF6`%%F^HJ9W`V<)A0Ks`J z1+l5eN7|K}EHB|_Twyykb$RRJ1)u`=u9TfbO$@(yC0JlZpb1NK!hwCgvVsbG5Zs-) zEFb(WzHOV)aSI&%6lH^KzYLT+i6uxkL6vm8Y=BTmhPP5F9BWWfMWcN;&JaG>h$UKU zpGn1)RE5t712`00`Ns5XSCBt6ejJJA18nNy4Q)Uh6ruRCE;Gcc@GN{olkyC-WOubP z69E0#9aX|EYrCoai?Y4^4De+=0!Qi+c4GCjNqxgpHzW*R68w%TaPSsp85v~mncA(4 zWqtC>flK_XE6xg{Rfw9!Q$aCowZ6Tyj&nHV&_EFgY#HE|=UKxx^vg%4`HTV6-Wz0> zM-~9k|03T#2VzC0DQ%X38rIpo+ONB}Iv=LCNq2o~00@U8Sy=nr)!NP|TL>P1?2 z|7t;jtRF`sr0mn5uJ5oY*e3?%n9|Lp0=jTj%EqRTk%FS?eQ}4njrHfF1~5fiwI#>{ zZLSN+3FHZ1npT|`GPO~I(y*+GBTxm~QTpTfpR?7037xNXnc&tMzdGkMNKuvpIXo8| zf>$ShLNffRQiPl6I$dTw?gRVoRs!W8G!-ax>B#f#jPu9*9VHMK*`7~m8lW3N=|($$ z(b~q)|2c*`$YNz2iz=vL|M&_?FXUX&y}7tWKScYEQzb|S){NT~Dv|%ZQfZj+bh?8% zEDcF^XV!Aoo?Bf3c+ENt?wQN8xAJ%%koMA~-j7Gt`7=Esa!pnlOmDeL*0*ENTF(&J z$r7z34bY3qT2M)h^=RLiyuTTr>UPF#)+6$O$#YH7-mFI*K_cqz+mNF{h2~GExKsx_ zhMi_Y0O`A|MWihjnw9CDks)6}{)Wie6RMMTc3Bvtzq`Upq;p$aFnXFO;Nq)>$d24u zB!H{mHxiz|s|H-!s-@UidoO*TPl5c*_r!%&*F}oK7LivkOP?8rahQOUlG3bGKV7NQ zIdOP`&FFJEN~(|_jzZJ?u^uTg;F0LEh~K)4$1x;I`Kbg}$$5x(LvgmtK_;$2;~tnX zwlwz2i$)-;XRxM|o|l>`YHm&uhr>?unA!jN8lz+18J0q+&}6s4vEJ@raNby52v&A zIgiI9{%^0(QiqdC-@nf{l7vQ0y6If5l;H@ZlyKr)jFLc zg18!Ok)k>n-}NpP^yd~Hgy@boz%18p90g{VvWmZSd${cRrr;XH?BpZ~2S+;GHwN^V z-WBVE<~e@aYps6&k6(=@5}#W~a|K7gEeh!GH=OpyZsr93xshIM^%nraArrG-xB4gv zeXoTYlZ>UMVH+D8ODih6$H1TZAhqit6xt_b858LeePGnVG!NL>54^TM!PeeE*&F-& z^&*H@T@l&cG|lepBYqbH@}ZFPOc{gXYmhc zixSje>9 zDw=LpM~L>ya@=!SdHL;0MZxVw`xYkGPp8+GyOaYcC@3_n<9FT`O8H!>d#x`=3Vht7 z5eB?7f4P3GfOQ!sn5Rwvpr59r$2ie&2D_cptsUZ6&b4*-T3(U(Z7FRPJG^t zVT_E7G#m&7#^Q3uJM_JX2h47-p!>g^!Gp={Fk!v@)P3;*PR~d%UVt_^jKJf-Dwn9VLz$7 zS$R^1qG7>oy0P0J)OnOo`^ZB`m}ft~^BbD81gIYiI;^5_44Z6q!`i-( z7DK#VyndKZ$m;Fsp*i4Iw^_i3D68#BReHq`Y|j+YJ=P;DJ9De*R1Ye1?8Lv|3+?0X zp|$zDxjPyISNq%{YHM4&@u#Mnvj=Y<$N1zc>lJ63odR4%l}MtZqR#(xXME8D!+$&b zj^O2PPGHVs%TL1Bm$#~_Ds1(o@5`QeeBS6eGvTAgMr-wYym!;A5b;{Q-t1or=U=q_ zLd(sj%N&>8h$i~Yt(3Gh2?IlXXej7_YIbZ)q+Z>04Lc!#)*CbVp9?$q+C4|&z3air z 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 G`0_t~Oa}h| diff --git a/srtp/src/cipher/cipher_aead_aes_gcm.rs b/srtp/src/cipher/cipher_aead_aes_gcm.rs deleted file mode 100644 index cc880694b..000000000 --- a/srtp/src/cipher/cipher_aead_aes_gcm.rs +++ /dev/null @@ -1,247 +0,0 @@ -use aes_gcm::aead::generic_array::GenericArray; -use aes_gcm::aead::{Aead, Payload}; -use aes_gcm::{Aes128Gcm, KeyInit, Nonce}; -use byteorder::{BigEndian, ByteOrder}; -use bytes::{Bytes, BytesMut}; -use util::marshal::*; - -use super::Cipher; -use crate::error::{Error, Result}; -use crate::key_derivation::*; - -pub const CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN: usize = 16; - -const RTCP_ENCRYPTION_FLAG: u8 = 0x80; - -/// AEAD Cipher based on AES. -pub(crate) struct CipherAeadAesGcm { - srtp_cipher: aes_gcm::Aes128Gcm, - srtcp_cipher: aes_gcm::Aes128Gcm, - srtp_session_salt: Vec, - srtcp_session_salt: Vec, -} - -impl Cipher for CipherAeadAesGcm { - fn auth_tag_len(&self) -> usize { - CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN - } - - fn encrypt_rtp( - &mut self, - payload: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - // Grow the given buffer to fit the output. - let header_len = header.marshal_size(); - let mut writer = BytesMut::with_capacity(payload.len() + self.auth_tag_len()); - - // Copy header unencrypted. - writer.extend_from_slice(&payload[..header_len]); - - let nonce = self.rtp_initialization_vector(header, roc); - - let encrypted = self.srtp_cipher.encrypt( - Nonce::from_slice(&nonce), - Payload { - msg: &payload[header_len..], - aad: &writer, - }, - )?; - - writer.extend(encrypted); - Ok(writer.freeze()) - } - - fn decrypt_rtp( - &mut self, - ciphertext: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - if ciphertext.len() < self.auth_tag_len() { - return Err(Error::ErrFailedToVerifyAuthTag); - } - - let nonce = self.rtp_initialization_vector(header, roc); - let payload_offset = header.marshal_size(); - let decrypted_msg: Vec = self.srtp_cipher.decrypt( - Nonce::from_slice(&nonce), - Payload { - msg: &ciphertext[payload_offset..], - aad: &ciphertext[..payload_offset], - }, - )?; - - let mut writer = BytesMut::with_capacity(payload_offset + decrypted_msg.len()); - writer.extend_from_slice(&ciphertext[..payload_offset]); - writer.extend(decrypted_msg); - - Ok(writer.freeze()) - } - - fn encrypt_rtcp(&mut self, decrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - let iv = self.rtcp_initialization_vector(srtcp_index, ssrc); - let aad = self.rtcp_additional_authenticated_data(decrypted, srtcp_index); - - let encrypted_data = self.srtcp_cipher.encrypt( - Nonce::from_slice(&iv), - Payload { - msg: &decrypted[8..], - aad: &aad, - }, - )?; - - let mut writer = BytesMut::with_capacity(encrypted_data.len() + aad.len()); - writer.extend_from_slice(&decrypted[..8]); - writer.extend(encrypted_data); - writer.extend_from_slice(&aad[8..]); - - Ok(writer.freeze()) - } - - fn decrypt_rtcp(&mut self, encrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - if encrypted.len() < self.auth_tag_len() + SRTCP_INDEX_SIZE { - return Err(Error::ErrFailedToVerifyAuthTag); - } - - let nonce = self.rtcp_initialization_vector(srtcp_index, ssrc); - let aad = self.rtcp_additional_authenticated_data(encrypted, srtcp_index); - - let decrypted_data = self.srtcp_cipher.decrypt( - Nonce::from_slice(&nonce), - Payload { - msg: &encrypted[8..(encrypted.len() - SRTCP_INDEX_SIZE)], - aad: &aad, - }, - )?; - - let mut writer = BytesMut::with_capacity(8 + decrypted_data.len()); - writer.extend_from_slice(&encrypted[..8]); - writer.extend(decrypted_data); - - Ok(writer.freeze()) - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - let pos = input.len() - 4; - let val = BigEndian::read_u32(&input[pos..]); - - (val & !((RTCP_ENCRYPTION_FLAG as u32) << 24)) as usize - } -} - -impl CipherAeadAesGcm { - /// Create a new AEAD instance. - pub(crate) fn new(master_key: &[u8], master_salt: &[u8]) -> Result { - let srtp_session_key = aes_cm_key_derivation( - LABEL_SRTP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let srtp_block = GenericArray::from_slice(&srtp_session_key); - - let srtp_cipher = Aes128Gcm::new(srtp_block); - - let srtcp_session_key = aes_cm_key_derivation( - LABEL_SRTCP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let srtcp_block = GenericArray::from_slice(&srtcp_session_key); - - let srtcp_cipher = Aes128Gcm::new(srtcp_block); - - let srtp_session_salt = aes_cm_key_derivation( - LABEL_SRTP_SALT, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let srtcp_session_salt = aes_cm_key_derivation( - LABEL_SRTCP_SALT, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - Ok(CipherAeadAesGcm { - srtp_cipher, - srtcp_cipher, - srtp_session_salt, - srtcp_session_salt, - }) - } - - /// The 12-octet IV used by AES-GCM SRTP is formed by first concatenating - /// 2 octets of zeroes, the 4-octet SSRC, the 4-octet rollover counter - /// (ROC), and the 2-octet sequence number (SEQ). The resulting 12-octet - /// value is then XORed to the 12-octet salt to form the 12-octet IV. - /// - /// https://tools.ietf.org/html/rfc7714#section-8.1 - pub(crate) fn rtp_initialization_vector( - &self, - header: &rtp::header::Header, - roc: u32, - ) -> Vec { - let mut iv = vec![0u8; 12]; - BigEndian::write_u32(&mut iv[2..], header.ssrc); - BigEndian::write_u32(&mut iv[6..], roc); - BigEndian::write_u16(&mut iv[10..], header.sequence_number); - - for (i, v) in iv.iter_mut().enumerate() { - *v ^= self.srtp_session_salt[i]; - } - - iv - } - - /// The 12-octet IV used by AES-GCM SRTCP is formed by first - /// concatenating 2 octets of zeroes, the 4-octet SSRC identifier, - /// 2 octets of zeroes, a single "0" bit, and the 31-bit SRTCP index. - /// The resulting 12-octet value is then XORed to the 12-octet salt to - /// form the 12-octet IV. - /// - /// https://tools.ietf.org/html/rfc7714#section-9.1 - pub(crate) fn rtcp_initialization_vector(&self, srtcp_index: usize, ssrc: u32) -> Vec { - let mut iv = vec![0u8; 12]; - - BigEndian::write_u32(&mut iv[2..], ssrc); - BigEndian::write_u32(&mut iv[8..], srtcp_index as u32); - - for (i, v) in iv.iter_mut().enumerate() { - *v ^= self.srtcp_session_salt[i]; - } - - iv - } - - /// In an SRTCP packet, a 1-bit Encryption flag is prepended to the - /// 31-bit SRTCP index to form a 32-bit value we shall call the - /// "ESRTCP word" - /// - /// https://tools.ietf.org/html/rfc7714#section-17 - pub(crate) fn rtcp_additional_authenticated_data( - &self, - rtcp_packet: &[u8], - srtcp_index: usize, - ) -> Vec { - let mut aad = vec![0u8; 12]; - - aad[..8].copy_from_slice(&rtcp_packet[..8]); - - BigEndian::write_u32(&mut aad[8..], srtcp_index as u32); - - aad[8] |= RTCP_ENCRYPTION_FLAG; - aad - } -} diff --git a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs b/srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs deleted file mode 100644 index d4a8391a8..000000000 --- a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs +++ /dev/null @@ -1,220 +0,0 @@ -use aes::cipher::generic_array::GenericArray; -use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; -use bytes::{BufMut, Bytes}; -use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; -use subtle::ConstantTimeEq; -use util::marshal::*; - -use super::{Cipher, CipherInner}; -use crate::error::{Error, Result}; -use crate::key_derivation::*; - -type Aes128Ctr = ctr::Ctr128BE; - -pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - srtp_session_key: Vec, - srtcp_session_key: Vec, -} - -impl CipherAesCmHmacSha1 { - pub fn new(master_key: &[u8], master_salt: &[u8]) -> Result { - let inner = CipherInner::new(master_key, master_salt)?; - - let srtp_session_key = aes_cm_key_derivation( - LABEL_SRTP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - let srtcp_session_key = aes_cm_key_derivation( - LABEL_SRTCP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - Ok(CipherAesCmHmacSha1 { - inner, - srtp_session_key, - srtcp_session_key, - }) - } -} - -impl Cipher for CipherAesCmHmacSha1 { - fn auth_tag_len(&self) -> usize { - self.inner.auth_tag_len() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) - } - - fn encrypt_rtp( - &mut self, - plaintext: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - let mut writer = Vec::with_capacity(plaintext.len() + self.auth_tag_len()); - - // Write the plaintext to the destination buffer. - writer.extend_from_slice(plaintext); - - // Encrypt the payload - let counter = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - - // Generate the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.auth_tag_len()]; - writer.extend(auth_tag); - - Ok(Bytes::from(writer)) - } - - fn decrypt_rtp( - &mut self, - encrypted: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - if encrypted_len < self.auth_tag_len() { - return Err(Error::SrtpTooSmall(encrypted_len, self.auth_tag_len())); - } - - let mut writer = Vec::with_capacity(encrypted_len - self.auth_tag_len()); - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.auth_tag_len()..]; - let cipher_text = &encrypted[..encrypted_len - self.auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtpFailedToVerifyAuthTag); - } - - // Write cipher_text to the destination buffer. - writer.extend_from_slice(cipher_text); - - // Decrypt the ciphertext for the payload. - let counter = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - - Ok(Bytes::from(writer)) - } - - fn encrypt_rtcp(&mut self, decrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - let mut writer = - Vec::with_capacity(decrypted.len() + SRTCP_INDEX_SIZE + self.auth_tag_len()); - - // Write the decrypted to the destination buffer. - writer.extend_from_slice(decrypted); - - // Encrypt everything after header - let counter = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - - // Add SRTCP index and set Encryption bit - writer.put_u32(srtcp_index as u32 | (1u32 << 31)); - - // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.auth_tag_len()]; - writer.extend(auth_tag); - - Ok(Bytes::from(writer)) - } - - fn decrypt_rtcp(&mut self, encrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - let encrypted_len = encrypted.len(); - if encrypted_len < self.auth_tag_len() + SRTCP_INDEX_SIZE { - return Err(Error::SrtcpTooSmall( - encrypted_len, - self.auth_tag_len() + SRTCP_INDEX_SIZE, - )); - } - - let tail_offset = encrypted_len - (self.auth_tag_len() + SRTCP_INDEX_SIZE); - - let mut writer = Vec::with_capacity(tail_offset); - - writer.extend_from_slice(&encrypted[0..tail_offset]); - - let is_encrypted = encrypted[tail_offset] >> 7; - if is_encrypted == 0 { - return Ok(Bytes::from(writer)); - } - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.auth_tag_len()..]; - if actual_tag.len() != self.auth_tag_len() { - return Err(Error::RtcpInvalidLengthAuthTag( - actual_tag.len(), - self.auth_tag_len(), - )); - } - - let cipher_text = &encrypted[..encrypted_len - self.auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtcpFailedToVerifyAuthTag); - } - - let counter = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - - stream.seek(0); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - - Ok(Bytes::from(writer)) - } -} diff --git a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs b/srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs deleted file mode 100644 index 00bae63ae..000000000 --- a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs +++ /dev/null @@ -1,133 +0,0 @@ -use byteorder::{BigEndian, ByteOrder}; -use hmac::{Hmac, Mac}; -use sha1::Sha1; - -use super::Cipher; -use crate::error::{Error, Result}; -use crate::key_derivation::*; -use crate::protection_profile::*; - -#[cfg(not(feature = "openssl"))] -mod ctrcipher; - -#[cfg(feature = "openssl")] -mod opensslcipher; - -#[cfg(not(feature = "openssl"))] -pub(crate) use ctrcipher::CipherAesCmHmacSha1; - -#[cfg(feature = "openssl")] -pub(crate) use opensslcipher::CipherAesCmHmacSha1; - -type HmacSha1 = Hmac; - -pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; - -pub(crate) struct CipherInner { - srtp_session_salt: Vec, - srtp_session_auth: HmacSha1, - srtcp_session_salt: Vec, - srtcp_session_auth: HmacSha1, -} - -impl CipherInner { - pub fn new(master_key: &[u8], master_salt: &[u8]) -> Result { - let srtp_session_salt = aes_cm_key_derivation( - LABEL_SRTP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - let srtcp_session_salt = aes_cm_key_derivation( - LABEL_SRTCP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - - let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); - - let srtp_session_auth_tag = aes_cm_key_derivation( - LABEL_SRTP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - let srtcp_session_auth_tag = aes_cm_key_derivation( - LABEL_SRTCP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - - let srtp_session_auth = HmacSha1::new_from_slice(&srtp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - let srtcp_session_auth = HmacSha1::new_from_slice(&srtcp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - srtp_session_salt, - srtp_session_auth, - srtcp_session_salt, - srtcp_session_auth, - }) - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// In the case of SRTP, M SHALL consist of the Authenticated - /// Portion of the packet (as specified in Figure 1) concatenated with - /// the roc, M = Authenticated Portion || roc; - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtp_auth_tag(&self, buf: &[u8], roc: u32) -> [u8; 20] { - let mut signer = self.srtp_session_auth.clone(); - - signer.update(buf); - - // For SRTP only, we need to hash the rollover counter as well. - signer.update(&roc.to_be_bytes()); - - signer.finalize().into_bytes().into() - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtcp_auth_tag(&self, buf: &[u8]) -> [u8; 20] { - let mut signer = self.srtcp_session_auth.clone(); - - signer.update(buf); - - signer.finalize().into_bytes().into() - } - - fn auth_tag_len(&self) -> usize { - CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - let tail_offset = input.len() - (self.auth_tag_len() + SRTCP_INDEX_SIZE); - (BigEndian::read_u32(&input[tail_offset..tail_offset + SRTCP_INDEX_SIZE]) & !(1 << 31)) - as usize - } -} diff --git a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs b/srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs deleted file mode 100644 index 48ce6c7a4..000000000 --- a/srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs +++ /dev/null @@ -1,261 +0,0 @@ -use bytes::{BufMut, Bytes}; -use openssl::cipher_ctx::CipherCtx; -use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; -use subtle::ConstantTimeEq; -use util::marshal::*; - -use super::{Cipher, CipherInner}; -use crate::{ - error::{Error, Result}, - key_derivation::*, -}; - -pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - rtp_ctx: CipherCtx, - rtcp_ctx: CipherCtx, -} - -impl CipherAesCmHmacSha1 { - pub fn new(master_key: &[u8], master_salt: &[u8]) -> Result { - let inner = CipherInner::new(master_key, master_salt)?; - - let srtp_session_key = aes_cm_key_derivation( - LABEL_SRTP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - let srtcp_session_key = aes_cm_key_derivation( - LABEL_SRTCP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let t = openssl::cipher::Cipher::aes_128_ctr(); - let mut rtp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtp_ctx - .encrypt_init(Some(t), Some(&srtp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - let t = openssl::cipher::Cipher::aes_128_ctr(); - let mut rtcp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtcp_ctx - .encrypt_init(Some(t), Some(&srtcp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - inner, - rtp_ctx, - rtcp_ctx, - }) - } -} - -impl Cipher for CipherAesCmHmacSha1 { - fn auth_tag_len(&self) -> usize { - self.inner.auth_tag_len() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) - } - - fn encrypt_rtp( - &mut self, - plaintext: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - let header_len = header.marshal_size(); - let mut writer = Vec::with_capacity(plaintext.len() + self.auth_tag_len()); - - // Copy the header unencrypted. - writer.extend_from_slice(&plaintext[..header_len]); - - // Encrypt the payload - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - writer.resize(plaintext.len(), 0); - self.rtp_ctx.encrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&plaintext[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - // Generate and write the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.auth_tag_len()]; - writer.extend_from_slice(auth_tag); - - Ok(Bytes::from(writer)) - } - - fn decrypt_rtp( - &mut self, - encrypted: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - if encrypted_len < self.auth_tag_len() { - return Err(Error::SrtpTooSmall(encrypted_len, self.auth_tag_len())); - } - let header_len = header.marshal_size(); - - let mut writer = Vec::with_capacity(encrypted_len - self.auth_tag_len()); - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.auth_tag_len()..]; - let cipher_text = &encrypted[..encrypted_len - self.auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtpFailedToVerifyAuthTag); - } - - // Write cipher_text to the destination buffer. - writer.extend_from_slice(&cipher_text[..header_len]); - - // Decrypt the ciphertext for the payload. - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - - writer.resize(encrypted_len - self.auth_tag_len(), 0); - self.rtp_ctx.decrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&cipher_text[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - Ok(Bytes::from(writer)) - } - - fn encrypt_rtcp(&mut self, decrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - let decrypted_len = decrypted.len(); - - let mut writer = Vec::with_capacity(decrypted_len + SRTCP_INDEX_SIZE + self.auth_tag_len()); - - // Write the decrypted to the destination buffer. - writer.extend_from_slice(&decrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - // Encrypt everything after header - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(decrypted_len, 0); - self.rtcp_ctx - .encrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &decrypted[HEADER_LENGTH + SSRC_LENGTH..], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - // Add SRTCP index and set Encryption bit - writer.put_u32(srtcp_index as u32 | (1u32 << 31)); - - // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.auth_tag_len()]; - writer.extend(auth_tag); - - Ok(Bytes::from(writer)) - } - - fn decrypt_rtcp(&mut self, encrypted: &[u8], srtcp_index: usize, ssrc: u32) -> Result { - let encrypted_len = encrypted.len(); - - if encrypted_len < self.auth_tag_len() + SRTCP_INDEX_SIZE { - return Err(Error::SrtcpTooSmall( - encrypted_len, - self.auth_tag_len() + SRTCP_INDEX_SIZE, - )); - } - - let tail_offset = encrypted_len - (self.auth_tag_len() + SRTCP_INDEX_SIZE); - - let mut writer = Vec::with_capacity(tail_offset); - - writer.extend_from_slice(&encrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - let is_encrypted = encrypted[tail_offset] >> 7; - if is_encrypted == 0 { - return Ok(Bytes::from(writer)); - } - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.auth_tag_len()..]; - if actual_tag.len() != self.auth_tag_len() { - return Err(Error::RtcpInvalidLengthAuthTag( - actual_tag.len(), - self.auth_tag_len(), - )); - } - - let cipher_text = &encrypted[..encrypted_len - self.auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtcpFailedToVerifyAuthTag); - } - - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(tail_offset, 0); - self.rtcp_ctx - .decrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &encrypted[HEADER_LENGTH + SSRC_LENGTH..tail_offset], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - Ok(Bytes::from(writer)) - } -} diff --git a/srtp/src/cipher/mod.rs b/srtp/src/cipher/mod.rs deleted file mode 100644 index ce3cc192b..000000000 --- a/srtp/src/cipher/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -pub mod cipher_aead_aes_gcm; -pub mod cipher_aes_cm_hmac_sha1; - -use bytes::Bytes; - -use crate::error::Result; - -///NOTE: Auth tag and AEAD auth tag are placed at the different position in SRTCP -/// -///In non-AEAD cipher, the authentication tag is placed *after* the ESRTCP word -///(Encrypted-flag and SRTCP index). -/// -///> AES_128_CM_HMAC_SHA1_80 -///> | RTCP Header | Encrypted payload |E| SRTCP Index | Auth tag | -///> ^ |----------| -///> | ^ -///> | authTagLen=10 -///> aeadAuthTagLen=0 -/// -///In AEAD cipher, the AEAD authentication tag is embedded in the ciphertext. -///It is *before* the ESRTCP word (Encrypted-flag and SRTCP index). -/// -///> AEAD_AES_128_GCM -///> | RTCP Header | Encrypted payload | AEAD auth tag |E| SRTCP Index | -///> |---------------| ^ -///> ^ authTagLen=0 -///> aeadAuthTagLen=16 -/// -///See https://tools.ietf.org/html/rfc7714 for the full specifications. - -/// Cipher represents a implementation of one -/// of the SRTP Specific ciphers. -pub(crate) trait Cipher { - /// Get authenticated tag length. - fn auth_tag_len(&self) -> usize; - - /// Retrieved RTCP index. - fn get_rtcp_index(&self, input: &[u8]) -> usize; - - /// Encrypt RTP payload. - fn encrypt_rtp( - &mut self, - payload: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result; - - /// Decrypt RTP payload. - fn decrypt_rtp( - &mut self, - payload: &[u8], - header: &rtp::header::Header, - roc: u32, - ) -> Result; - - /// Encrypt RTCP payload. - fn encrypt_rtcp(&mut self, payload: &[u8], srtcp_index: usize, ssrc: u32) -> Result; - - /// Decrypt RTCP payload. - fn decrypt_rtcp(&mut self, payload: &[u8], srtcp_index: usize, ssrc: u32) -> Result; -} diff --git a/srtp/src/config.rs b/srtp/src/config.rs deleted file mode 100644 index 8fbab5f22..000000000 --- a/srtp/src/config.rs +++ /dev/null @@ -1,83 +0,0 @@ -use util::KeyingMaterialExporter; - -use crate::error::Result; -use crate::option::*; -use crate::protection_profile::*; - -const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; - -/// SessionKeys bundles the keys required to setup an SRTP session -#[derive(Default, Debug, Clone)] -pub struct SessionKeys { - pub local_master_key: Vec, - pub local_master_salt: Vec, - pub remote_master_key: Vec, - pub remote_master_salt: Vec, -} - -/// Config is used to configure a session. -/// You can provide either a KeyingMaterialExporter to export keys -/// or directly pass the keys themselves. -/// After a Config is passed to a session it must not be modified. -#[derive(Default)] -pub struct Config { - pub keys: SessionKeys, - pub profile: ProtectionProfile, - //LoggerFactory: logging.LoggerFactory - /// List of local/remote context options. - /// ReplayProtection is enabled on remote context by default. - /// Default replay protection window size is 64. - pub local_rtp_options: Option, - pub remote_rtp_options: Option, - - pub local_rtcp_options: Option, - pub remote_rtcp_options: Option, -} - -impl Config { - /// ExtractSessionKeysFromDTLS allows setting the Config SessionKeys by - /// extracting them from DTLS. This behavior is defined in RFC5764: - /// - pub async fn extract_session_keys_from_dtls( - &mut self, - exporter: impl KeyingMaterialExporter, - is_client: bool, - ) -> Result<()> { - let key_len = self.profile.key_len(); - let salt_len = self.profile.salt_len(); - - let keying_material = exporter - .export_keying_material( - LABEL_EXTRACTOR_DTLS_SRTP, - &[], - (key_len * 2) + (salt_len * 2), - ) - .await?; - - let mut offset = 0; - let client_write_key = keying_material[offset..offset + key_len].to_vec(); - offset += key_len; - - let server_write_key = keying_material[offset..offset + key_len].to_vec(); - offset += key_len; - - let client_write_salt = keying_material[offset..offset + salt_len].to_vec(); - offset += salt_len; - - let server_write_salt = keying_material[offset..offset + salt_len].to_vec(); - - if is_client { - self.keys.local_master_key = client_write_key; - self.keys.local_master_salt = client_write_salt; - self.keys.remote_master_key = server_write_key; - self.keys.remote_master_salt = server_write_salt; - } else { - self.keys.local_master_key = server_write_key; - self.keys.local_master_salt = server_write_salt; - self.keys.remote_master_key = client_write_key; - self.keys.remote_master_salt = client_write_salt; - } - - Ok(()) - } -} diff --git a/srtp/src/context/context_test.rs b/srtp/src/context/context_test.rs deleted file mode 100644 index b2114c981..000000000 --- a/srtp/src/context/context_test.rs +++ /dev/null @@ -1,305 +0,0 @@ -use bytes::Bytes; -use lazy_static::lazy_static; - -use super::*; -use crate::key_derivation::*; - -const CIPHER_CONTEXT_ALGO: ProtectionProfile = ProtectionProfile::Aes128CmHmacSha1_80; -const DEFAULT_SSRC: u32 = 0; - -#[test] -fn test_context_roc() -> Result<()> { - let key_len = CIPHER_CONTEXT_ALGO.key_len(); - let salt_len = CIPHER_CONTEXT_ALGO.salt_len(); - - let mut c = Context::new( - &vec![0; key_len], - &vec![0; salt_len], - CIPHER_CONTEXT_ALGO, - None, - None, - )?; - - let roc = c.get_roc(123); - assert!(roc.is_none(), "ROC must return None for unused SSRC"); - - c.set_roc(123, 100); - let roc = c.get_roc(123); - if let Some(r) = roc { - assert_eq!(r, 100, "ROC is set to 100, but returned {r}") - } else { - panic!("ROC must return value for used SSRC"); - } - - Ok(()) -} - -#[test] -fn test_context_index() -> Result<()> { - let key_len = CIPHER_CONTEXT_ALGO.key_len(); - let salt_len = CIPHER_CONTEXT_ALGO.salt_len(); - - let mut c = Context::new( - &vec![0; key_len], - &vec![0; salt_len], - CIPHER_CONTEXT_ALGO, - None, - None, - )?; - - let index = c.get_index(123); - assert!(index.is_none(), "Index must return None for unused SSRC"); - - c.set_index(123, 100); - let index = c.get_index(123); - if let Some(i) = index { - assert_eq!(i, 100, "Index is set to 100, but returned {i}"); - } else { - panic!("Index must return true for used SSRC") - } - - Ok(()) -} - -#[test] -fn test_key_len() -> Result<()> { - let key_len = CIPHER_CONTEXT_ALGO.key_len(); - let salt_len = CIPHER_CONTEXT_ALGO.salt_len(); - - let result = Context::new(&[], &vec![0; salt_len], CIPHER_CONTEXT_ALGO, None, None); - assert!(result.is_err(), "CreateContext accepted a 0 length key"); - - let result = Context::new(&vec![0; key_len], &[], CIPHER_CONTEXT_ALGO, None, None); - assert!(result.is_err(), "CreateContext accepted a 0 length salt"); - - let result = Context::new( - &vec![0; key_len], - &vec![0; salt_len], - CIPHER_CONTEXT_ALGO, - None, - None, - ); - assert!( - result.is_ok(), - "CreateContext failed with a valid length key and salt" - ); - - Ok(()) -} - -#[test] -fn test_valid_packet_counter() -> Result<()> { - let master_key = vec![ - 0x0d, 0xcd, 0x21, 0x3e, 0x4c, 0xbc, 0xf2, 0x8f, 0x01, 0x7f, 0x69, 0x94, 0x40, 0x1e, 0x28, - 0x89, - ]; - let master_salt = vec![ - 0x62, 0x77, 0x60, 0x38, 0xc0, 0x6d, 0xc9, 0x41, 0x9f, 0x6d, 0xd9, 0x43, 0x3e, 0x7c, - ]; - - let srtp_session_salt = aes_cm_key_derivation( - LABEL_SRTP_SALT, - &master_key, - &master_salt, - 0, - master_salt.len(), - )?; - - let s = SrtpSsrcState { - ssrc: 4160032510, - ..Default::default() - }; - let expected_counter = [ - 0xcf, 0x90, 0x1e, 0xa5, 0xda, 0xd3, 0x2c, 0x15, 0x00, 0xa2, 0x24, 0xae, 0xae, 0xaf, 0x00, - 0x00, - ]; - let counter = generate_counter(32846, s.rollover_counter, s.ssrc, &srtp_session_salt); - assert_eq!( - counter, expected_counter, - "Session Key {counter:?} does not match expected {expected_counter:?}", - ); - - Ok(()) -} - -#[test] -fn test_rollover_count() -> Result<()> { - let mut s = SrtpSsrcState { - ssrc: DEFAULT_SSRC, - ..Default::default() - }; - - // Set initial seqnum - let roc = s.next_rollover_count(65530); - assert_eq!(roc, 0, "Initial rolloverCounter must be 0"); - s.update_rollover_count(65530); - - // Invalid packets never update ROC - s.next_rollover_count(0); - s.next_rollover_count(0x4000); - s.next_rollover_count(0x8000); - s.next_rollover_count(0xFFFF); - s.next_rollover_count(0); - - // We rolled over to 0 - let roc = s.next_rollover_count(0); - assert_eq!(roc, 1, "rolloverCounter was not updated after it crossed 0"); - s.update_rollover_count(0); - - let roc = s.next_rollover_count(65530); - assert_eq!( - roc, 0, - "rolloverCounter was not updated when it rolled back, failed to handle out of order" - ); - s.update_rollover_count(65530); - - let roc = s.next_rollover_count(5); - assert_eq!( - roc, 1, - "rolloverCounter was not updated when it rolled over initial, to handle out of order" - ); - s.update_rollover_count(5); - - s.next_rollover_count(6); - s.update_rollover_count(6); - - s.next_rollover_count(7); - s.update_rollover_count(7); - - let roc = s.next_rollover_count(8); - assert_eq!( - roc, 1, - "rolloverCounter was improperly updated for non-significant packets" - ); - s.update_rollover_count(8); - - // valid packets never update ROC - let roc = s.next_rollover_count(0x4000); - assert_eq!( - roc, 1, - "rolloverCounter was improperly updated for non-significant packets" - ); - s.update_rollover_count(0x4000); - - let roc = s.next_rollover_count(0x8000); - assert_eq!( - roc, 1, - "rolloverCounter was improperly updated for non-significant packets" - ); - s.update_rollover_count(0x8000); - - let roc = s.next_rollover_count(0xFFFF); - assert_eq!( - roc, 1, - "rolloverCounter was improperly updated for non-significant packets" - ); - s.update_rollover_count(0xFFFF); - - let roc = s.next_rollover_count(0); - assert_eq!( - roc, 2, - "rolloverCounter must be incremented after wrapping, got {roc}" - ); - - Ok(()) -} - -lazy_static! { - static ref MASTER_KEY: Bytes = Bytes::from_static(&[ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, - ]); - static ref MASTER_SALT: Bytes = Bytes::from_static(&[ - 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, - ]); - static ref DECRYPTED_RTP_PACKET: Bytes = Bytes::from_static(&[ - 0x80, 0x0f, 0x12, 0x34, 0xde, 0xca, 0xfb, 0xad, 0xca, 0xfe, 0xba, 0xbe, 0xab, 0xab, 0xab, - 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, - ]); - static ref ENCRYPTED_RTP_PACKET: Bytes = Bytes::from_static(&[ - 0x80, 0x0f, 0x12, 0x34, 0xde, 0xca, 0xfb, 0xad, 0xca, 0xfe, 0xba, 0xbe, 0xc5, 0x00, 0x2e, - 0xde, 0x04, 0xcf, 0xdd, 0x2e, 0xb9, 0x11, 0x59, 0xe0, 0x88, 0x0a, 0xa0, 0x6e, 0xd2, 0x97, - 0x68, 0x26, 0xf7, 0x96, 0xb2, 0x01, 0xdf, 0x31, 0x31, 0xa1, 0x27, 0xe8, 0xa3, 0x92, - ]); - static ref DECRYPTED_RTCP_PACKET: Bytes = Bytes::from_static(&[ - 0x81, 0xc8, 0x00, 0x0b, 0xca, 0xfe, 0xba, 0xbe, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, - 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, - ]); - static ref ENCRYPTED_RTCP_PACKET: Bytes = Bytes::from_static(&[ - 0x81, 0xc8, 0x00, 0x0b, 0xca, 0xfe, 0xba, 0xbe, 0xc9, 0x8b, 0x8b, 0x5d, 0xf0, 0x39, 0x2a, - 0x55, 0x85, 0x2b, 0x6c, 0x21, 0xac, 0x8e, 0x70, 0x25, 0xc5, 0x2c, 0x6f, 0xbe, 0xa2, 0xb3, - 0xb4, 0x46, 0xea, 0x31, 0x12, 0x3b, 0xa8, 0x8c, 0xe6, 0x1e, 0x80, 0x00, 0x00, 0x01, - ]); -} - -#[test] -fn test_encrypt_rtp() { - let mut ctx = Context::new( - &MASTER_KEY, - &MASTER_SALT, - ProtectionProfile::AeadAes128Gcm, - None, - None, - ) - .expect("Error creating srtp context"); - - let gotten_encrypted_rtp_packet = ctx - .encrypt_rtp(&DECRYPTED_RTP_PACKET) - .expect("Error encrypting rtp payload"); - - assert_eq!(gotten_encrypted_rtp_packet, *ENCRYPTED_RTP_PACKET) -} - -#[test] -fn test_decrypt_rtp() { - let mut ctx = Context::new( - &MASTER_KEY, - &MASTER_SALT, - ProtectionProfile::AeadAes128Gcm, - None, - None, - ) - .expect("Error creating srtp context"); - - let gotten_decrypted_rtp_packet = ctx - .decrypt_rtp(&ENCRYPTED_RTP_PACKET) - .expect("Error decrypting rtp payload"); - - assert_eq!(gotten_decrypted_rtp_packet, *DECRYPTED_RTP_PACKET) -} - -#[test] -fn test_encrypt_rtcp() { - let mut ctx = Context::new( - &MASTER_KEY, - &MASTER_SALT, - ProtectionProfile::AeadAes128Gcm, - None, - None, - ) - .expect("Error creating srtp context"); - - let gotten_encrypted_rtcp_packet = ctx - .encrypt_rtcp(&DECRYPTED_RTCP_PACKET) - .expect("Error encrypting rtcp payload"); - - assert_eq!(gotten_encrypted_rtcp_packet, *ENCRYPTED_RTCP_PACKET) -} - -#[test] -fn test_decrypt_rtcp() { - let mut ctx = Context::new( - &MASTER_KEY, - &MASTER_SALT, - ProtectionProfile::AeadAes128Gcm, - None, - None, - ) - .expect("Error creating srtp context"); - - let gotten_decrypted_rtcp_packet = ctx - .decrypt_rtcp(&ENCRYPTED_RTCP_PACKET) - .expect("Error decrypting rtcp payload"); - - assert_eq!(gotten_decrypted_rtcp_packet, *DECRYPTED_RTCP_PACKET) -} diff --git a/srtp/src/context/mod.rs b/srtp/src/context/mod.rs deleted file mode 100644 index ae3f3b1ce..000000000 --- a/srtp/src/context/mod.rs +++ /dev/null @@ -1,201 +0,0 @@ -#[cfg(test)] -mod context_test; -#[cfg(test)] -mod srtcp_test; -#[cfg(test)] -mod srtp_test; - -use std::collections::HashMap; - -use util::replay_detector::*; - -use crate::cipher::cipher_aead_aes_gcm::*; -use crate::cipher::cipher_aes_cm_hmac_sha1::*; -use crate::cipher::*; -use crate::error::{Error, Result}; -use crate::option::*; -use crate::protection_profile::*; - -pub mod srtcp; -pub mod srtp; - -const MAX_ROC_DISORDER: u16 = 100; - -/// Encrypt/Decrypt state for a single SRTP SSRC -#[derive(Default)] -pub(crate) struct SrtpSsrcState { - ssrc: u32, - rollover_counter: u32, - rollover_has_processed: bool, - last_sequence_number: u16, - replay_detector: Option>, -} - -/// Encrypt/Decrypt state for a single SRTCP SSRC -#[derive(Default)] -pub(crate) struct SrtcpSsrcState { - srtcp_index: usize, - ssrc: u32, - replay_detector: Option>, -} - -impl SrtpSsrcState { - pub fn next_rollover_count(&self, sequence_number: u16) -> u32 { - let mut roc = self.rollover_counter; - - if !self.rollover_has_processed { - } else if sequence_number == 0 { - // We exactly hit the rollover count - - // Only update rolloverCounter if lastSequenceNumber is greater then MAX_ROCDISORDER - // otherwise we already incremented for disorder - if self.last_sequence_number > MAX_ROC_DISORDER { - roc += 1; - } - } else if self.last_sequence_number < MAX_ROC_DISORDER - && sequence_number > (MAX_SEQUENCE_NUMBER - MAX_ROC_DISORDER) - { - // Our last sequence number incremented because we crossed 0, but then our current number was within MAX_ROCDISORDER of the max - // So we fell behind, drop to account for jitter - roc -= 1; - } else if sequence_number < MAX_ROC_DISORDER - && self.last_sequence_number > (MAX_SEQUENCE_NUMBER - MAX_ROC_DISORDER) - { - // our current is within a MAX_ROCDISORDER of 0 - // and our last sequence number was a high sequence number, increment to account for jitter - roc += 1; - } - - roc - } - - /// https://tools.ietf.org/html/rfc3550#appendix-A.1 - pub fn update_rollover_count(&mut self, sequence_number: u16) { - if !self.rollover_has_processed { - self.rollover_has_processed = true; - } else if sequence_number == 0 { - // We exactly hit the rollover count - - // Only update rolloverCounter if lastSequenceNumber is greater then MAX_ROCDISORDER - // otherwise we already incremented for disorder - if self.last_sequence_number > MAX_ROC_DISORDER { - self.rollover_counter += 1; - } - } else if self.last_sequence_number < MAX_ROC_DISORDER - && sequence_number > (MAX_SEQUENCE_NUMBER - MAX_ROC_DISORDER) - { - // Our last sequence number incremented because we crossed 0, but then our current number was within MAX_ROCDISORDER of the max - // So we fell behind, drop to account for jitter - self.rollover_counter -= 1; - } else if sequence_number < MAX_ROC_DISORDER - && self.last_sequence_number > (MAX_SEQUENCE_NUMBER - MAX_ROC_DISORDER) - { - // our current is within a MAX_ROCDISORDER of 0 - // and our last sequence number was a high sequence number, increment to account for jitter - self.rollover_counter += 1; - } - self.last_sequence_number = sequence_number; - } -} - -/// Context represents a SRTP cryptographic context -/// Context can only be used for one-way operations -/// it must either used ONLY for encryption or ONLY for decryption -pub struct Context { - cipher: Box, - - srtp_ssrc_states: HashMap, - srtcp_ssrc_states: HashMap, - - new_srtp_replay_detector: ContextOption, - new_srtcp_replay_detector: ContextOption, -} - -impl Context { - /// CreateContext creates a new SRTP Context - pub fn new( - master_key: &[u8], - master_salt: &[u8], - profile: ProtectionProfile, - srtp_ctx_opt: Option, - srtcp_ctx_opt: Option, - ) -> Result { - let key_len = profile.key_len(); - let salt_len = profile.salt_len(); - - if master_key.len() != key_len { - return Err(Error::SrtpMasterKeyLength(key_len, master_key.len())); - } else if master_salt.len() != salt_len { - return Err(Error::SrtpSaltLength(salt_len, master_salt.len())); - } - - let cipher: Box = match profile { - ProtectionProfile::Aes128CmHmacSha1_80 => { - Box::new(CipherAesCmHmacSha1::new(master_key, master_salt)?) - } - - ProtectionProfile::AeadAes128Gcm => { - Box::new(CipherAeadAesGcm::new(master_key, master_salt)?) - } - }; - - let srtp_ctx_opt = if let Some(ctx_opt) = srtp_ctx_opt { - ctx_opt - } else { - srtp_no_replay_protection() - }; - - let srtcp_ctx_opt = if let Some(ctx_opt) = srtcp_ctx_opt { - ctx_opt - } else { - srtcp_no_replay_protection() - }; - - Ok(Context { - cipher, - srtp_ssrc_states: HashMap::new(), - srtcp_ssrc_states: HashMap::new(), - new_srtp_replay_detector: srtp_ctx_opt, - new_srtcp_replay_detector: srtcp_ctx_opt, - }) - } - - fn get_srtp_ssrc_state(&mut self, ssrc: u32) -> &mut SrtpSsrcState { - let s = SrtpSsrcState { - ssrc, - replay_detector: Some((self.new_srtp_replay_detector)()), - ..Default::default() - }; - - self.srtp_ssrc_states.entry(ssrc).or_insert(s) - } - - fn get_srtcp_ssrc_state(&mut self, ssrc: u32) -> &mut SrtcpSsrcState { - let s = SrtcpSsrcState { - ssrc, - replay_detector: Some((self.new_srtcp_replay_detector)()), - ..Default::default() - }; - self.srtcp_ssrc_states.entry(ssrc).or_insert(s) - } - - /// roc returns SRTP rollover counter value of specified SSRC. - fn get_roc(&self, ssrc: u32) -> Option { - self.srtp_ssrc_states.get(&ssrc).map(|s| s.rollover_counter) - } - - /// set_roc sets SRTP rollover counter value of specified SSRC. - fn set_roc(&mut self, ssrc: u32, roc: u32) { - self.get_srtp_ssrc_state(ssrc).rollover_counter = roc; - } - - /// index returns SRTCP index value of specified SSRC. - fn get_index(&self, ssrc: u32) -> Option { - self.srtcp_ssrc_states.get(&ssrc).map(|s| s.srtcp_index) - } - - /// set_index sets SRTCP index value of specified SSRC. - fn set_index(&mut self, ssrc: u32, index: usize) { - self.get_srtcp_ssrc_state(ssrc).srtcp_index = index; - } -} diff --git a/srtp/src/context/srtcp.rs b/srtp/src/context/srtcp.rs deleted file mode 100644 index 8bc54fda1..000000000 --- a/srtp/src/context/srtcp.rs +++ /dev/null @@ -1,50 +0,0 @@ -use bytes::Bytes; -use util::marshal::*; - -use super::*; -use crate::error::Result; - -impl Context { - /// DecryptRTCP decrypts a RTCP packet with an encrypted payload - pub fn decrypt_rtcp(&mut self, encrypted: &[u8]) -> Result { - let mut buf = encrypted; - rtcp::header::Header::unmarshal(&mut buf)?; - - let index = self.cipher.get_rtcp_index(encrypted); - let ssrc = u32::from_be_bytes([encrypted[4], encrypted[5], encrypted[6], encrypted[7]]); - - if let Some(replay_detector) = &mut self.get_srtcp_ssrc_state(ssrc).replay_detector { - if !replay_detector.check(index as u64) { - return Err(Error::SrtcpSsrcDuplicated(ssrc, index)); - } - } - - let dst = self.cipher.decrypt_rtcp(encrypted, index, ssrc)?; - - if let Some(replay_detector) = &mut self.get_srtcp_ssrc_state(ssrc).replay_detector { - replay_detector.accept(); - } - - Ok(dst) - } - - /// EncryptRTCP marshals and encrypts an RTCP packet, writing to the dst buffer provided. - /// If the dst buffer does not have the capacity to hold `len(plaintext) + 14` bytes, a new one will be allocated and returned. - pub fn encrypt_rtcp(&mut self, decrypted: &[u8]) -> Result { - let mut buf = decrypted; - rtcp::header::Header::unmarshal(&mut buf)?; - - let ssrc = u32::from_be_bytes([decrypted[4], decrypted[5], decrypted[6], decrypted[7]]); - - let index = { - let state = self.get_srtcp_ssrc_state(ssrc); - state.srtcp_index += 1; - if state.srtcp_index > MAX_SRTCP_INDEX { - state.srtcp_index = 0; - } - state.srtcp_index - }; - - self.cipher.encrypt_rtcp(decrypted, index, ssrc) - } -} diff --git a/srtp/src/context/srtcp_test.rs b/srtp/src/context/srtcp_test.rs deleted file mode 100644 index 55d772258..000000000 --- a/srtp/src/context/srtcp_test.rs +++ /dev/null @@ -1,257 +0,0 @@ -use bytes::{Buf, Bytes, BytesMut}; -use lazy_static::lazy_static; - -use super::*; -use crate::key_derivation::*; - -pub struct RTCPTestCase { - ssrc: u32, - index: usize, - encrypted: Bytes, - decrypted: Bytes, -} - -lazy_static! { - static ref RTCP_TEST_MASTER_KEY: Bytes = Bytes::from_static(&[ - 0xfd, 0xa6, 0x25, 0x95, 0xd7, 0xf6, 0x92, 0x6f, 0x7d, 0x9c, 0x02, 0x4c, 0xc9, 0x20, 0x9f, - 0x34 - ]); - - static ref RTCP_TEST_MASTER_SALT: Bytes = Bytes::from_static(&[ - 0xa9, 0x65, 0x19, 0x85, 0x54, 0x0b, 0x47, 0xbe, 0x2f, 0x27, 0xa8, 0xb8, 0x81, 0x23 - ]); - - static ref RTCP_TEST_CASES: Vec = vec![ - RTCPTestCase { - ssrc: 0x66ef91ff, - index: 0, - encrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x66, 0xef, 0x91, 0xff, 0xcd, 0x34, 0xc5, 0x78, 0xb2, 0x8b, - 0xe1, 0x6b, 0xc5, 0x09, 0xd5, 0x77, 0xe4, 0xce, 0x5f, 0x20, 0x80, 0x21, 0xbd, 0x66, - 0x74, 0x65, 0xe9, 0x5f, 0x49, 0xe5, 0xf5, 0xc0, 0x68, 0x4e, 0xe5, 0x6a, 0x78, 0x07, - 0x75, 0x46, 0xed, 0x90, 0xf6, 0xdc, 0x9d, 0xef, 0x3b, 0xdf, 0xf2, 0x79, 0xa9, 0xd8, - 0x80, 0x00, 0x00, 0x01, 0x60, 0xc0, 0xae, 0xb5, 0x6f, 0x40, 0x88, 0x0e, 0x28, 0xba - ]), - decrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x66, 0xef, 0x91, 0xff, 0xdf, 0x48, 0x80, 0xdd, 0x61, 0xa6, - 0x2e, 0xd3, 0xd8, 0xbc, 0xde, 0xbe, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x16, 0x04, - 0x81, 0xca, 0x00, 0x06, 0x66, 0xef, 0x91, 0xff, 0x01, 0x10, 0x52, 0x6e, 0x54, 0x35, - 0x43, 0x6d, 0x4a, 0x68, 0x7a, 0x79, 0x65, 0x74, 0x41, 0x78, 0x77, 0x2b, 0x00, 0x00 - ]), - }, - RTCPTestCase{ - ssrc: 0x11111111, - index: 0, - encrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x11, 0x11, 0x11, 0x11, 0x17, 0x8c, 0x15, 0xf1, 0x4b, 0x11, - 0xda, 0xf5, 0x74, 0x53, 0x86, 0x2b, 0xc9, 0x07, 0x29, 0x40, 0xbf, 0x22, 0xf6, 0x46, - 0x11, 0xa4, 0xc1, 0x3a, 0xff, 0x5a, 0xbd, 0xd0, 0xf8, 0x8b, 0x38, 0xe4, 0x95, 0x38, - 0x5d, 0xcf, 0x1b, 0xf5, 0x27, 0x77, 0xfb, 0xdb, 0x3f, 0x10, 0x68, 0x99, 0xd8, 0xad, - 0x80, 0x00, 0x00, 0x01, 0x34, 0x3c, 0x2e, 0x83, 0x17, 0x13, 0x93, 0x69, 0xcf, 0xc0 - ]), - decrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x11, 0x11, 0x11, 0x11, 0xdf, 0x48, 0x80, 0xdd, 0x61, 0xa6, - 0x2e, 0xd3, 0xd8, 0xbc, 0xde, 0xbe, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x16, 0x04, - 0x81, 0xca, 0x00, 0x06, 0x66, 0xef, 0x91, 0xff, 0x01, 0x10, 0x52, 0x6e, 0x54, 0x35, - 0x43, 0x6d, 0x4a, 0x68, 0x7a, 0x79, 0x65, 0x74, 0x41, 0x78, 0x77, 0x2b, 0x00, 0x00 - ]), - }, - RTCPTestCase{ - ssrc: 0x11111111, - index: 0x7ffffffe, // Upper boundary of index - encrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x11, 0x11, 0x11, 0x11, 0x17, 0x8c, 0x15, 0xf1, 0x4b, 0x11, - 0xda, 0xf5, 0x74, 0x53, 0x86, 0x2b, 0xc9, 0x07, 0x29, 0x40, 0xbf, 0x22, 0xf6, 0x46, - 0x11, 0xa4, 0xc1, 0x3a, 0xff, 0x5a, 0xbd, 0xd0, 0xf8, 0x8b, 0x38, 0xe4, 0x95, 0x38, - 0x5d, 0xcf, 0x1b, 0xf5, 0x27, 0x77, 0xfb, 0xdb, 0x3f, 0x10, 0x68, 0x99, 0xd8, 0xad, - 0xff, 0xff, 0xff, 0xff, 0x5a, 0x99, 0xce, 0xed, 0x9f, 0x2e, 0x4d, 0x9d, 0xfa, 0x97 - ]), - decrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x0, 0x6, 0x11, 0x11, 0x11, 0x11, 0x4, 0x99, 0x47, 0x53, 0xc4, 0x1e, - 0xb9, 0xde, 0x52, 0xa3, 0x1d, 0x77, 0x2f, 0xff, 0xcc, 0x75, 0xbb, 0x6a, 0x29, 0xb8, - 0x1, 0xb7, 0x2e, 0x4b, 0x4e, 0xcb, 0xa4, 0x81, 0x2d, 0x46, 0x4, 0x5e, 0x86, 0x90, - 0x17, 0x4f, 0x4d, 0x78, 0x2f, 0x58, 0xb8, 0x67, 0x91, 0x89, 0xe3, 0x61, 0x1, 0x7d - ]), - }, - RTCPTestCase{ - ssrc: 0x11111111, - index: 0x7fffffff, // Will be wrapped to 0 - encrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x00, 0x06, 0x11, 0x11, 0x11, 0x11, 0x17, 0x8c, 0x15, 0xf1, 0x4b, 0x11, - 0xda, 0xf5, 0x74, 0x53, 0x86, 0x2b, 0xc9, 0x07, 0x29, 0x40, 0xbf, 0x22, 0xf6, 0x46, - 0x11, 0xa4, 0xc1, 0x3a, 0xff, 0x5a, 0xbd, 0xd0, 0xf8, 0x8b, 0x38, 0xe4, 0x95, 0x38, - 0x5d, 0xcf, 0x1b, 0xf5, 0x27, 0x77, 0xfb, 0xdb, 0x3f, 0x10, 0x68, 0x99, 0xd8, 0xad, - 0x80, 0x00, 0x00, 0x00, 0x7d, 0x51, 0xf8, 0x0e, 0x56, 0x40, 0x72, 0x7b, 0x9e, 0x02 - ]), - decrypted: Bytes::from_static(&[ - 0x80, 0xc8, 0x0, 0x6, 0x11, 0x11, 0x11, 0x11, 0xda, 0xb5, 0xe0, 0x56, 0x9a, 0x4a, - 0x74, 0xed, 0x8a, 0x54, 0xc, 0xcf, 0xd5, 0x9, 0xb1, 0x40, 0x1, 0x42, 0xc3, 0x9a, - 0x76, 0x0, 0xa9, 0xd4, 0xf7, 0x29, 0x9e, 0x51, 0xfb, 0x3c, 0xc1, 0x74, 0x72, 0xf9, - 0x52, 0xb1, 0x92, 0x31, 0xca, 0x22, 0xab, 0x3e, 0xc5, 0x5f, 0x83, 0x34, 0xf0, 0x28 - ]), - }, - ]; -} - -#[test] -fn test_rtcp_lifecycle() -> Result<()> { - let mut encrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - )?; - let mut decrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - )?; - - for test_case in &*RTCP_TEST_CASES { - let decrypt_result = decrypt_context.decrypt_rtcp(&test_case.encrypted)?; - assert_eq!( - decrypt_result, test_case.decrypted, - "RTCP failed to decrypt" - ); - - encrypt_context.set_index(test_case.ssrc, test_case.index); - let encrypt_result = encrypt_context.encrypt_rtcp(&test_case.decrypted)?; - assert_eq!( - encrypt_result, test_case.encrypted, - "RTCP failed to encrypt" - ); - } - - Ok(()) -} - -#[test] -fn test_rtcp_invalid_auth_tag() -> Result<()> { - let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_tag_len(); - - let mut decrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - )?; - - let decrypt_result = decrypt_context.decrypt_rtcp(&RTCP_TEST_CASES[0].encrypted)?; - assert_eq!( - decrypt_result, RTCP_TEST_CASES[0].decrypted, - "RTCP failed to decrypt" - ); - - // Zero out auth tag - let mut rtcp_packet = BytesMut::new(); - rtcp_packet.extend_from_slice(&RTCP_TEST_CASES[0].encrypted); - let rtcp_packet_len = rtcp_packet.len(); - rtcp_packet[rtcp_packet_len - auth_tag_len..].copy_from_slice(&vec![0; auth_tag_len]); - let rtcp_packet = rtcp_packet.freeze(); - let decrypt_result = decrypt_context.decrypt_rtcp(&rtcp_packet); - assert!( - decrypt_result.is_err(), - "Was able to decrypt RTCP packet with invalid Auth Tag" - ); - - Ok(()) -} - -#[test] -fn test_rtcp_replay_detector_separation() -> Result<()> { - let mut decrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - Some(srtcp_replay_protection(10)), - )?; - - let rtcp_packet1 = RTCP_TEST_CASES[0].encrypted.clone(); - let decrypt_result1 = decrypt_context.decrypt_rtcp(&rtcp_packet1)?; - assert_eq!( - decrypt_result1, RTCP_TEST_CASES[0].decrypted, - "RTCP failed to decrypt" - ); - - let rtcp_packet2 = RTCP_TEST_CASES[1].encrypted.clone(); - let decrypt_result2 = decrypt_context.decrypt_rtcp(&rtcp_packet2)?; - assert_eq!( - decrypt_result2, RTCP_TEST_CASES[1].decrypted, - "RTCP failed to decrypt" - ); - - let result = decrypt_context.decrypt_rtcp(&rtcp_packet1); - assert!( - result.is_err(), - "Was able to decrypt duplicated RTCP packet" - ); - - let result = decrypt_context.decrypt_rtcp(&rtcp_packet2); - assert!( - result.is_err(), - "Was able to decrypt duplicated RTCP packet" - ); - - Ok(()) -} - -fn get_rtcp_index(encrypted: &Bytes, auth_tag_len: usize) -> u32 { - let tail_offset = encrypted.len() - (auth_tag_len + SRTCP_INDEX_SIZE); - let reader = &mut encrypted.slice(tail_offset..tail_offset + SRTCP_INDEX_SIZE); - //^(1 << 31) - reader.get_u32() & 0x7FFFFFFF -} - -#[test] -fn test_encrypt_rtcp_separation() -> Result<()> { - let mut encrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - )?; - - let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_tag_len(); - - let mut decrypt_context = Context::new( - &RTCP_TEST_MASTER_KEY, - &RTCP_TEST_MASTER_SALT, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - Some(srtcp_replay_protection(10)), - )?; - - let inputs = vec![ - RTCP_TEST_CASES[0].decrypted.clone(), - RTCP_TEST_CASES[1].decrypted.clone(), - RTCP_TEST_CASES[0].decrypted.clone(), - RTCP_TEST_CASES[1].decrypted.clone(), - ]; - let mut encrypted_rctps = vec![]; - - for input in &inputs { - let encrypted = encrypt_context.encrypt_rtcp(input)?; - encrypted_rctps.push(encrypted); - } - - for (i, expected_index) in [1, 1, 2, 2].iter().enumerate() { - assert_eq!( - *expected_index, - get_rtcp_index(&encrypted_rctps[i], auth_tag_len), - "RTCP index does not match" - ); - } - - for (i, output) in encrypted_rctps.iter().enumerate() { - let decrypted = decrypt_context.decrypt_rtcp(output)?; - assert_eq!(inputs[i], decrypted); - } - - Ok(()) -} diff --git a/srtp/src/context/srtp.rs b/srtp/src/context/srtp.rs deleted file mode 100644 index aaf0d9931..000000000 --- a/srtp/src/context/srtp.rs +++ /dev/null @@ -1,70 +0,0 @@ -use bytes::Bytes; -use util::marshal::*; - -use super::*; -use crate::error::Result; - -impl Context { - pub fn decrypt_rtp_with_header( - &mut self, - encrypted: &[u8], - header: &rtp::header::Header, - ) -> Result { - let roc = { - let state = self.get_srtp_ssrc_state(header.ssrc); - if let Some(replay_detector) = &mut state.replay_detector { - if !replay_detector.check(header.sequence_number as u64) { - return Err(Error::SrtpSsrcDuplicated( - header.ssrc, - header.sequence_number, - )); - } - } - - state.next_rollover_count(header.sequence_number) - }; - - let dst = self.cipher.decrypt_rtp(encrypted, header, roc)?; - { - let state = self.get_srtp_ssrc_state(header.ssrc); - if let Some(replay_detector) = &mut state.replay_detector { - replay_detector.accept(); - } - state.update_rollover_count(header.sequence_number); - } - - Ok(dst) - } - - /// DecryptRTP decrypts a RTP packet with an encrypted payload - pub fn decrypt_rtp(&mut self, encrypted: &[u8]) -> Result { - let mut buf = encrypted; - let header = rtp::header::Header::unmarshal(&mut buf)?; - self.decrypt_rtp_with_header(encrypted, &header) - } - - pub fn encrypt_rtp_with_header( - &mut self, - payload: &[u8], - header: &rtp::header::Header, - ) -> Result { - let roc = self - .get_srtp_ssrc_state(header.ssrc) - .next_rollover_count(header.sequence_number); - - let dst = self.cipher.encrypt_rtp(payload, header, roc)?; - - self.get_srtp_ssrc_state(header.ssrc) - .update_rollover_count(header.sequence_number); - - Ok(dst) - } - - /// EncryptRTP marshals and encrypts an RTP packet, writing to the dst buffer provided. - /// If the dst buffer does not have the capacity to hold `len(plaintext) + 10` bytes, a new one will be allocated and returned. - pub fn encrypt_rtp(&mut self, plaintext: &[u8]) -> Result { - let mut buf = plaintext; - let header = rtp::header::Header::unmarshal(&mut buf)?; - self.encrypt_rtp_with_header(plaintext, &header) - } -} diff --git a/srtp/src/context/srtp_test.rs b/srtp/src/context/srtp_test.rs deleted file mode 100644 index 8e5e5083a..000000000 --- a/srtp/src/context/srtp_test.rs +++ /dev/null @@ -1,172 +0,0 @@ -use bytes::Bytes; -use lazy_static::lazy_static; -use util::marshal::*; - -use super::*; - -struct RTPTestCase { - sequence_number: u16, - encrypted: Bytes, -} - -lazy_static! { - static ref RTP_TEST_CASE_DECRYPTED: Bytes = Bytes::from_static(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05]); - static ref RTP_TEST_CASES: Vec = vec![ - RTPTestCase { - sequence_number: 5000, - encrypted: Bytes::from_static(&[ - 0x6d, 0xd3, 0x7e, 0xd5, 0x99, 0xb7, 0x2d, 0x28, 0xb1, 0xf3, 0xa1, 0xf0, 0xc, 0xfb, - 0xfd, 0x8 - ]), - }, - RTPTestCase { - sequence_number: 5001, - encrypted: Bytes::from_static(&[ - 0xda, 0x47, 0xb, 0x2a, 0x74, 0x53, 0x65, 0xbd, 0x2f, 0xeb, 0xdc, 0x4b, 0x6d, 0x23, - 0xf3, 0xde - ]), - }, - RTPTestCase { - sequence_number: 5002, - encrypted: Bytes::from_static(&[ - 0x6e, 0xa7, 0x69, 0x8d, 0x24, 0x6d, 0xdc, 0xbf, 0xec, 0x2, 0x1c, 0xd1, 0x60, 0x76, - 0xc1, 0x0e - ]), - }, - RTPTestCase { - sequence_number: 5003, - encrypted: Bytes::from_static(&[ - 0x24, 0x7e, 0x96, 0xc8, 0x7d, 0x33, 0xa2, 0x92, 0x8d, 0x13, 0x8d, 0xe0, 0x76, 0x9f, - 0x08, 0xdc - ]), - }, - RTPTestCase { - sequence_number: 5004, - encrypted: Bytes::from_static(&[ - 0x75, 0x43, 0x28, 0xe4, 0x3a, 0x77, 0x59, 0x9b, 0x2e, 0xdf, 0x7b, 0x12, 0x68, 0x0b, - 0x57, 0x49 - ]), - }, - RTPTestCase{ - sequence_number: 65535, // upper boundary - encrypted: Bytes::from_static(&[ - 0xaf, 0xf7, 0xc2, 0x70, 0x37, 0x20, 0x83, 0x9c, 0x2c, 0x63, 0x85, 0x15, 0x0e, 0x44, - 0xca, 0x36 - ]), - }, - ]; -} - -fn build_test_context() -> Result { - let master_key = Bytes::from_static(&[ - 0x0d, 0xcd, 0x21, 0x3e, 0x4c, 0xbc, 0xf2, 0x8f, 0x01, 0x7f, 0x69, 0x94, 0x40, 0x1e, 0x28, - 0x89, - ]); - let master_salt = Bytes::from_static(&[ - 0x62, 0x77, 0x60, 0x38, 0xc0, 0x6d, 0xc9, 0x41, 0x9f, 0x6d, 0xd9, 0x43, 0x3e, 0x7c, - ]); - - Context::new( - &master_key, - &master_salt, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - ) -} - -#[test] -fn test_rtp_invalid_auth() -> Result<()> { - let master_key = Bytes::from_static(&[ - 0x0d, 0xcd, 0x21, 0x3e, 0x4c, 0xbc, 0xf2, 0x8f, 0x01, 0x7f, 0x69, 0x94, 0x40, 0x1e, 0x28, - 0x89, - ]); - let invalid_salt = Bytes::from_static(&[ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]); - - let mut encrypt_context = build_test_context()?; - let mut invalid_context = Context::new( - &master_key, - &invalid_salt, - ProtectionProfile::Aes128CmHmacSha1_80, - None, - None, - )?; - - for test_case in &*RTP_TEST_CASES { - let pkt = rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: test_case.sequence_number, - ..Default::default() - }, - payload: RTP_TEST_CASE_DECRYPTED.clone(), - }; - - let pkt_raw = pkt.marshal()?; - let out = encrypt_context.encrypt_rtp(&pkt_raw)?; - - let result = invalid_context.decrypt_rtp(&out); - assert!( - result.is_err(), - "Managed to decrypt with incorrect salt for packet with SeqNum: {}", - test_case.sequence_number - ); - } - - Ok(()) -} - -#[test] -fn test_rtp_lifecycle() -> Result<()> { - let mut encrypt_context = build_test_context()?; - let mut decrypt_context = build_test_context()?; - let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_tag_len(); - - for test_case in RTP_TEST_CASES.iter() { - let decrypted_pkt = rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: test_case.sequence_number, - ..Default::default() - }, - payload: RTP_TEST_CASE_DECRYPTED.clone(), - }; - - let decrypted_raw = decrypted_pkt.marshal()?; - - let encrypted_pkt = rtp::packet::Packet { - header: rtp::header::Header { - sequence_number: test_case.sequence_number, - ..Default::default() - }, - payload: test_case.encrypted.clone(), - }; - - let encrypted_raw = encrypted_pkt.marshal()?; - let actual_encrypted = encrypt_context.encrypt_rtp(&decrypted_raw)?; - assert_eq!( - actual_encrypted, encrypted_raw, - "RTP packet with SeqNum invalid encryption: {}", - test_case.sequence_number - ); - - let actual_decrypted = decrypt_context.decrypt_rtp(&encrypted_raw)?; - assert_ne!( - encrypted_raw[..encrypted_raw.len() - auth_tag_len].to_vec(), - actual_decrypted, - "DecryptRTP improperly encrypted in place" - ); - - assert_eq!( - actual_decrypted, decrypted_raw, - "RTP packet with SeqNum invalid decryption: {}", - test_case.sequence_number, - ) - } - - Ok(()) -} - -//TODO: BenchmarkEncryptRTP -//TODO: BenchmarkEncryptRTPInPlace -//TODO: BenchmarkDecryptRTP diff --git a/srtp/src/error.rs b/srtp/src/error.rs deleted file mode 100644 index b4f1a0346..000000000 --- a/srtp/src/error.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::io; - -use thiserror::Error; -use tokio::sync::mpsc::error::SendError as MpscSendError; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("duplicated packet")] - ErrDuplicated, - #[error("SRTP master key is not long enough")] - ErrShortSrtpMasterKey, - #[error("SRTP master salt is not long enough")] - ErrShortSrtpMasterSalt, - #[error("no such SRTP Profile")] - ErrNoSuchSrtpProfile, - #[error("indexOverKdr > 0 is not supported yet")] - ErrNonZeroKdrNotSupported, - #[error("exporter called with wrong label")] - ErrExporterWrongLabel, - #[error("no config provided")] - ErrNoConfig, - #[error("no conn provided")] - ErrNoConn, - #[error("failed to verify auth tag")] - ErrFailedToVerifyAuthTag, - #[error("packet is too short to be rtcp packet")] - ErrTooShortRtcp, - #[error("payload differs")] - ErrPayloadDiffers, - #[error("started channel used incorrectly, should only be closed")] - ErrStartedChannelUsedIncorrectly, - #[error("stream has not been inited, unable to close")] - ErrStreamNotInited, - #[error("stream is already closed")] - ErrStreamAlreadyClosed, - #[error("stream is already inited")] - ErrStreamAlreadyInited, - #[error("failed to cast child")] - ErrFailedTypeAssertion, - - #[error("index_over_kdr > 0 is not supported yet")] - UnsupportedIndexOverKdr, - #[error("SRTP Master Key must be len {0}, got {1}")] - SrtpMasterKeyLength(usize, usize), - #[error("SRTP Salt must be len {0}, got {1}")] - SrtpSaltLength(usize, usize), - #[error("SyntaxError: {0}")] - ExtMapParse(String), - #[error("srtp ssrc={0} index={1}: duplicated")] - SrtpSsrcDuplicated(u32, u16), - #[error("srtcp ssrc={0} index={1}: duplicated")] - SrtcpSsrcDuplicated(u32, usize), - #[error("ssrc {0} not exist in srtcp_ssrc_state")] - SsrcMissingFromSrtcp(u32), - #[error("Stream with ssrc {0} exists")] - StreamWithSsrcExists(u32), - #[error("Session RTP/RTCP type must be same as input buffer")] - SessionRtpRtcpTypeMismatch, - #[error("Session EOF")] - SessionEof, - #[error("too short SRTP packet: only {0} bytes, expected > {1} bytes")] - SrtpTooSmall(usize, usize), - #[error("too short SRTCP packet: only {0} bytes, expected > {1} bytes")] - SrtcpTooSmall(usize, usize), - #[error("failed to verify rtp auth tag")] - RtpFailedToVerifyAuthTag, - #[error("too short auth tag: only {0} bytes, expected > {1} bytes")] - RtcpInvalidLengthAuthTag(usize, usize), - #[error("failed to verify rtcp auth tag")] - RtcpFailedToVerifyAuthTag, - #[error("SessionSRTP has been closed")] - SessionSrtpAlreadyClosed, - #[error("this stream is not a RTPStream")] - InvalidRtpStream, - #[error("this stream is not a RTCPStream")] - InvalidRtcpStream, - - #[error("{0}")] - Io(#[source] IoError), - #[error("{0}")] - KeyingMaterial(#[from] util::KeyingMaterialExporterError), - #[error("mpsc send: {0}")] - MpscSend(String), - #[error("{0}")] - Util(#[from] util::Error), - #[error("{0}")] - Rtcp(#[from] rtcp::Error), - #[error("aes gcm: {0}")] - AesGcm(#[from] aes_gcm::Error), - - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -// Because Tokio SendError is parameterized, we sadly lose the backtrace. -impl From> for Error { - fn from(e: MpscSendError) -> Self { - Error::MpscSend(e.to_string()) - } -} diff --git a/srtp/src/key_derivation.rs b/srtp/src/key_derivation.rs deleted file mode 100644 index 343450937..000000000 --- a/srtp/src/key_derivation.rs +++ /dev/null @@ -1,173 +0,0 @@ -use aes::cipher::generic_array::GenericArray; -use aes::cipher::BlockEncrypt; -use aes::Aes128; -use aes_gcm::KeyInit; - -use crate::error::{Error, Result}; - -pub const LABEL_SRTP_ENCRYPTION: u8 = 0x00; -pub const LABEL_SRTP_AUTHENTICATION_TAG: u8 = 0x01; -pub const LABEL_SRTP_SALT: u8 = 0x02; -pub const LABEL_SRTCP_ENCRYPTION: u8 = 0x03; -pub const LABEL_SRTCP_AUTHENTICATION_TAG: u8 = 0x04; -pub const LABEL_SRTCP_SALT: u8 = 0x05; - -pub(crate) const SRTCP_INDEX_SIZE: usize = 4; - -pub(crate) fn aes_cm_key_derivation( - label: u8, - master_key: &[u8], - master_salt: &[u8], - index_over_kdr: usize, - out_len: usize, -) -> Result> { - if index_over_kdr != 0 { - // 24-bit "index DIV kdr" must be xored to prf input. - return Err(Error::UnsupportedIndexOverKdr); - } - - // https://tools.ietf.org/html/rfc3711#appendix-B.3 - // The input block for AES-CM is generated by exclusive-oring the master salt with the - // concatenation of the encryption key label 0x00 with (index DIV kdr), - // - index is 'rollover count' and DIV is 'divided by' - - let n_master_key = master_key.len(); - let n_master_salt = master_salt.len(); - - let mut prf_in = vec![0u8; n_master_key]; - prf_in[..n_master_salt].copy_from_slice(master_salt); - - prf_in[7] ^= label; - - //The resulting value is then AES encrypted using the master key to get the cipher key. - let key = GenericArray::from_slice(master_key); - let block = Aes128::new(key); - - let mut out = vec![0u8; ((out_len + n_master_key) / n_master_key) * n_master_key]; - for (i, n) in (0..out_len).step_by(n_master_key).enumerate() { - //BigEndian.PutUint16(prfIn[nMasterKey-2:], i) - prf_in[n_master_key - 2] = ((i >> 8) & 0xFF) as u8; - prf_in[n_master_key - 1] = (i & 0xFF) as u8; - - out[n..n + n_master_key].copy_from_slice(&prf_in); - let out_key = GenericArray::from_mut_slice(&mut out[n..n + n_master_key]); - block.encrypt_block(out_key); - } - - Ok(out[..out_len].to_vec()) -} - -/// Generate IV https://tools.ietf.org/html/rfc3711#section-4.1.1 -/// where the 128-bit integer value IV SHALL be defined by the SSRC, the -/// SRTP packet index i, and the SRTP session salting key k_s, as below. -/// ROC = a 32-bit unsigned rollover counter (roc), which records how many -/// times the 16-bit RTP sequence number has been reset to zero after -/// passing through 65,535 -/// ```nobuild -/// i = 2^16 * roc + SEQ -/// IV = (salt*2 ^ 16) | (ssrc*2 ^ 64) | (i*2 ^ 16) -/// ``` -pub(crate) fn generate_counter( - sequence_number: u16, - rollover_counter: u32, - ssrc: u32, - session_salt: &[u8], -) -> [u8; 16] { - assert!(session_salt.len() <= 16); - - let mut counter = [0; 16]; - - let ssrc_be = ssrc.to_be_bytes(); - let rollover_be = rollover_counter.to_be_bytes(); - let seq_be = ((sequence_number as u32) << 16).to_be_bytes(); - - counter[4..8].copy_from_slice(&ssrc_be); - counter[8..12].copy_from_slice(&rollover_be); - counter[12..16].copy_from_slice(&seq_be); - - for i in 0..session_salt.len() { - counter[i] ^= session_salt[i]; - } - - counter -} - -#[cfg(test)] -mod test { - use super::*; - use crate::protection_profile::*; - - #[test] - fn test_valid_session_keys() -> Result<()> { - // Key Derivation Test Vectors from https://tools.ietf.org/html/rfc3711#appendix-B.3 - let master_key = vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ]; - let master_salt = vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ]; - - let expected_session_key = vec![ - 0xC6, 0x1E, 0x7A, 0x93, 0x74, 0x4F, 0x39, 0xEE, 0x10, 0x73, 0x4A, 0xFE, 0x3F, 0xF7, - 0xA0, 0x87, - ]; - let expected_session_salt = vec![ - 0x30, 0xCB, 0xBC, 0x08, 0x86, 0x3D, 0x8C, 0x85, 0xD4, 0x9D, 0xB3, 0x4A, 0x9A, 0xE1, - ]; - let expected_session_auth_tag = vec![ - 0xCE, 0xBE, 0x32, 0x1F, 0x6F, 0xF7, 0x71, 0x6B, 0x6F, 0xD4, 0xAB, 0x49, 0xAF, 0x25, - 0x6A, 0x15, 0x6D, 0x38, 0xBA, 0xA4, - ]; - - let session_key = aes_cm_key_derivation( - LABEL_SRTP_ENCRYPTION, - &master_key, - &master_salt, - 0, - master_key.len(), - )?; - assert_eq!( - session_key, expected_session_key, - "Session Key:\n{session_key:?} \ndoes not match expected:\n{expected_session_key:?}\nMaster Key:\n{master_key:?}\nMaster Salt:\n{master_salt:?}\n", - ); - - let session_salt = aes_cm_key_derivation( - LABEL_SRTP_SALT, - &master_key, - &master_salt, - 0, - master_salt.len(), - )?; - assert_eq!( - session_salt, expected_session_salt, - "Session Salt {session_salt:?} does not match expected {expected_session_salt:?}" - ); - - let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); - - let session_auth_tag = aes_cm_key_derivation( - LABEL_SRTP_AUTHENTICATION_TAG, - &master_key, - &master_salt, - 0, - auth_key_len, - )?; - assert_eq!( - session_auth_tag, expected_session_auth_tag, - "Session Auth Tag {session_auth_tag:?} does not match expected {expected_session_auth_tag:?}", - ); - - Ok(()) - } - - // This test asserts that calling aesCmKeyDerivation with a non-zero indexOverKdr fails - // Currently this isn't supported, but the API makes sure we can add this in the future - #[test] - fn test_index_over_kdr() -> Result<()> { - let result = aes_cm_key_derivation(LABEL_SRTP_AUTHENTICATION_TAG, &[], &[], 1, 0); - assert!(result.is_err()); - - Ok(()) - } -} diff --git a/srtp/src/lib.rs b/srtp/src/lib.rs deleted file mode 100644 index 044042eb2..000000000 --- a/srtp/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -mod cipher; -pub mod config; -pub mod context; -mod error; -mod key_derivation; -pub mod option; -pub mod protection_profile; -pub mod session; -pub mod stream; - -pub use error::Error; diff --git a/srtp/src/option.rs b/srtp/src/option.rs deleted file mode 100644 index d2513a866..000000000 --- a/srtp/src/option.rs +++ /dev/null @@ -1,36 +0,0 @@ -use util::replay_detector::*; - -pub type ContextOption = Box Box) + Send + Sync>; - -pub(crate) const MAX_SEQUENCE_NUMBER: u16 = 65535; -pub(crate) const MAX_SRTCP_INDEX: usize = 0x7FFFFFFF; - -/// srtp_replay_protection sets SRTP replay protection window size. -pub fn srtp_replay_protection(window_size: usize) -> ContextOption { - Box::new(move || -> Box { - Box::new(WrappedSlidingWindowDetector::new( - window_size, - MAX_SEQUENCE_NUMBER as u64, - )) - }) -} - -/// Sets SRTCP replay protection window size. -pub fn srtcp_replay_protection(window_size: usize) -> ContextOption { - Box::new(move || -> Box { - Box::new(WrappedSlidingWindowDetector::new( - window_size, - MAX_SRTCP_INDEX as u64, - )) - }) -} - -/// srtp_no_replay_protection disables SRTP replay protection. -pub fn srtp_no_replay_protection() -> ContextOption { - Box::new(|| -> Box { Box::::default() }) -} - -/// srtcp_no_replay_protection disables SRTCP replay protection. -pub fn srtcp_no_replay_protection() -> ContextOption { - Box::new(|| -> Box { Box::::default() }) -} diff --git a/srtp/src/protection_profile.rs b/srtp/src/protection_profile.rs deleted file mode 100644 index 0991ea737..000000000 --- a/srtp/src/protection_profile.rs +++ /dev/null @@ -1,37 +0,0 @@ -/// ProtectionProfile specifies Cipher and AuthTag details, similar to TLS cipher suite -#[derive(Default, Debug, Clone, Copy)] -#[repr(u8)] -pub enum ProtectionProfile { - #[default] - Aes128CmHmacSha1_80 = 0x0001, - AeadAes128Gcm = 0x0007, -} - -impl ProtectionProfile { - pub(crate) fn key_len(&self) -> usize { - match *self { - ProtectionProfile::Aes128CmHmacSha1_80 | ProtectionProfile::AeadAes128Gcm => 16, - } - } - - pub(crate) fn salt_len(&self) -> usize { - match *self { - ProtectionProfile::Aes128CmHmacSha1_80 => 14, - ProtectionProfile::AeadAes128Gcm => 12, - } - } - - pub(crate) fn auth_tag_len(&self) -> usize { - match *self { - ProtectionProfile::Aes128CmHmacSha1_80 => 10, //CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN, - ProtectionProfile::AeadAes128Gcm => 16, //CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN, - } - } - - pub(crate) fn auth_key_len(&self) -> usize { - match *self { - ProtectionProfile::Aes128CmHmacSha1_80 => 20, - ProtectionProfile::AeadAes128Gcm => 0, - } - } -} diff --git a/srtp/src/session/mod.rs b/srtp/src/session/mod.rs deleted file mode 100644 index efee1bef8..000000000 --- a/srtp/src/session/mod.rs +++ /dev/null @@ -1,271 +0,0 @@ -#[cfg(test)] -mod session_rtcp_test; -#[cfg(test)] -mod session_rtp_test; - -use std::collections::{HashMap, HashSet}; -use std::marker::{Send, Sync}; -use std::sync::Arc; - -use bytes::Bytes; -use tokio::sync::{mpsc, Mutex}; -use util::conn::Conn; -use util::marshal::*; - -use crate::config::*; -use crate::context::*; -use crate::error::{Error, Result}; -use crate::option::*; -use crate::stream::*; - -const DEFAULT_SESSION_SRTP_REPLAY_PROTECTION_WINDOW: usize = 64; -const DEFAULT_SESSION_SRTCP_REPLAY_PROTECTION_WINDOW: usize = 64; - -/// Session implements io.ReadWriteCloser and provides a bi-directional SRTP session -/// SRTP itself does not have a design like this, but it is common in most applications -/// for local/remote to each have their own keying material. This provides those patterns -/// instead of making everyone re-implement -pub struct Session { - local_context: Arc>, - streams_map: Arc>>>, - new_stream_rx: Arc>>>, - close_stream_tx: mpsc::Sender, - close_session_tx: mpsc::Sender<()>, - pub(crate) udp_tx: Arc, - is_rtp: bool, -} - -impl Session { - pub async fn new( - conn: Arc, - config: Config, - is_rtp: bool, - ) -> Result { - let local_context = Context::new( - &config.keys.local_master_key, - &config.keys.local_master_salt, - config.profile, - config.local_rtp_options, - config.local_rtcp_options, - )?; - - let mut remote_context = Context::new( - &config.keys.remote_master_key, - &config.keys.remote_master_salt, - config.profile, - if config.remote_rtp_options.is_none() { - Some(srtp_replay_protection( - DEFAULT_SESSION_SRTP_REPLAY_PROTECTION_WINDOW, - )) - } else { - config.remote_rtp_options - }, - if config.remote_rtcp_options.is_none() { - Some(srtcp_replay_protection( - DEFAULT_SESSION_SRTCP_REPLAY_PROTECTION_WINDOW, - )) - } else { - config.remote_rtcp_options - }, - )?; - - let streams_map = Arc::new(Mutex::new(HashMap::new())); - let (mut new_stream_tx, new_stream_rx) = mpsc::channel(8); - let (close_stream_tx, mut close_stream_rx) = mpsc::channel(8); - let (close_session_tx, mut close_session_rx) = mpsc::channel(8); - let udp_tx = Arc::clone(&conn); - let udp_rx = Arc::clone(&conn); - let cloned_streams_map = Arc::clone(&streams_map); - let cloned_close_stream_tx = close_stream_tx.clone(); - - tokio::spawn(async move { - let mut buf = vec![0u8; 8192]; - - loop { - let incoming_stream = Session::incoming( - &udp_rx, - &mut buf, - &cloned_streams_map, - &cloned_close_stream_tx, - &mut new_stream_tx, - &mut remote_context, - is_rtp, - ); - let close_stream = close_stream_rx.recv(); - let close_session = close_session_rx.recv(); - - tokio::select! { - result = incoming_stream => match result{ - Ok(()) => {}, - Err(err) => log::info!("{}", err), - }, - opt = close_stream => if let Some(ssrc) = opt { - Session::close_stream(&cloned_streams_map, ssrc).await - }, - _ = close_session => break - } - } - }); - - Ok(Session { - local_context: Arc::new(Mutex::new(local_context)), - streams_map, - new_stream_rx: Arc::new(Mutex::new(new_stream_rx)), - close_stream_tx, - close_session_tx, - udp_tx, - is_rtp, - }) - } - - async fn close_stream(streams_map: &Arc>>>, ssrc: u32) { - let mut streams = streams_map.lock().await; - streams.remove(&ssrc); - } - - async fn incoming( - udp_rx: &Arc, - buf: &mut [u8], - streams_map: &Arc>>>, - close_stream_tx: &mpsc::Sender, - new_stream_tx: &mut mpsc::Sender>, - remote_context: &mut Context, - is_rtp: bool, - ) -> Result<()> { - let n = udp_rx.recv(buf).await?; - if n == 0 { - return Err(Error::SessionEof); - } - - let decrypted = if is_rtp { - remote_context.decrypt_rtp(&buf[0..n])? - } else { - remote_context.decrypt_rtcp(&buf[0..n])? - }; - - let mut buf = &decrypted[..]; - let ssrcs = if is_rtp { - vec![rtp::header::Header::unmarshal(&mut buf)?.ssrc] - } else { - let pkts = rtcp::packet::unmarshal(&mut buf)?; - destination_ssrc(&pkts) - }; - - for ssrc in ssrcs { - let (stream, is_new) = - Session::get_or_create_stream(streams_map, close_stream_tx.clone(), is_rtp, ssrc) - .await; - if is_new { - log::trace!( - "srtp session got new {} stream {}", - if is_rtp { "rtp" } else { "rtcp" }, - ssrc - ); - new_stream_tx.send(Arc::clone(&stream)).await?; - } - - match stream.buffer.write(&decrypted).await { - Ok(_) => {} - Err(err) => { - // Silently drop data when the buffer is full. - if util::Error::ErrBufferFull != err { - return Err(err.into()); - } - } - } - } - - Ok(()) - } - - async fn get_or_create_stream( - streams_map: &Arc>>>, - close_stream_tx: mpsc::Sender, - is_rtp: bool, - ssrc: u32, - ) -> (Arc, bool) { - let mut streams = streams_map.lock().await; - - if let Some(stream) = streams.get(&ssrc) { - (Arc::clone(stream), false) - } else { - let stream = Arc::new(Stream::new(ssrc, close_stream_tx, is_rtp)); - streams.insert(ssrc, Arc::clone(&stream)); - (stream, true) - } - } - - /// open on the given SSRC to create a stream, it can be used - /// if you want a certain SSRC, but don't want to wait for Accept - pub async fn open(&self, ssrc: u32) -> Arc { - let (stream, _) = Session::get_or_create_stream( - &self.streams_map, - self.close_stream_tx.clone(), - self.is_rtp, - ssrc, - ) - .await; - - stream - } - - /// accept returns a stream to handle RTCP for a single SSRC - pub async fn accept(&self) -> Result> { - let mut new_stream_rx = self.new_stream_rx.lock().await; - let result = new_stream_rx.recv().await; - if let Some(stream) = result { - Ok(stream) - } else { - Err(Error::SessionSrtpAlreadyClosed) - } - } - - pub async fn close(&self) -> Result<()> { - self.close_session_tx.send(()).await?; - - Ok(()) - } - - pub async fn write(&self, buf: &Bytes, is_rtp: bool) -> Result { - if self.is_rtp != is_rtp { - return Err(Error::SessionRtpRtcpTypeMismatch); - } - - let encrypted = { - let mut local_context = self.local_context.lock().await; - - if is_rtp { - local_context.encrypt_rtp(buf)? - } else { - local_context.encrypt_rtcp(buf)? - } - }; - - Ok(self.udp_tx.send(&encrypted).await?) - } - - pub async fn write_rtp(&self, pkt: &rtp::packet::Packet) -> Result { - let raw = pkt.marshal()?; - self.write(&raw, true).await - } - - pub async fn write_rtcp( - &self, - pkt: &(dyn rtcp::packet::Packet + Send + Sync), - ) -> Result { - let raw = pkt.marshal()?; - self.write(&raw, false).await - } -} - -/// create a list of Destination SSRCs -/// that's a superset of all Destinations in the slice. -fn destination_ssrc(pkts: &[Box]) -> Vec { - let mut ssrc_set = HashSet::new(); - for p in pkts { - for ssrc in p.destination_ssrc() { - ssrc_set.insert(ssrc); - } - } - ssrc_set.into_iter().collect() -} diff --git a/srtp/src/session/session_rtcp_test.rs b/srtp/src/session/session_rtcp_test.rs deleted file mode 100644 index 8d880e9e0..000000000 --- a/srtp/src/session/session_rtcp_test.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::sync::Arc; - -use bytes::{Bytes, BytesMut}; -use rtcp::payload_feedbacks::*; -use tokio::sync::{mpsc, Mutex}; -use util::conn::conn_pipe::*; - -use super::*; -use crate::error::Result; -use crate::protection_profile::*; - -async fn build_session_srtcp_pair() -> Result<(Session, Session)> { - let (ua, ub) = pipe(); - - let ca = Config { - profile: ProtectionProfile::Aes128CmHmacSha1_80, - keys: SessionKeys { - local_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - local_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - remote_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - remote_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - }, - - local_rtp_options: None, - remote_rtp_options: None, - - local_rtcp_options: None, - remote_rtcp_options: None, - }; - - let cb = Config { - profile: ProtectionProfile::Aes128CmHmacSha1_80, - keys: SessionKeys { - local_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - local_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - remote_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - remote_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - }, - - local_rtp_options: None, - remote_rtp_options: None, - - local_rtcp_options: None, - remote_rtcp_options: None, - }; - - let sa = Session::new(Arc::new(ua), ca, false).await?; - let sb = Session::new(Arc::new(ub), cb, false).await?; - - Ok((sa, sb)) -} - -const TEST_SSRC: u32 = 5000; - -#[tokio::test] -async fn test_session_srtcp_accept() -> Result<()> { - let (sa, sb) = build_session_srtcp_pair().await?; - - let rtcp_packet = picture_loss_indication::PictureLossIndication { - media_ssrc: TEST_SSRC, - ..Default::default() - }; - - let test_payload = rtcp_packet.marshal()?; - sa.write_rtcp(&rtcp_packet).await?; - - let read_stream = sb.accept().await?; - let ssrc = read_stream.get_ssrc(); - assert_eq!( - ssrc, TEST_SSRC, - "SSRC mismatch during accept exp({TEST_SSRC}) actual({ssrc})" - ); - - let mut read_buffer = BytesMut::with_capacity(test_payload.len()); - read_buffer.resize(test_payload.len(), 0u8); - read_stream.read(&mut read_buffer).await?; - - assert_eq!( - &test_payload[..], - &read_buffer[..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - &test_payload[..], - &read_buffer[..] - ); - - sa.close().await?; - sb.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_session_srtcp_listen() -> Result<()> { - let (sa, sb) = build_session_srtcp_pair().await?; - - let rtcp_packet = picture_loss_indication::PictureLossIndication { - media_ssrc: TEST_SSRC, - ..Default::default() - }; - - let test_payload = rtcp_packet.marshal()?; - let read_stream = sb.open(TEST_SSRC).await; - - sa.write_rtcp(&rtcp_packet).await?; - - let mut read_buffer = BytesMut::with_capacity(test_payload.len()); - read_buffer.resize(test_payload.len(), 0u8); - read_stream.read(&mut read_buffer).await?; - - assert_eq!( - &test_payload[..], - &read_buffer[..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - &test_payload[..], - &read_buffer[..] - ); - - sa.close().await?; - sb.close().await?; - - Ok(()) -} - -fn encrypt_srtcp( - context: &mut Context, - pkt: &(dyn rtcp::packet::Packet + Send + Sync), -) -> Result { - let decrypted = pkt.marshal()?; - let encrypted = context.encrypt_rtcp(&decrypted)?; - Ok(encrypted) -} - -const PLI_PACKET_SIZE: usize = 8; - -async fn get_sender_ssrc(read_stream: &Arc) -> Result { - let auth_tag_size = ProtectionProfile::Aes128CmHmacSha1_80.auth_tag_len(); - - let mut read_buffer = BytesMut::with_capacity(PLI_PACKET_SIZE + auth_tag_size); - read_buffer.resize(PLI_PACKET_SIZE + auth_tag_size, 0u8); - - let pkts = read_stream.read_rtcp(&mut read_buffer).await?; - let mut bytes = &pkts[0].marshal()?[..]; - let pli = picture_loss_indication::PictureLossIndication::unmarshal(&mut bytes)?; - - Ok(pli.sender_ssrc) -} - -#[tokio::test] -async fn test_session_srtcp_replay_protection() -> Result<()> { - let (sa, sb) = build_session_srtcp_pair().await?; - - let read_stream = sb.open(TEST_SSRC).await; - - // Generate test packets - let mut packets = vec![]; - let mut expected_ssrc = vec![]; - { - let mut local_context = sa.local_context.lock().await; - for i in 0..0x10u32 { - expected_ssrc.push(i); - - let packet = picture_loss_indication::PictureLossIndication { - media_ssrc: TEST_SSRC, - sender_ssrc: i, - }; - - let encrypted = encrypt_srtcp(&mut local_context, &packet)?; - - packets.push(encrypted); - } - } - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - - let received_ssrc = Arc::new(Mutex::new(vec![])); - let cloned_received_ssrc = Arc::clone(&received_ssrc); - let count = expected_ssrc.len(); - - tokio::spawn(async move { - let mut i = 0; - while i < count { - match get_sender_ssrc(&read_stream).await { - Ok(ssrc) => { - let mut r = cloned_received_ssrc.lock().await; - r.push(ssrc); - - i += 1; - } - Err(_) => break, - } - } - - drop(done_tx); - }); - - // Write with replay attack - for packet in &packets { - sa.udp_tx.send(packet).await?; - - // Immediately replay - sa.udp_tx.send(packet).await?; - } - for packet in &packets { - // Delayed replay - sa.udp_tx.send(packet).await?; - } - - done_rx.recv().await; - - sa.close().await?; - sb.close().await?; - - { - let received_ssrc = received_ssrc.lock().await; - assert_eq!(&expected_ssrc[..], &received_ssrc[..]); - } - - Ok(()) -} diff --git a/srtp/src/session/session_rtp_test.rs b/srtp/src/session/session_rtp_test.rs deleted file mode 100644 index 5764d1346..000000000 --- a/srtp/src/session/session_rtp_test.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use bytes::{Bytes, BytesMut}; -use tokio::net::UdpSocket; -use tokio::sync::{mpsc, Mutex}; - -use super::*; -use crate::error::Result; -use crate::protection_profile::*; - -async fn build_session_srtp_pair() -> Result<(Session, Session)> { - let ua = UdpSocket::bind("127.0.0.1:0").await?; - let ub = UdpSocket::bind("127.0.0.1:0").await?; - - ua.connect(ub.local_addr()?).await?; - ub.connect(ua.local_addr()?).await?; - - let ca = Config { - profile: ProtectionProfile::Aes128CmHmacSha1_80, - keys: SessionKeys { - local_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - local_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - remote_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - remote_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - }, - - local_rtp_options: None, - remote_rtp_options: None, - - local_rtcp_options: None, - remote_rtcp_options: None, - }; - - let cb = Config { - profile: ProtectionProfile::Aes128CmHmacSha1_80, - keys: SessionKeys { - local_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - local_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - remote_master_key: vec![ - 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, - 0x41, 0x39, - ], - remote_master_salt: vec![ - 0x0E, 0xC6, 0x75, 0xAD, 0x49, 0x8A, 0xFE, 0xEB, 0xB6, 0x96, 0x0B, 0x3A, 0xAB, 0xE6, - ], - }, - - local_rtp_options: None, - remote_rtp_options: None, - - local_rtcp_options: None, - remote_rtcp_options: None, - }; - - let sa = Session::new(Arc::new(ua), ca, true).await?; - let sb = Session::new(Arc::new(ub), cb, true).await?; - - Ok((sa, sb)) -} - -const TEST_SSRC: u32 = 5000; -const RTP_HEADER_SIZE: usize = 12; - -#[tokio::test] -async fn test_session_srtp_accept() -> Result<()> { - let test_payload = Bytes::from_static(&[0x00, 0x01, 0x03, 0x04]); - let mut read_buffer = BytesMut::with_capacity(RTP_HEADER_SIZE + test_payload.len()); - read_buffer.resize(RTP_HEADER_SIZE + test_payload.len(), 0u8); - let (sa, sb) = build_session_srtp_pair().await?; - - let packet = rtp::packet::Packet { - header: rtp::header::Header { - ssrc: TEST_SSRC, - ..Default::default() - }, - payload: test_payload.clone(), - }; - sa.write_rtp(&packet).await?; - - let read_stream = sb.accept().await?; - let ssrc = read_stream.get_ssrc(); - assert_eq!( - ssrc, TEST_SSRC, - "SSRC mismatch during accept exp({TEST_SSRC}) actual({ssrc})" - ); - - read_stream.read(&mut read_buffer).await?; - - assert_eq!( - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..] - ); - - sa.close().await?; - sb.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_session_srtp_listen() -> Result<()> { - let test_payload = Bytes::from_static(&[0x00, 0x01, 0x03, 0x04]); - let mut read_buffer = BytesMut::with_capacity(RTP_HEADER_SIZE + test_payload.len()); - read_buffer.resize(RTP_HEADER_SIZE + test_payload.len(), 0u8); - let (sa, sb) = build_session_srtp_pair().await?; - - let packet = rtp::packet::Packet { - header: rtp::header::Header { - ssrc: TEST_SSRC, - ..Default::default() - }, - payload: test_payload.clone(), - }; - - let read_stream = sb.open(TEST_SSRC).await; - - sa.write_rtp(&packet).await?; - - read_stream.read(&mut read_buffer).await?; - - assert_eq!( - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..] - ); - - sa.close().await?; - sb.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_session_srtp_multi_ssrc() -> Result<()> { - let ssrcs = vec![5000, 5001, 5002]; - let test_payload = Bytes::from_static(&[0x00, 0x01, 0x03, 0x04]); - let mut read_buffer = BytesMut::with_capacity(RTP_HEADER_SIZE + test_payload.len()); - read_buffer.resize(RTP_HEADER_SIZE + test_payload.len(), 0u8); - let (sa, sb) = build_session_srtp_pair().await?; - - let mut read_streams = HashMap::new(); - for ssrc in &ssrcs { - let read_stream = sb.open(*ssrc).await; - read_streams.insert(*ssrc, read_stream); - } - - for ssrc in &ssrcs { - let packet = rtp::packet::Packet { - header: rtp::header::Header { - ssrc: *ssrc, - ..Default::default() - }, - payload: test_payload.clone(), - }; - sa.write_rtp(&packet).await?; - - if let Some(read_stream) = read_streams.get_mut(ssrc) { - read_stream.read(&mut read_buffer).await?; - - assert_eq!( - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - &test_payload[..], - &read_buffer[RTP_HEADER_SIZE..] - ); - } else { - panic!("ssrc {} not found", *ssrc); - } - } - - sa.close().await?; - sb.close().await?; - - Ok(()) -} - -fn encrypt_srtp(context: &mut Context, pkt: &rtp::packet::Packet) -> Result { - let decrypted = pkt.marshal()?; - let encrypted = context.encrypt_rtp(&decrypted)?; - Ok(encrypted) -} - -async fn payload_srtp( - read_stream: &Arc, - header_size: usize, - expected_payload: &[u8], -) -> Result { - let mut read_buffer = BytesMut::with_capacity(header_size + expected_payload.len()); - read_buffer.resize(header_size + expected_payload.len(), 0u8); - - let pkt = read_stream.read_rtp(&mut read_buffer).await?; - - assert_eq!( - expected_payload, - &pkt.payload[..], - "Sent buffer does not match the one received exp({:?}) actual({:?})", - expected_payload, - &pkt.payload[..] - ); - - Ok(pkt.header.sequence_number) -} - -#[tokio::test] -async fn test_session_srtp_replay_protection() -> Result<()> { - let test_payload = Bytes::from_static(&[0x00, 0x01, 0x03, 0x04]); - - let (sa, sb) = build_session_srtp_pair().await?; - - let read_stream = sb.open(TEST_SSRC).await; - - // Generate test packets - let mut packets = vec![]; - let mut expected_sequence_number = vec![]; - { - let mut local_context = sa.local_context.lock().await; - let mut i = 0xFFF0u16; - while i != 0x10 { - expected_sequence_number.push(i); - - let packet = rtp::packet::Packet { - header: rtp::header::Header { - ssrc: TEST_SSRC, - sequence_number: i, - ..Default::default() - }, - payload: test_payload.clone(), - }; - - let encrypted = encrypt_srtp(&mut local_context, &packet)?; - - packets.push(encrypted); - - if i == 0xFFFF { - i = 0; - } else { - i += 1; - } - } - } - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - - let received_sequence_number = Arc::new(Mutex::new(vec![])); - let cloned_received_sequence_number = Arc::clone(&received_sequence_number); - let count = expected_sequence_number.len(); - - tokio::spawn(async move { - let mut i = 0; - while i < count { - let seq = payload_srtp(&read_stream, RTP_HEADER_SIZE, &test_payload) - .await - .unwrap(); - let mut r = cloned_received_sequence_number.lock().await; - r.push(seq); - - i += 1; - } - - drop(done_tx); - }); - - // Write with replay attack - for packet in &packets { - sa.udp_tx.send(packet).await?; - - // Immediately replay - sa.udp_tx.send(packet).await?; - } - for packet in &packets { - // Delayed replay - sa.udp_tx.send(packet).await?; - } - - done_rx.recv().await; - - sa.close().await?; - sb.close().await?; - - { - let received_sequence_number = received_sequence_number.lock().await; - assert_eq!(&received_sequence_number[..], &expected_sequence_number[..]); - } - - Ok(()) -} diff --git a/srtp/src/stream.rs b/srtp/src/stream.rs deleted file mode 100644 index 735f12cf3..000000000 --- a/srtp/src/stream.rs +++ /dev/null @@ -1,91 +0,0 @@ -use tokio::sync::mpsc; -use util::marshal::*; -use util::Buffer; - -use crate::error::{Error, Result}; - -/// Limit the buffer size to 1MB -pub const SRTP_BUFFER_SIZE: usize = 1000 * 1000; - -/// Limit the buffer size to 100KB -pub const SRTCP_BUFFER_SIZE: usize = 100 * 1000; - -/// Stream handles decryption for a single RTP/RTCP SSRC -#[derive(Debug)] -pub struct Stream { - ssrc: u32, - tx: mpsc::Sender, - pub(crate) buffer: Buffer, - is_rtp: bool, -} - -impl Stream { - /// Create a new stream - pub fn new(ssrc: u32, tx: mpsc::Sender, is_rtp: bool) -> Self { - Stream { - ssrc, - tx, - // Create a buffer with a 1MB limit - buffer: Buffer::new( - 0, - if is_rtp { - SRTP_BUFFER_SIZE - } else { - SRTCP_BUFFER_SIZE - }, - ), - is_rtp, - } - } - - /// GetSSRC returns the SSRC we are demuxing for - pub fn get_ssrc(&self) -> u32 { - self.ssrc - } - - /// Check if RTP is a stream. - pub fn is_rtp_stream(&self) -> bool { - self.is_rtp - } - - /// Read reads and decrypts full RTP packet from the nextConn - pub async fn read(&self, buf: &mut [u8]) -> Result { - Ok(self.buffer.read(buf, None).await?) - } - - /// ReadRTP reads and decrypts full RTP packet and its header from the nextConn - pub async fn read_rtp(&self, buf: &mut [u8]) -> Result { - if !self.is_rtp { - return Err(Error::InvalidRtpStream); - } - - let n = self.buffer.read(buf, None).await?; - let mut b = &buf[..n]; - let pkt = rtp::packet::Packet::unmarshal(&mut b)?; - - Ok(pkt) - } - - /// read_rtcp reads and decrypts full RTP packet and its header from the nextConn - pub async fn read_rtcp( - &self, - buf: &mut [u8], - ) -> Result>> { - if self.is_rtp { - return Err(Error::InvalidRtcpStream); - } - - let n = self.buffer.read(buf, None).await?; - let mut b = &buf[..n]; - let pkt = rtcp::packet::unmarshal(&mut b)?; - - Ok(pkt) - } - - /// Close removes the ReadStream from the session and cleans up any associated state - pub async fn close(&self) -> Result<()> { - self.buffer.close().await; - let _ = self.tx.send(self.ssrc).await; - Ok(()) - } -} diff --git a/stun/.gitignore b/stun/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/stun/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/stun/CHANGELOG.md b/stun/CHANGELOG.md deleted file mode 100644 index 2a6eaf605..000000000 --- a/stun/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# webrtc-stun changelog - -## Unreleased - -## v0.4.4 - -* Increased minimum support rust version to `1.60.0`. -* Increased required `webrtc-util` version to `0.7.0`. - -## v0.4.3 - -* [#9 update deps + loosen some requirements](https://github.com/webrtc-rs/stun/pull/9) by [@melekes](https://github.com/melekes). - -## Prior to 0.4.3 - -Before 0.4.3 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/stun/releases). diff --git a/stun/Cargo.toml b/stun/Cargo.toml deleted file mode 100644 index efe071965..000000000 --- a/stun/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "stun" -version = "0.6.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of STUN" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/stun" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/stun" - -[features] -default = [] -bench = [] - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn"] } - -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -lazy_static = "1" -url = "2" -rand = "0.8" -base64 = "0.21" -subtle = "2.4" -crc = "3" -ring = "0.17" -md-5 = "0.10" -thiserror = "1" - -[dev-dependencies] -tokio-test = "0.4" -clap = "3" -criterion = "0.5" - - -[[bench]] -name = "bench" -harness = false - -[[example]] -name = "stun_client" -path = "examples/stun_client.rs" -bench = false - -[[example]] -name = "stun_decode" -path = "examples/stun_decode.rs" -bench = false diff --git a/stun/LICENSE-APACHE b/stun/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/stun/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/stun/LICENSE-MIT b/stun/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/stun/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/stun/README.md b/stun/README.md deleted file mode 100644 index 1f70e3642..000000000 --- a/stun/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of STUN. Rewrite Pion STUN in Rust -

diff --git a/stun/benches/bench.rs b/stun/benches/bench.rs deleted file mode 100644 index 3c967880d..000000000 --- a/stun/benches/bench.rs +++ /dev/null @@ -1,659 +0,0 @@ -use std::io::Cursor; -use std::net::Ipv4Addr; -use std::ops::{Add, Sub}; -use std::time::Duration; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use criterion::{criterion_group, criterion_main, Criterion}; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; -use stun::addr::{AlternateServer, MappedAddress}; -use stun::agent::{noop_handler, Agent, TransactionId}; -use stun::attributes::{ - ATTR_CHANNEL_NUMBER, ATTR_DONT_FRAGMENT, ATTR_ERROR_CODE, ATTR_MESSAGE_INTEGRITY, ATTR_NONCE, - ATTR_REALM, ATTR_SOFTWARE, ATTR_USERNAME, ATTR_XORMAPPED_ADDRESS, -}; -use stun::error_code::{ErrorCode, ErrorCodeAttribute, CODE_STALE_NONCE}; -use stun::fingerprint::{FINGERPRINT, FINGERPRINT_SIZE}; -use stun::integrity::MessageIntegrity; -use stun::message::{ - is_message, Getter, Message, MessageType, Setter, ATTRIBUTE_HEADER_SIZE, BINDING_REQUEST, - CLASS_REQUEST, MESSAGE_HEADER_SIZE, METHOD_BINDING, -}; -use stun::textattrs::{Nonce, Realm, Software, Username}; -use stun::uattrs::UnknownAttributes; -use stun::xoraddr::{xor_bytes, XorMappedAddress}; -use tokio::time::Instant; - -// AGENT_COLLECT_CAP is initial capacity for Agent.Collect slices, -// sufficient to make function zero-alloc in most cases. -const AGENT_COLLECT_CAP: usize = 100; - -fn benchmark_addr(c: &mut Criterion) { - let mut m = Message::new(); - - let ma_addr = MappedAddress { - ip: "122.12.34.5".parse().unwrap(), - port: 5412, - }; - c.bench_function("BenchmarkMappedAddress_AddTo", |b| { - b.iter(|| { - ma_addr.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - - let as_addr = AlternateServer { - ip: "122.12.34.5".parse().unwrap(), - port: 5412, - }; - c.bench_function("BenchmarkAlternateServer_AddTo", |b| { - b.iter(|| { - as_addr.add_to(&mut m).unwrap(); - m.reset(); - }) - }); -} - -fn benchmark_agent(c: &mut Criterion) { - let deadline = Instant::now().add(Duration::from_secs(60 * 60 * 24)); - let gc_deadline = deadline.sub(Duration::from_secs(1)); - - { - let mut a = Agent::new(noop_handler()); - for _ in 0..AGENT_COLLECT_CAP { - a.start(TransactionId::new(), deadline).unwrap(); - } - - c.bench_function("BenchmarkAgent_GC", |b| { - b.iter(|| { - a.collect(gc_deadline).unwrap(); - }) - }); - - a.close().unwrap(); - } - - { - let mut a = Agent::new(noop_handler()); - for _ in 0..AGENT_COLLECT_CAP { - a.start(TransactionId::new(), deadline).unwrap(); - } - - let mut m = Message::new(); - m.build(&[Box::::default()]).unwrap(); - c.bench_function("BenchmarkAgent_Process", |b| { - b.iter(|| { - a.process(m.clone()).unwrap(); - }) - }); - - a.close().unwrap(); - } -} - -fn benchmark_attributes(c: &mut Criterion) { - { - let m = Message::new(); - c.bench_function("BenchmarkMessage_GetNotFound", |b| { - b.iter(|| { - let _ = m.get(ATTR_REALM); - }) - }); - } - - { - let mut m = Message::new(); - m.add(ATTR_USERNAME, &[1, 2, 3, 4, 5, 6, 7]); - c.bench_function("BenchmarkMessage_Get", |b| { - b.iter(|| { - let _ = m.get(ATTR_USERNAME); - }) - }); - } -} - -//TODO: add benchmark_client - -fn benchmark_error_code(c: &mut Criterion) { - { - let mut m = Message::new(); - c.bench_function("BenchmarkErrorCode_AddTo", |b| { - b.iter(|| { - let _ = CODE_STALE_NONCE.add_to(&mut m); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let a = ErrorCodeAttribute { - code: ErrorCode(404), - reason: b"not found!".to_vec(), - }; - c.bench_function("BenchmarkErrorCodeAttribute_AddTo", |b| { - b.iter(|| { - let _ = a.add_to(&mut m); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let mut a = ErrorCodeAttribute { - code: ErrorCode(404), - reason: b"not found!".to_vec(), - }; - let _ = a.add_to(&mut m); - c.bench_function("BenchmarkErrorCodeAttribute_GetFrom", |b| { - b.iter(|| { - a.get_from(&m).unwrap(); - }) - }); - } -} - -fn benchmark_fingerprint(c: &mut Criterion) { - { - let mut m = Message::new(); - let s = Software::new(ATTR_SOFTWARE, "software".to_owned()); - let addr = XorMappedAddress { - ip: Ipv4Addr::new(213, 1, 223, 5).into(), - port: 0, - }; - let _ = addr.add_to(&mut m); - let _ = s.add_to(&mut m); - c.bench_function("BenchmarkFingerprint_AddTo", |b| { - b.iter(|| { - let _ = FINGERPRINT.add_to(&mut m); - m.write_length(); - m.length -= (ATTRIBUTE_HEADER_SIZE + FINGERPRINT_SIZE) as u32; - m.raw.drain(m.length as usize + MESSAGE_HEADER_SIZE..); - m.attributes.0.drain(m.attributes.0.len() - 1..); - }) - }); - } - - { - let mut m = Message::new(); - let s = Software::new(ATTR_SOFTWARE, "software".to_owned()); - let addr = XorMappedAddress { - ip: Ipv4Addr::new(213, 1, 223, 5).into(), - port: 0, - }; - let _ = addr.add_to(&mut m); - let _ = s.add_to(&mut m); - m.write_header(); - FINGERPRINT.add_to(&mut m).unwrap(); - m.write_header(); - c.bench_function("BenchmarkFingerprint_Check", |b| { - b.iter(|| { - FINGERPRINT.check(&m).unwrap(); - }) - }); - } -} - -fn benchmark_message_build_overhead(c: &mut Criterion) { - let t = BINDING_REQUEST; - let username = Username::new(ATTR_USERNAME, "username".to_owned()); - let nonce = Nonce::new(ATTR_NONCE, "nonce".to_owned()); - let realm = Realm::new(ATTR_REALM, "example.org".to_owned()); - - { - let mut m = Message::new(); - c.bench_function("BenchmarkBuildOverhead/Build", |b| { - b.iter(|| { - let _ = m.build(&[ - Box::new(username.clone()), - Box::new(nonce.clone()), - Box::new(realm.clone()), - Box::new(FINGERPRINT), - ]); - }) - }); - } - - { - let mut m = Message::new(); - c.bench_function("BenchmarkBuildOverhead/Raw", |b| { - b.iter(|| { - m.reset(); - m.write_header(); - m.set_type(t); - let _ = username.add_to(&mut m); - let _ = nonce.add_to(&mut m); - let _ = realm.add_to(&mut m); - let _ = FINGERPRINT.add_to(&mut m); - }) - }); - } -} - -fn benchmark_message_integrity(c: &mut Criterion) { - { - let mut m = Message::new(); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); - m.write_header(); - c.bench_function("BenchmarkMessageIntegrity_AddTo", |b| { - b.iter(|| { - m.write_header(); - integrity.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - m.raw = Vec::with_capacity(1024); - let software = Software::new(ATTR_SOFTWARE, "software".to_owned()); - let _ = software.add_to(&mut m); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); - m.write_header(); - integrity.add_to(&mut m).unwrap(); - m.write_header(); - c.bench_function("BenchmarkMessageIntegrity_Check", |b| { - b.iter(|| { - integrity.check(&mut m).unwrap(); - }) - }); - } -} - -fn benchmark_message(c: &mut Criterion) { - { - let mut m = Message::new(); - c.bench_function("BenchmarkMessage_Write", |b| { - b.iter(|| { - m.add(ATTR_ERROR_CODE, &[0xff, 0x11, 0x12, 0x34]); - m.transaction_id = TransactionId::new(); - m.typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - m.write_header(); - m.reset(); - }) - }); - } - - { - let m = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - c.bench_function("BenchmarkMessageType_Value", |b| { - b.iter(|| { - let _ = m.value(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - length: 0, - transaction_id: TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), - ..Default::default() - }; - m.write_header(); - let mut buf = vec![]; - c.bench_function("BenchmarkMessage_WriteTo", |b| { - b.iter(|| { - { - let mut writer = Cursor::new(&mut buf); - m.write_to(&mut writer).unwrap(); - } - buf.clear(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - length: 0, - transaction_id: TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), - ..Default::default() - }; - m.write_header(); - let mut mrec = Message::new(); - c.bench_function("BenchmarkMessage_ReadFrom", |b| { - b.iter(|| { - let mut reader = Cursor::new(&m.raw); - mrec.read_from(&mut reader).unwrap(); - mrec.reset(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - length: 0, - transaction_id: TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), - ..Default::default() - }; - m.write_header(); - let mut mrec = Message::new(); - c.bench_function("BenchmarkMessage_ReadBytes", |b| { - b.iter(|| { - mrec.write(&m.raw).unwrap(); - mrec.reset(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - transaction_id: TransactionId::new(), - ..Default::default() - }; - let software = Software::new(ATTR_SOFTWARE, "cydev/stun test".to_owned()); - software.add_to(&mut m).unwrap(); - m.write_header(); - c.bench_function("BenchmarkIsMessage", |b| { - b.iter(|| { - assert!(is_message(&m.raw), "Should be message"); - }) - }); - } - - { - let mut m = Message::new(); - m.write_header(); - c.bench_function("BenchmarkMessage_NewTransactionID", |b| { - b.iter(|| { - m.new_transaction_id().unwrap(); - }) - }); - } - - { - let mut m = Message::new(); - let s = Software::new(ATTR_SOFTWARE, "software".to_owned()); - let addr = XorMappedAddress { - ip: Ipv4Addr::new(213, 1, 223, 5).into(), - ..Default::default() - }; - c.bench_function("BenchmarkMessageFull", |b| { - b.iter(|| { - addr.add_to(&mut m).unwrap(); - s.add_to(&mut m).unwrap(); - m.write_attributes(); - m.write_header(); - FINGERPRINT.add_to(&mut m).unwrap(); - m.write_header(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let s = Software::new(ATTR_SOFTWARE, "software".to_owned()); - let addr = XorMappedAddress { - ip: Ipv4Addr::new(213, 1, 223, 5).into(), - ..Default::default() - }; - c.bench_function("BenchmarkMessageFullHardcore", |b| { - b.iter(|| { - addr.add_to(&mut m).unwrap(); - s.add_to(&mut m).unwrap(); - m.write_header(); - m.reset(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - transaction_id: TransactionId::new(), - raw: vec![0u8; 128], - ..Default::default() - }; - c.bench_function("BenchmarkMessage_WriteHeader", |b| { - b.iter(|| { - m.write_header(); - }) - }); - } - - { - let mut m = Message::new(); - m.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2])), - Box::new(Software::new(ATTR_SOFTWARE, "webrtc-rs/stun".to_owned())), - Box::new(MessageIntegrity::new_long_term_integrity( - "username".to_owned(), - "realm".to_owned(), - "password".to_owned(), - )), - Box::new(FINGERPRINT), - ]) - .unwrap(); - let mut a = Message::new(); - m.clone_to(&mut a).unwrap(); - c.bench_function("BenchmarkMessage_CloneTo", |b| { - b.iter(|| { - m.clone_to(&mut a).unwrap(); - }) - }); - } - - { - let mut m = Message::new(); - m.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2])), - Box::new(FINGERPRINT), - ]) - .unwrap(); - let mut a = Message::new(); - m.clone_to(&mut a).unwrap(); - c.bench_function("BenchmarkMessage_AddTo", |b| { - b.iter(|| { - m.add_to(&mut a).unwrap(); - }) - }); - } - - { - let typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ, - transaction_id: TransactionId::new(), - ..Default::default() - }; - m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); - m.write_header(); - let mut mdecoded = Message::new(); - c.bench_function("BenchmarkDecode", |b| { - b.iter(|| { - mdecoded.reset(); - mdecoded.raw.clone_from(&m.raw); - mdecoded.decode().unwrap(); - }) - }); - } -} - -fn benchmark_text_attributes(c: &mut Criterion) { - { - let mut m = Message::new(); - let u = Username::new(ATTR_USERNAME, "test".to_owned()); - c.bench_function("BenchmarkUsername_AddTo", |b| { - b.iter(|| { - u.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let mut u = Username::new(ATTR_USERNAME, "test".to_owned()); - u.add_to(&mut m).unwrap(); - c.bench_function("BenchmarkUsername_GetFrom", |b| { - b.iter(|| { - u.get_from(&m).unwrap(); - u.text.clear(); - }) - }); - } - - { - let mut m = Message::new(); - let n = Nonce::new(ATTR_NONCE, "nonce".to_owned()); - c.bench_function("BenchmarkNonce_AddTo", |b| { - b.iter(|| { - n.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let nonce = String::from_utf8(vec![b'a'; 2048]).unwrap(); - let n = Nonce::new(ATTR_NONCE, nonce); - c.bench_function("BenchmarkNonce_AddTo_BadLength", |b| { - b.iter(|| { - assert!(n.add_to(&mut m).is_err()); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let mut n = Nonce::new(ATTR_NONCE, "nonce".to_owned()); - n.add_to(&mut m).unwrap(); - c.bench_function("BenchmarkNonce_GetFrom", |b| { - b.iter(|| { - n.get_from(&m).unwrap(); - }) - }); - } -} - -fn benchmark_unknown_attributes(c: &mut Criterion) { - let mut m = Message::new(); - let a = UnknownAttributes(vec![ - ATTR_DONT_FRAGMENT, - ATTR_CHANNEL_NUMBER, - ATTR_REALM, - ATTR_MESSAGE_INTEGRITY, - ]); - - { - c.bench_function("BenchmarkUnknownAttributes/AddTo", |b| { - b.iter(|| { - a.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - a.add_to(&mut m).unwrap(); - let mut attrs = UnknownAttributes(Vec::with_capacity(10)); - c.bench_function("BenchmarkUnknownAttributes/GetFrom", |b| { - b.iter(|| { - attrs.get_from(&m).unwrap(); - attrs.0.clear(); - }) - }); - } -} - -fn benchmark_xor(c: &mut Criterion) { - let mut r = StdRng::seed_from_u64(666); - let mut a = [0u8; 1024]; - let mut d = [0u8; 1024]; - r.fill(&mut a); - r.fill(&mut d); - let mut dst = [0u8; 1024]; - c.bench_function("BenchmarkXOR", |b| { - b.iter(|| { - let _ = xor_bytes(&mut dst, &a, &d); - }) - }); -} - -fn benchmark_xoraddr(c: &mut Criterion) { - { - let mut m = Message::new(); - let ip = "192.168.1.32".parse().unwrap(); - c.bench_function("BenchmarkXORMappedAddress_AddTo", |b| { - b.iter(|| { - let addr = XorMappedAddress { ip, port: 3654 }; - addr.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); - - m.transaction_id.0.copy_from_slice(&transaction_id); - let addr_value = [0, 1, 156, 213, 244, 159, 56, 174]; //hex.DecodeString("00019cd5f49f38ae") - m.add(ATTR_XORMAPPED_ADDRESS, &addr_value); - let mut addr = XorMappedAddress::default(); - c.bench_function("BenchmarkXORMappedAddress_GetFrom", |b| { - b.iter(|| { - addr.get_from(&m).unwrap(); - }) - }); - } -} - -criterion_group!( - benches, - benchmark_addr, - benchmark_agent, - benchmark_attributes, - //TODO: benchmark_client, - benchmark_error_code, - benchmark_fingerprint, - benchmark_message_build_overhead, - benchmark_message_integrity, - benchmark_message, - benchmark_text_attributes, - benchmark_unknown_attributes, - benchmark_xor, - benchmark_xoraddr, -); -criterion_main!(benches); diff --git a/stun/codecov.yml b/stun/codecov.yml deleted file mode 100644 index 4b006c24c..000000000 --- a/stun/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 5ed548cd-073b-4748-b584-ca2d637027bf - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/stun/doc/webrtc.rs.png b/stun/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 zcmb4qgL7ri^Y*>Tjm?d1I~&`!ZQHhO+t}FV#!hyV?8dg8{pItmdjEj8ZcW{)o}N>4 zx}T@J&zwF{it-W&usE;)002QsQdAiL01Ey$pdr5`Gl^x@001?ix2mSAvXKXoqqBp# zm8}_(tCyo0k(sBJIRN0fUYBc?uFIKR_Rj>P7X%1*;=-D%!e739>vp6Rv;H}K%AjN% z%DcHW3_g8n$P~)+@55m2gYff&+%v=3t}J8sh4Nu}Z#(Bu;NSg|$Mwm_$ETD)A6$R` zrT-_CFeWVkg zB7gp^)W4@cMDlL#-2Aql!*4&XKC#q4d>8(88yHBYX{~4BYQ>HRdws?~;s^PC;=ZrF z@MNC&nO~*t|CJFC9}0bc^c3jx{&xoY;Fu8Ta|y`z`-hZ5IR51JwxpYWg?BTv^+~kf zU+~U3+Q0eBKc4FIGbGpeb!zWDbH96|_vp@*6vMp5m-JN5{n-io@AoWcA41yqs`reS zzCM9Gff=NJ&D)ENf_JT=h@aKE-;{qDh2qY>bqyIuQs-;F;Os>|>f5NBB*Z)zS>01^ zYHIb`#ii-od{1Z8dgn^NzTDXQP`BLlg8GXC{teD|pYUHtz{Bvr2||ynMRbTQ|5=Q^ z=xCepqO_&09vLaG{)E+xpA+id_QWP z9NhvK2hg3arVWD|!TLZ`z``o#RpiX^F)##i0{YPj*wEZP^F-#Xv}0>qrKdwy_MWFP zJelPwLo~RgI!!aXpNyEaWwP(f%aKJ%^cA@-6YQKBJ_MuEG@Yr+F@pB0HC2b&)^&A{ z+}W+m&a`!HtKPVL?%?1qjJ=PmK7^!QgK>~+I6VQ^ld((Y*Pb;L5qKx6~E+?yuMxn%(Y8Gwqw+8y6n$x~*5$p9gdQ3|EFc zR(5r$dw+cDvg^CGes+i==bF}G(_*R^j@GCL2F7c%ZV5{((N?hE>1C#qhD>YYwuh+4 zlFL$#_zustzL^|+mbkfeen^~Q{`)h@MgQmD*d$l_<&U#R&YHGSm$SNGlyK}7e}D4( z`>#7TA|$OpC-leXjGvT8K=l!I%`50^T(C;VbM3Edft*R=r%h5Q?HmeQ5X7YqDrRJPoft#UYad_=m;>;ETqfA_uXdOBL=cu9JB{E$F-gVVoF;&dO{;C%YrVp;) zPB%dn6N3!zCBJBkTQj+mU=z{7=x@357Pr4`<#9bRw2v9yJb~bOJCaGLgSvtI^a~_; zpoH`UQ$Z#!S}p5*0>8HtXU>yJBc!OoU~3@2sPY0S6Ez{~~cM>p^!14V=!w?Tru|)Yb2A zoA?OH(gHH40Qaf55cJNXMB~19*UoD0a=M*ieUYlZU$J{cuiavQpjHQ3cCNm*_Mj4F zAV%>@DdeP$f?EUFi)kerkdQP#1R&gJ?m}l-;?Cuz^S*<~1YbTUR2&047A&ckI3a6Z|`JmmU_0Sj^OWAk%Bfgv5<6}c+rs{BbNkRrL zW9lA7Q~acKKAdKrNwqfcVXV$j0*L==bbQU4);_34>|}{ISs^ z+=QBD3xuJ$)K6C_qwdX+oLQ3*?h>=r*(~IZKf1I!fp>_CQ}=6Dv7UEMw7~3*G^+Uh z$50BRUd=$*dXF?OJJY|x9KDq5km0O@Ey>-@bUeE%!Pq=b0*c7)??4D*;@24J3z;n< zYTe1g49U2c2|MDpA36}RP+iZ<*G!X}1hw=%i`chjp`y1y5jK(Qg9eC{TdwM{bhz+!tMACo{i?5NK&}&s@cJvJrZ+Kfh+43lw$j^md$W9Z;2Rf9CB(qE4BX z%M+P0{Bl(`VAjXF8*^6T#fr*;ak`#P*x1Y`JDwzZo7w=ktR0hu6hyw|Yc6nvSu@K+ z)}y2AI9ao$+gFZcNl6@WuYp|bw=1=8Tc)JqBU0x_F}FGjRNY2xT=OiH4qtJib)=;u z3g^^xq)ZtPo8?2}&p#_=25N}M!W)~SB$C~#w}M|HtQwV6hY*a3Q+I$nUYCmm&z(%} zC}UWop&a}(mc}%Kw-^DL$5= zj0rRWUbHDV7I*@9TLBj17SG?Elr@7fzA^|P;HA-V7#1a^z`Lm`gK-DN3(A@&yYeAYLAXPyCym-# znC|}GLqjKn_PV?n7IH?XVBVwRuVyw~PY~;K)e&DLqR!$phbW$z zMjMo^CN*M24|5godW4g&06{xOAY%-S9t7FL@>7h=1M>cII1uC! zLj+HWyX6D5f;oor3JJQ5go@{dgjgCtFNIhGSVM*(HhNmD;FA?o3MgyX;mvNFzzpf+ z*?LW^;>a5!fv_c%6@INu2HQSi9~;f$ON3)uQ+wcF17jm-Lv4u7aP|g%SrVLL-IhU1 zR)-hO*ep)rTekEB-a(~)Xpc|%Yz8WfWTh1PDHhzT7 z(?FD2aA7cysf{O@jP3g3l7i1b*8yhBCrXZ@>tJ2pZ_5Zq1f~7Ak0HusxMdS1MYX@^!uE;B1M#SUi(2tHm?5FvyTS8B7Bbk|_uT-*aW!-B|Iv!Qg6spzp-zTu;$Zp8G6E&8w695FWfQWeLK zsN4noQ13EnuxpKoySdSaOWy?%%ZEE(2;c>UCn4e@Fm~1ejlBpK6Rp&Y7kgH5Oiq@0 z-(EAV7*=@CrP3$lb5->3m9|!6x{hAhCYTp2f1#ish$FO)|wL17Yh> zvb~6XZZsF$AR--Jafn;yIkfF@?9ma=?UoC6swl-}bx8HXgolyZ=L?n($Pvhly2j{D z!k^x;Vgo|OXGm`MD&*`PlbST>no0UN+r*^|8EW-tw!~Sp!ru!Or7Y0ljYnvVF)W&i zbg^LdI(TT-Y`m+GbVwUsPuEDi3mf?;OViv7S+bdI=jm4BAq;h-p2CP*Pfg36B z=PyY|QTSECa9}llrGo;VomOktjb%OQ3cxx6x^={a*lO1_ME5-Dh?GL zmQJdV)c5>OHnh@0GJIfT-LP+C4;k|x7UMxET*`SbNh-B;(WHE1egmN z51Fd@imCHWJdFZLSpvb8sq>1=NFh zo==R?!9n2cE7X|AzXKkIFbN9mqIxr;K~4A>!46~uo;GIgS!Xlk+NPb{%jw57tYr&@ z@nU^OKZlGY@&J{h*TI-@dqDbF*!mcj@~w`ku8S2B)YPO>6XZHVpg=fS0P~9Y2vO8~ zC+VqqC+(?b$nG2Y8}LC@(W0A4;u-NXrP3=d!GngYTx@H!F$ znHrA(L0Yua$O@pMsi2lVg*xOmFrDv&$*QvXDf+S#(BA5a3msN8*vu6@vj3<0Iq;;=@2Er@_#Cnv=$f9yUVP2kYb)Rl zpbm;@pS_!qAEfk|EgXWfb}&%hLO77{f|>U1ZCMps)CCeVlI|<36F<=Q7W6=+_}abCI{r zNr)lBDkWrWR#PVW@5o^_Sr`j1eJ(?iu1(Z|s-BJ$gh+JV)5)71X3us_E=I-{xKQZm4<%xH({H{ap>uIbCEEML&P9)FlP)vQ10@j!);yM$ zT@uS7!$OD&!{L=8E^66*87QS&RUn&?l?t0caI|K2N)$a%0R4m04iN157k%~*w0+45 z`W9Fd3%BB=fX#nv*cfb=p|`mEBK3=^l+=n@Rhj4|tp`sr)L-&84;Nl7K?IB*3&wDH z)?d4NavTZO{)YeDayVt2;ZL~5I*c%pnDTkENRB@*vv*lZ=A#AB@V9^;yJ@hUGmfCt zK62FjYv?%sE*}~^v?+$5nlFioC%k!!d2%>3H@NG-L=!Y606%lUUa3`-2pz87dK|w@ zP4zB&5q3Lh{LQ$K(QDe6DNpouXD6yT#Qje-DiL0T=k%Gd9GYs>`IDv+9lG*F$uct` zJTDO1&x))R3w7y;n-irj^&jnpAX&$*eIQT@= zu(EeL;V<3H{1u7Ot|670B>Z1Al0rPO$a`Md*DFDZp(7e30We~+7vfa1x2r}^G8reh z07e2fqniB|U zM8?6`G&%s~bT~46m$;jna?=$5nV|7(*ipL?k$tF&*>)wh$o&`r4qH&b7AzUe(Fo?J zU0}8>*l*;7>g;Vwu{;wm3| z>QFbLG1Ne9Ea6LY0Qz>Sbg2c?d-%dpma1*(R`bn@Y(CYBuv*HR zw1aF#?}h1^7Vn&wc^?dYPbUObl3}NIXaMim4?RejWP)XZVPP~bXY|mr$;xVb<8ni| zOc#@+#56hPAg7@ML73_~t`;@2$#i@XM{DwV-cZ<{%Z0thDpq2B^?PPlME@?U`q-m1Q=>%!QTZ;O3z((Z)ki z7kbQFwJalo&jY3+Mqs#5<#f~c!|n&9RS|yyiA^FSEltdY#J}GP#)!g+hAv7_D1`Op zfMOb@mB~(NiHfumqyN#L&SM$Um!mr}I6`!3%|xFvMa8=8vM@A`?wdZX^B`|YysZGB zkYJUE{g@O~Td96dlSZPi<+;+2;Hb7;w=A$$A(vW5UY>p`B3F-Ss6C|8aPC7x``co>t zU^T1*dLn-`qFNb;h}jxfgPNrw>+{BcJVEt zIVl@hB+=6?i?XNz87R&VEWvSue<7CL{ z2oG!vDCGUU$lDkZq01t_h(h9rd7Yft*h}L<67?&#Jc45Y1g_aSnc*B)#n=}mv7Mfu zN|VDJHHPzspa}U6JL{#8Z;0hkO#_VED$3R3G)&8t2u)>}8t`x`VPvXEbTMG}jgHDl z_SR3TJl`ir&qhk}WL5QuW^g(zx(K6$g_)@8p(RS>yg)1XrGl{DLXok+ zp&66PD?sXL(QekmJUyk3!irEZ7ldrp(ZMRtTGP7 znWi%=FtjctrWggEZR@=0>*dwxO%X9H)NgvOWY8DZ!>-chZgf7Oj*=%}_+gu>TFC<= zM}fk>G>GuHaix3_vDh9eR&TvKR$57dg_2EMt+$8Kh|%S>J{h`MRjvQK4;%rhja z%<~EnT4wpFM|QiP_HMsk{&mm|7cP(x)1MYIU9|dZnM@Q6@#5z;Q0-YkQDI;L961X_ z7HLemmqJ_1ZXG&z=r^E>USAcH9tmy8m|Bq!?plBUp-NscM3jiQ8RU0~MFJw4EZAoj zbcM!6h#JX3>44b$KtBoB##rxn_;A`P#t#~5(N=Ty*oII+>a)oIEc{$)2XVA(a0U<5 zoVMIqhB)Y~>Z`fPQk>>sNnnqQF&GdbtrW{eQ|W~tiuvx-V~CF@nxx~Y9>t($W{0+SObuMthm)2pE5H~fYOmHS2~z#-)mLYLy6G|l)^mnZq6hmRSn*JC1p%|20L;jz^yHOv)$aQP%$ zmflFf~79JvwG0vd)A$Un0nGx}|%%fqplB?S5p0lBsBRND~#9 zB98MK7);XA6y0t}t}NSAF!Hgjs2VUP{w;(TO0j@u$TnhP#G2+AW)?~ZI5DcV z)c{Nhymi57iyJz=ri4BzdS}n_t-6D{tFJG_VUG!h1x0a$oHkUdz5GH~oXHv!<-Mo26 zEvA{>8BrXS<$qDYx&|@Tc3XhtsxbjQGcb~QTA;_0DsD|K8>U(%n$R>z#uEOQ0OO@anz${U!&evo>1eCR)Qi;Lm z<*G@q#=BFxv_%KV=fC?a1Hs({xo)%q-a>c}l{QZ4^}3zc57qq*lbOR#pJvMucMY9Rnl6%mDP-yTzKi{*YC@g)CR2Jb)4rzzEgFWkUzen1 zMO$qSJfv6Rjfut|1NZG{EJ#+GzsE;SCh(E*G-fdKdxBT32 z*k(nmqP@G=Wz)mpA2j;rx4$m z#TS3W+lF>^-fozQS)u*m?KA_C6Gg^GGQCiE%=2>^RPaT9-ZOl?;cKg4kg>bA0k5O1 zja_d-ZmLxwAB#fEgUgbN8}u;Fj0UyESY|<@%tjZy{ch9yR1`h!INw}fJpIrOM795! z1u;MY=xK7hZV<_Fvt^)A?}B@zLLCDKuji-gRZuK+=Y3W$>-k70nr&F>0a*u1~gyzPZqg_7?Bp={YZX(h}sTFc5WDDJxt6Q8HA z{|@|c?eSJjKw}ws_@m~7?NJ2ZN<4?;l~@xZ$Y1TCb`QR#E4oDsKnbkwzVJ3!YK0|T zVRYq)ong5#4P8`|;7-v5=g28>!X@LW)WdclA@Iusvp_$z} z{i09t)xz|_!KN-LW*lCY)rza`nQ+l1Ey;c*7RkRa>Pu-J=O`el=q+SmPhn@7dgfV zrZRbSwh!xVvDOO#kCOs^z`xaCzz!x^G{D`GwoqBU-W-lBqOPA<;i02uhN*B7@PmaI zbfj^R8USXsoiblQhqQ=mv1g=nrAnQawu}w?Qwu%nf+9~eSlM1;pWqk%*02uJ2M6w1~H-zq315?xDlDqFk&_Co^xyHyovAAU! zQupRV_9qZn(5=i{v*x8nzE{y-&Un~XtrV?j^{HmaJT}H-F5`FwI!z|UYH`x<7+)3V z@UfG;gKX zW0(t&F_0s|)@JzT;a{OKDg~UN(hEvD2kI>+D~Vu< z75lpYi<{EQZSUD(56cI8b zvHa@&B6;w%a8%+c{gy{8!!{+$N?mSe>8ix`SE+ciUu1N5 zcBFbr&P*fk3P~gaSiC;{crW{V`uk!mHnNNOeoFyNXqUCiYIyqQ)uO)wyrHtJsGh6V z45o(N6BFrBA+PmzlxqiJeuRFuZNY5zsVDicXyJ$o*sda_yF15yapw?j7woa9|NZLL z4+z0rCYW`zRAI?^ZDFGmqG*;k^L;#Nl?w13CRyCEpA!)Fpj3ZWaOm2hG;+bLU%^6= zcB_WAZm{x!8ae{pK>9tw*_x;_>7_LIrBhK!vC%VMMP(jQgO>|~7% zEDuXXq}ZQGo%7PX%SKKAAzzy%sb&~2)jXIOP%BQJI(hGaF~0fcY40)$g(wOtqHm5~ zKa5C3&tnb2Mrfp_Biez3+f(oGB9$rr;{AHJ(5@+$(lI3c~W=QgPM94a*70RI#EewwS^co*l|{x|IDP*6IqKm8W?V zW}8Bbi);zqyL=VHJpY;8sxeg)FYo)pjVlV-p!`{wlpl}`cLeUUKD*!0Q1tljk zbv8L!Ke@{!@2`C~ZYrsmSAM|RfG(nRde&o5{6tB}3Qe@uSyvfTm99Q{PVPYvT_!i* z`3|cM${k(HQ>XT##JL+vv?F_GjeXw&Xmw#$#EJ+8SlGDJcAB&`*_9>?pd>>iU&CNz z+v7JX8{`6aMxi@m7tL1XSg?4`V-V6~-aY92gl- zusI&&P_p0FDT;iwk^L`94_gGz1juy87}+R=q7waP$DZmGxF3?%A4w}DL=pZ|1Bu@- zq9+nkV+H(gPw5O}UkG&ScI7@2RV*?bHn&NrhvqM=95N37P+h@GI^7{K8FfVXAAzKm zd49L=WdSMqB)pgESwXv%V51{D88If1=zz`OM=rq%mUwFA@2Hn?w|)cxy<#Y&I6q&>%bg7zJ=|1__bkL zIScrkD>2Fq=A9i3SBSk(EsuZS+_iMP3V!?re)Ua;T8W4#N{NX4zgEew2FYB%BtFSu zLA+4|6&W>c{Bxq+#3F7jWDKWe8uTLJN(^l`rp|k~Y&J4#hWKEc!NG*l>X4Y`dc^u* zqG#al{&DFs3Hc`@eM{ucb_e0rCEw*=pm6UrTa69gyz7Q6`K~cVHw~o0LI^*IDWqYV z&O|;r`5MpQ>H6k@VXV1l?t^Q{de%5lfh^xF+zQPBS;1i)^2l!&ns$=rhU+F%>KmE= ze%InY=PX19L_Rk2QkB;(bff06VOfaQ!g6FqWJz_oN?)Q?)1+5SvwD5gD&Gs72$!E( z))`ww(f~<1kx*zbjWpG;c&vIJIvn+Sg3oh=Zoeo)m@YY!GhHDVLem-4zmTmz+AkAP z=E_uvAbq?e6A<(zSX}m=FCmZo9_U&CT*G_ApbDI|pmc3r`yTpu3zDIJBfAMo_Yx4I z5Nd=k?Wcl?)dAtXmfhR>==2IGt*Y5aaCC%@#ia82v-jsuUhjub`N|fO zZmx5FJ&xeSR|hMkv5bT$;NO40!rqFMFUi-zvX;wNv+LS_11L$WH6BMdd(GWsPAY8C$HlG(5 zlHy1Mh>_sP;K1l$X_Q7o@Twxp96=(Y(L$_~#KioW%fDNMFiDA~%~Bi_=ZAfB!zI7- z@;Y4(H|KIUW+B3aixjEQVgJAWBtlt&ii@@2;&=;*kqd(ekpassLCA&}iN+!52rRXU zmoWeCLz^d^VaWqwGj=>GuYi2yp@228y1-RILu~mBvh7F&M40bnM*WnwCqSvm#V^HZ z8Vl0p)OneZKGfG_T3?J46?G!@A#ot?G_n_5X$@tOB3NZ5dYf<|_T@JwU11HNHU{Q6 ztZixo8Gau&H1;VBbt-@+ch>uu!mu=uSWpfh^MZ8H1NZvZae%7)T49*N;?i>aOvt++ zXwI%WHyqx-aPgItwO?DIqB*Uh-O&RMZ!R;^f$=9Zbt08kQPzLWbddQ!L3nw;}xhj8XGF0>TzgX zXAb%Ya9Lxu@*38K2*4dK65kYV4MuyfL~Wq5t6`;N*v;lCt|crM2#cS{d5RmRy{eg& zMllR~v0T{@4Zu)~Mfm%rccN5S0O>#_Qwgo86lNQ^0a2@zUWtR>BJ>=GTq0XUA}F@f z(_2nOtD5)1?x~a|Bo-K59rbCCe%;+9n#K`k2UdRt{+9Nibt^YH*QO?6)jr|bRM9KH z7M4i_hyxcUQM(4f*sj2%CedlYR8?n1d_=W`I6Wn?>Zc1qib^o=@P7yVzLc!R`ATd5 zCwU&IL$#)rIgaMQ9jN?Isi-*a^i@~&(QeJx9#&(A^ZF^+rP4RRdw=V%W11q*MAg^c znLR}m$o;#@ZYZFY%>Sbgk?p*MmQcUsan1j(Dg>=IxO}Jd{Vj`HH%PKF4g4t7+v+4M z#-eCev4KYA>-KjRS~6}^SH#nHIA&`k(VKHh^?TXYbpwLtlP@+ zb)|jChy`^(7l|zpIHb`|Niqd`1e|R|I^r(S&)|bVGAO17iBM-zEiD*BatW^mx5gWy zYve`i>4#sFT2Gf5Nx=}`!E=QgzE#4oi8y0ym*vZ+?Ijdq;;=7V6FP83rv9^2({|^) zf#e3@-_999V9U`2>q+`UwE??DzjK{ZQvqH1S>^N+o$1ssfne3e(EivEIx47=V9CfM zK!l(}$zZht@iJxbI72q;k{!3K1J*gUXa1QWNqaE+$+x$T*h80X7nIq8(+Mgd9icL; zdDf*63yi6yT1~Wvxpm`~*2JG!66PNwRT#;Km3fP_=VqZqSm1+}p5OMc+G5hyxPR_h zJGV>vX^EpmjafWj$3YXz@iD(uS}|$BZow}JnrFKcX#qn|i!}8QWh3HcpdN{yF$R^E znMz$m7~+t5nRiFku%a>g-=UP)p+S5}zTy;=W^7kemN?ZF6`%%F^HJ9W`V<)A0Ks`J z1+l5eN7|K}EHB|_Twyykb$RRJ1)u`=u9TfbO$@(yC0JlZpb1NK!hwCgvVsbG5Zs-) zEFb(WzHOV)aSI&%6lH^KzYLT+i6uxkL6vm8Y=BTmhPP5F9BWWfMWcN;&JaG>h$UKU zpGn1)RE5t712`00`Ns5XSCBt6ejJJA18nNy4Q)Uh6ruRCE;Gcc@GN{olkyC-WOubP z69E0#9aX|EYrCoai?Y4^4De+=0!Qi+c4GCjNqxgpHzW*R68w%TaPSsp85v~mncA(4 zWqtC>flK_XE6xg{Rfw9!Q$aCowZ6Tyj&nHV&_EFgY#HE|=UKxx^vg%4`HTV6-Wz0> zM-~9k|03T#2VzC0DQ%X38rIpo+ONB}Iv=LCNq2o~00@U8Sy=nr)!NP|TL>P1?2 z|7t;jtRF`sr0mn5uJ5oY*e3?%n9|Lp0=jTj%EqRTk%FS?eQ}4njrHfF1~5fiwI#>{ zZLSN+3FHZ1npT|`GPO~I(y*+GBTxm~QTpTfpR?7037xNXnc&tMzdGkMNKuvpIXo8| zf>$ShLNffRQiPl6I$dTw?gRVoRs!W8G!-ax>B#f#jPu9*9VHMK*`7~m8lW3N=|($$ z(b~q)|2c*`$YNz2iz=vL|M&_?FXUX&y}7tWKScYEQzb|S){NT~Dv|%ZQfZj+bh?8% zEDcF^XV!Aoo?Bf3c+ENt?wQN8xAJ%%koMA~-j7Gt`7=Esa!pnlOmDeL*0*ENTF(&J z$r7z34bY3qT2M)h^=RLiyuTTr>UPF#)+6$O$#YH7-mFI*K_cqz+mNF{h2~GExKsx_ zhMi_Y0O`A|MWihjnw9CDks)6}{)Wie6RMMTc3Bvtzq`Upq;p$aFnXFO;Nq)>$d24u zB!H{mHxiz|s|H-!s-@UidoO*TPl5c*_r!%&*F}oK7LivkOP?8rahQOUlG3bGKV7NQ zIdOP`&FFJEN~(|_jzZJ?u^uTg;F0LEh~K)4$1x;I`Kbg}$$5x(LvgmtK_;$2;~tnX zwlwz2i$)-;XRxM|o|l>`YHm&uhr>?unA!jN8lz+18J0q+&}6s4vEJ@raNby52v&A zIgiI9{%^0(QiqdC-@nf{l7vQ0y6If5l;H@ZlyKr)jFLc zg18!Ok)k>n-}NpP^yd~Hgy@boz%18p90g{VvWmZSd${cRrr;XH?BpZ~2S+;GHwN^V z-WBVE<~e@aYps6&k6(=@5}#W~a|K7gEeh!GH=OpyZsr93xshIM^%nraArrG-xB4gv zeXoTYlZ>UMVH+D8ODih6$H1TZAhqit6xt_b858LeePGnVG!NL>54^TM!PeeE*&F-& z^&*H@T@l&cG|lepBYqbH@}ZFPOc{gXYmhc zixSje>9 zDw=LpM~L>ya@=!SdHL;0MZxVw`xYkGPp8+GyOaYcC@3_n<9FT`O8H!>d#x`=3Vht7 z5eB?7f4P3GfOQ!sn5Rwvpr59r$2ie&2D_cptsUZ6&b4*-T3(U(Z7FRPJG^t zVT_E7G#m&7#^Q3uJM_JX2h47-p!>g^!Gp={Fk!v@)P3;*PR~d%UVt_^jKJf-Dwn9VLz$7 zS$R^1qG7>oy0P0J)OnOo`^ZB`m}ft~^BbD81gIYiI;^5_44Z6q!`i-( z7DK#VyndKZ$m;Fsp*i4Iw^_i3D68#BReHq`Y|j+YJ=P;DJ9De*R1Ye1?8Lv|3+?0X zp|$zDxjPyISNq%{YHM4&@u#Mnvj=Y<$N1zc>lJ63odR4%l}MtZqR#(xXME8D!+$&b zj^O2PPGHVs%TL1Bm$#~_Ds1(o@5`QeeBS6eGvTAgMr-wYym!;A5b;{Q-t1or=U=q_ zLd(sj%N&>8h$i~Yt(3Gh2?IlXXej7_YIbZ)q+Z>04Lc!#)*CbVp9?$q+C4|&z3air z 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 G`0_t~Oa}h| diff --git a/stun/examples/stun_client.rs b/stun/examples/stun_client.rs deleted file mode 100644 index 48e2c0415..000000000 --- a/stun/examples/stun_client.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::sync::Arc; - -use clap::{App, Arg}; -use stun::agent::*; -use stun::client::*; -use stun::message::*; -use stun::xoraddr::*; -use stun::Error; -use tokio::net::UdpSocket; - -#[tokio::main] -async fn main() -> Result<(), Error> { - let mut app = App::new("STUN Client") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of STUN Client") - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("server") - .required_unless("FULLHELP") - .takes_value(true) - .default_value("stun.l.google.com:19302") - .long("server") - .help("STUN Server"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let server = matches.value_of("server").unwrap(); - - let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); - - let conn = UdpSocket::bind("0:0").await?; - println!("Local address: {}", conn.local_addr()?); - - println!("Connecting to: {server}"); - conn.connect(server).await?; - - let mut client = ClientBuilder::new().with_conn(Arc::new(conn)).build()?; - - let mut msg = Message::new(); - msg.build(&[Box::::default(), Box::new(BINDING_REQUEST)])?; - - client.send(&msg, Some(Arc::new(handler_tx))).await?; - - if let Some(event) = handler_rx.recv().await { - let msg = event.event_body?; - let mut xor_addr = XorMappedAddress::default(); - xor_addr.get_from(&msg)?; - println!("Got response: {xor_addr}"); - } - - client.close().await?; - - Ok(()) -} diff --git a/stun/examples/stun_decode.rs b/stun/examples/stun_decode.rs deleted file mode 100644 index 959cbc15a..000000000 --- a/stun/examples/stun_decode.rs +++ /dev/null @@ -1,44 +0,0 @@ -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use clap::{App, Arg}; -use stun::message::Message; - -fn main() { - let mut app = App::new("STUN decode") - .version("0.1.0") - .author("Jtplouffe ") - .about("An example of STUN decode") - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("data") - .required_unless("FULLHELP") - .takes_value(true) - .index(1) - .help("base64 encoded message, e.g. 'AAEAHCESpEJML0JTQWsyVXkwcmGALwAWaHR0cDovL2xvY2FsaG9zdDozMDAwLwAA'"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let encoded_data = matches.value_of("data").unwrap(); - let decoded_data = match BASE64_STANDARD.decode(encoded_data) { - Ok(d) => d, - Err(e) => panic!("Unable to decode base64 value: {e}"), - }; - - let mut message = Message::new(); - message.raw = decoded_data; - - match message.decode() { - Ok(_) => println!("{message}"), - Err(e) => panic!("Unable to decode message: {e}"), - } -} diff --git a/stun/src/addr.rs b/stun/src/addr.rs deleted file mode 100644 index cafe609ff..000000000 --- a/stun/src/addr.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[cfg(test)] -mod addr_test; - -use std::fmt; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -use crate::attributes::*; -use crate::error::*; -use crate::message::*; - -pub(crate) const FAMILY_IPV4: u16 = 0x01; -pub(crate) const FAMILY_IPV6: u16 = 0x02; -pub(crate) const IPV4LEN: usize = 4; -pub(crate) const IPV6LEN: usize = 16; - -/// MappedAddress represents MAPPED-ADDRESS attribute. -/// -/// This attribute is used only by servers for achieving backwards -/// compatibility with RFC 3489 clients. -/// -/// RFC 5389 Section 15.1 -pub struct MappedAddress { - pub ip: IpAddr, - pub port: u16, -} - -impl fmt::Display for MappedAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let family = match self.ip { - IpAddr::V4(_) => FAMILY_IPV4, - IpAddr::V6(_) => FAMILY_IPV6, - }; - if family == FAMILY_IPV4 { - write!(f, "{}:{}", self.ip, self.port) - } else { - write!(f, "[{}]:{}", self.ip, self.port) - } - } -} - -impl Default for MappedAddress { - fn default() -> Self { - MappedAddress { - ip: IpAddr::V4(Ipv4Addr::from(0)), - port: 0, - } - } -} - -impl Setter for MappedAddress { - /// add_to adds MAPPED-ADDRESS to message. - fn add_to(&self, m: &mut Message) -> Result<()> { - self.add_to_as(m, ATTR_MAPPED_ADDRESS) - } -} - -impl Getter for MappedAddress { - /// get_from decodes MAPPED-ADDRESS from message. - fn get_from(&mut self, m: &Message) -> Result<()> { - self.get_from_as(m, ATTR_MAPPED_ADDRESS) - } -} - -impl MappedAddress { - /// get_from_as decodes MAPPED-ADDRESS value in message m as an attribute of type t. - pub fn get_from_as(&mut self, m: &Message, t: AttrType) -> Result<()> { - let v = m.get(t)?; - if v.len() <= 4 { - return Err(Error::ErrUnexpectedEof); - } - - let family = u16::from_be_bytes([v[0], v[1]]); - if family != FAMILY_IPV6 && family != FAMILY_IPV4 { - return Err(Error::Other(format!("bad value {family}"))); - } - self.port = u16::from_be_bytes([v[2], v[3]]); - - if family == FAMILY_IPV6 { - let mut ip = [0; IPV6LEN]; - let l = std::cmp::min(ip.len(), v[4..].len()); - ip[..l].copy_from_slice(&v[4..4 + l]); - self.ip = IpAddr::V6(Ipv6Addr::from(ip)); - } else { - let mut ip = [0; IPV4LEN]; - let l = std::cmp::min(ip.len(), v[4..].len()); - ip[..l].copy_from_slice(&v[4..4 + l]); - self.ip = IpAddr::V4(Ipv4Addr::from(ip)); - }; - - Ok(()) - } - - /// add_to_as adds MAPPED-ADDRESS value to m as t attribute. - pub fn add_to_as(&self, m: &mut Message, t: AttrType) -> Result<()> { - let family = match self.ip { - IpAddr::V4(_) => FAMILY_IPV4, - IpAddr::V6(_) => FAMILY_IPV6, - }; - - let mut value = vec![0u8; 4]; - //value[0] = 0 // first 8 bits are zeroes - value[0..2].copy_from_slice(&family.to_be_bytes()); - value[2..4].copy_from_slice(&self.port.to_be_bytes()); - - match self.ip { - IpAddr::V4(ipv4) => value.extend_from_slice(&ipv4.octets()), - IpAddr::V6(ipv6) => value.extend_from_slice(&ipv6.octets()), - }; - - m.add(t, &value); - Ok(()) - } -} - -/// AlternateServer represents ALTERNATE-SERVER attribute. -/// -/// RFC 5389 Section 15.11 -pub type AlternateServer = MappedAddress; - -/// ResponseOrigin represents RESPONSE-ORIGIN attribute. -/// -/// RFC 5780 Section 7.3 -pub type ResponseOrigin = MappedAddress; - -/// OtherAddress represents OTHER-ADDRESS attribute. -/// -/// RFC 5780 Section 7.4 -pub type OtherAddress = MappedAddress; diff --git a/stun/src/addr/addr_test.rs b/stun/src/addr/addr_test.rs deleted file mode 100644 index 77f5ac6d9..000000000 --- a/stun/src/addr/addr_test.rs +++ /dev/null @@ -1,183 +0,0 @@ -use super::*; -use crate::error::*; - -#[test] -fn test_mapped_address() -> Result<()> { - let mut m = Message::new(); - let addr = MappedAddress { - ip: "122.12.34.5".parse().unwrap(), - port: 5412, - }; - assert_eq!(addr.to_string(), "122.12.34.5:5412", "bad string {addr}"); - - //"add_to" - { - addr.add_to(&mut m)?; - - //"GetFrom" - { - let mut got = MappedAddress::default(); - got.get_from(&m)?; - assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); - - //"Not found" - { - let message = Message::new(); - let result = got.get_from(&message); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "should be not found: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - } - //"Bad family" - { - let (mut v, _) = m.attributes.get(ATTR_MAPPED_ADDRESS); - v.value[0] = 32; - got.get_from(&m)? - } - //"Bad length" - { - let mut message = Message::new(); - message.add(ATTR_MAPPED_ADDRESS, &[1, 2, 3]); - let result = got.get_from(&message); - if let Err(err) = result { - assert_eq!( - Error::ErrUnexpectedEof, - err, - "<{}> should be <{}>", - err, - Error::ErrUnexpectedEof - ); - } else { - panic!("expected error, but got ok"); - } - } - } - } - - Ok(()) -} - -#[test] -fn test_mapped_address_v6() -> Result<()> { - let mut m = Message::new(); - let addr = MappedAddress { - ip: "::".parse().unwrap(), - port: 5412, - }; - - //"add_to" - { - addr.add_to(&mut m)?; - - //"GetFrom" - { - let mut got = MappedAddress::default(); - got.get_from(&m)?; - assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); - - //"Not found" - { - let message = Message::new(); - let result = got.get_from(&message); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "<{}> should be <{}>", - err, - Error::ErrAttributeNotFound, - ); - } else { - panic!("expected error, but got ok"); - } - } - } - } - Ok(()) -} - -#[test] -fn test_alternate_server() -> Result<()> { - let mut m = Message::new(); - let addr = MappedAddress { - ip: "122.12.34.5".parse().unwrap(), - port: 5412, - }; - - //"add_to" - { - addr.add_to(&mut m)?; - - //"GetFrom" - { - let mut got = AlternateServer::default(); - got.get_from(&m)?; - assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); - - //"Not found" - { - let message = Message::new(); - let result = got.get_from(&message); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "<{}> should be <{}>", - err, - Error::ErrAttributeNotFound, - ); - } else { - panic!("expected error, but got ok"); - } - } - } - } - - Ok(()) -} - -#[test] -fn test_other_address() -> Result<()> { - let mut m = Message::new(); - let addr = OtherAddress { - ip: "122.12.34.5".parse().unwrap(), - port: 5412, - }; - - //"add_to" - { - addr.add_to(&mut m)?; - - //"GetFrom" - { - let mut got = OtherAddress::default(); - got.get_from(&m)?; - assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); - - //"Not found" - { - let message = Message::new(); - let result = got.get_from(&message); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "<{}> should be <{}>", - err, - Error::ErrAttributeNotFound, - ); - } else { - panic!("expected error, but got ok"); - } - } - } - } - - Ok(()) -} diff --git a/stun/src/agent.rs b/stun/src/agent.rs deleted file mode 100644 index 4562df722..000000000 --- a/stun/src/agent.rs +++ /dev/null @@ -1,283 +0,0 @@ -#[cfg(test)] -mod agent_test; - -use std::collections::HashMap; -use std::sync::Arc; - -use rand::Rng; -use tokio::sync::mpsc; -use tokio::time::Instant; - -use crate::client::ClientTransaction; -use crate::error::*; -use crate::message::*; - -/// Handler handles state changes of transaction. -/// Handler is called on transaction state change. -/// Usage of e is valid only during call, user must -/// copy needed fields explicitly. -pub type Handler = Option>>; - -/// noop_handler just discards any event. -pub fn noop_handler() -> Handler { - None -} - -/// Agent is low-level abstraction over transaction list that -/// handles concurrency (all calls are goroutine-safe) and -/// time outs (via Collect call). -pub struct Agent { - /// transactions is map of transactions that are currently - /// in progress. Event handling is done in such way when - /// transaction is unregistered before AgentTransaction access, - /// minimizing mux lock and protecting AgentTransaction from - /// data races via unexpected concurrent access. - transactions: HashMap, - /// all calls are invalid if true - closed: bool, - /// handles transactions - handler: Handler, -} - -#[derive(Debug, Clone)] -pub enum EventType { - Callback(TransactionId), - Insert(ClientTransaction), - Remove(TransactionId), - Close, -} - -impl Default for EventType { - fn default() -> Self { - EventType::Callback(TransactionId::default()) - } -} - -/// Event is passed to Handler describing the transaction event. -/// Do not reuse outside Handler. -#[derive(Debug)] //Clone -pub struct Event { - pub event_type: EventType, - pub event_body: Result, -} - -impl Default for Event { - fn default() -> Self { - Event { - event_type: EventType::default(), - event_body: Ok(Message::default()), - } - } -} - -/// AgentTransaction represents transaction in progress. -/// Concurrent access is invalid. -pub(crate) struct AgentTransaction { - id: TransactionId, - deadline: Instant, -} - -/// AGENT_COLLECT_CAP is initial capacity for Agent.Collect slices, -/// sufficient to make function zero-alloc in most cases. -const AGENT_COLLECT_CAP: usize = 100; - -#[derive(PartialEq, Eq, Hash, Copy, Clone, Default, Debug)] -pub struct TransactionId(pub [u8; TRANSACTION_ID_SIZE]); - -impl TransactionId { - /// new returns new random transaction ID using crypto/rand - /// as source. - pub fn new() -> Self { - let mut b = TransactionId([0u8; TRANSACTION_ID_SIZE]); - rand::thread_rng().fill(&mut b.0); - b - } -} - -impl Setter for TransactionId { - fn add_to(&self, m: &mut Message) -> Result<()> { - m.transaction_id = *self; - m.write_transaction_id(); - Ok(()) - } -} - -/// ClientAgent is Agent implementation that is used by Client to -/// process transactions. -#[derive(Debug)] -pub enum ClientAgent { - Process(Message), - Collect(Instant), - Start(TransactionId, Instant), - Stop(TransactionId), - Close, -} - -impl Agent { - /// new initializes and returns new Agent with provided handler. - pub fn new(handler: Handler) -> Self { - Agent { - transactions: HashMap::new(), - closed: false, - handler, - } - } - - /// stop_with_error removes transaction from list and calls handler with - /// provided error. Can return ErrTransactionNotExists and ErrAgentClosed. - pub fn stop_with_error(&mut self, id: TransactionId, error: Error) -> Result<()> { - if self.closed { - return Err(Error::ErrAgentClosed); - } - - let v = self.transactions.remove(&id); - if let Some(t) = v { - if let Some(handler) = &self.handler { - handler.send(Event { - event_type: EventType::Callback(t.id), - event_body: Err(error), - })?; - } - Ok(()) - } else { - Err(Error::ErrTransactionNotExists) - } - } - - /// process incoming message, synchronously passing it to handler. - pub fn process(&mut self, message: Message) -> Result<()> { - if self.closed { - return Err(Error::ErrAgentClosed); - } - - self.transactions.remove(&message.transaction_id); - - let e = Event { - event_type: EventType::Callback(message.transaction_id), - event_body: Ok(message), - }; - - if let Some(handler) = &self.handler { - handler.send(e)?; - } - - Ok(()) - } - - /// close terminates all transactions with ErrAgentClosed and renders Agent to - /// closed state. - pub fn close(&mut self) -> Result<()> { - if self.closed { - return Err(Error::ErrAgentClosed); - } - - for id in self.transactions.keys() { - let e = Event { - event_type: EventType::Callback(*id), - event_body: Err(Error::ErrAgentClosed), - }; - if let Some(handler) = &self.handler { - handler.send(e)?; - } - } - self.transactions = HashMap::new(); - self.closed = true; - self.handler = noop_handler(); - - Ok(()) - } - - /// start registers transaction with provided id and deadline. - /// Could return ErrAgentClosed, ErrTransactionExists. - /// - /// Agent handler is guaranteed to be eventually called. - pub fn start(&mut self, id: TransactionId, deadline: Instant) -> Result<()> { - if self.closed { - return Err(Error::ErrAgentClosed); - } - if self.transactions.contains_key(&id) { - return Err(Error::ErrTransactionExists); - } - - self.transactions - .insert(id, AgentTransaction { id, deadline }); - - Ok(()) - } - - /// stop stops transaction by id with ErrTransactionStopped, blocking - /// until handler returns. - pub fn stop(&mut self, id: TransactionId) -> Result<()> { - self.stop_with_error(id, Error::ErrTransactionStopped) - } - - /// collect terminates all transactions that have deadline before provided - /// time, blocking until all handlers will process ErrTransactionTimeOut. - /// Will return ErrAgentClosed if agent is already closed. - /// - /// It is safe to call Collect concurrently but makes no sense. - pub fn collect(&mut self, deadline: Instant) -> Result<()> { - if self.closed { - // Doing nothing if agent is closed. - // All transactions should be already closed - // during Close() call. - return Err(Error::ErrAgentClosed); - } - - let mut to_remove: Vec = Vec::with_capacity(AGENT_COLLECT_CAP); - - // Adding all transactions with deadline before gc_time - // to toCall and to_remove slices. - // No allocs if there are less than AGENT_COLLECT_CAP - // timed out transactions. - for (id, t) in &self.transactions { - if t.deadline < deadline { - to_remove.push(*id); - } - } - // Un-registering timed out transactions. - for id in &to_remove { - self.transactions.remove(id); - } - - for id in to_remove { - let event = Event { - event_type: EventType::Callback(id), - event_body: Err(Error::ErrTransactionTimeOut), - }; - if let Some(handler) = &self.handler { - handler.send(event)?; - } - } - - Ok(()) - } - - /// set_handler sets agent handler to h. - pub fn set_handler(&mut self, h: Handler) -> Result<()> { - if self.closed { - return Err(Error::ErrAgentClosed); - } - self.handler = h; - - Ok(()) - } - - pub(crate) async fn run(mut agent: Agent, mut rx: mpsc::Receiver) { - while let Some(client_agent) = rx.recv().await { - let result = match client_agent { - ClientAgent::Process(message) => agent.process(message), - ClientAgent::Collect(deadline) => agent.collect(deadline), - ClientAgent::Start(tid, deadline) => agent.start(tid, deadline), - ClientAgent::Stop(tid) => agent.stop(tid), - ClientAgent::Close => agent.close(), - }; - - if let Err(err) = result { - if Error::ErrAgentClosed == err { - break; - } - } - } - } -} diff --git a/stun/src/agent/agent_test.rs b/stun/src/agent/agent_test.rs deleted file mode 100644 index 8ca1afa23..000000000 --- a/stun/src/agent/agent_test.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::ops::Add; - -use tokio::time::Duration; - -use super::*; -use crate::error::*; - -#[tokio::test] -async fn test_agent_process_in_transaction() -> Result<()> { - let mut m = Message::new(); - let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut a = Agent::new(Some(Arc::new(handler_tx))); - m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - a.start(m.transaction_id, Instant::now())?; - a.process(m)?; - a.close()?; - - while let Some(e) = handler_rx.recv().await { - assert!(e.event_body.is_ok(), "got error: {:?}", e.event_body); - - let tid = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - assert_eq!( - e.event_body.as_ref().unwrap().transaction_id, - tid, - "{:?} (got) != {:?} (expected)", - e.event_body.as_ref().unwrap().transaction_id, - tid - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_agent_process() -> Result<()> { - let mut m = Message::new(); - let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut a = Agent::new(Some(Arc::new(handler_tx))); - m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - a.process(m.clone())?; - a.close()?; - - while let Some(e) = handler_rx.recv().await { - assert!(e.event_body.is_ok(), "got error: {:?}", e.event_body); - - let tid = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - assert_eq!( - e.event_body.as_ref().unwrap().transaction_id, - tid, - "{:?} (got) != {:?} (expected)", - e.event_body.as_ref().unwrap().transaction_id, - tid - ); - } - - let result = a.process(m); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrAgentClosed, - "closed agent should return <{}>, but got <{}>", - Error::ErrAgentClosed, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_agent_start() -> Result<()> { - let mut a = Agent::new(noop_handler()); - let id = TransactionId::new(); - let deadline = Instant::now().add(Duration::from_secs(3600)); - a.start(id, deadline)?; - - let result = a.start(id, deadline); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrTransactionExists, - "duplicate start should return <{}>, got <{}>", - Error::ErrTransactionExists, - err, - ); - } else { - panic!("expected error, but got ok"); - } - a.close()?; - - let id = TransactionId::new(); - let result = a.start(id, deadline); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrAgentClosed, - "start on closed agent should return <{}>, got <{}>", - Error::ErrAgentClosed, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - let result = a.set_handler(noop_handler()); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrAgentClosed, - "SetHandler on closed agent should return <{}>, got <{}>", - Error::ErrAgentClosed, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_agent_stop() -> Result<()> { - let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut a = Agent::new(Some(Arc::new(handler_tx))); - - let result = a.stop(TransactionId::default()); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrTransactionNotExists, - "unexpected error: {}, should be {}", - Error::ErrTransactionNotExists, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - let id = TransactionId::new(); - let deadline = Instant::now().add(Duration::from_millis(200)); - a.start(id, deadline)?; - a.stop(id)?; - - let timeout = tokio::time::sleep(Duration::from_millis(400)); - tokio::pin!(timeout); - - tokio::select! { - evt = handler_rx.recv() => { - if let Err(err) = evt.unwrap().event_body{ - assert_eq!( - err, - Error::ErrTransactionStopped, - "unexpected error: {}, should be {}", - err, - Error::ErrTransactionStopped - ); - }else{ - panic!("expected error, got ok"); - } - } - _ = timeout.as_mut() => panic!("timed out"), - } - - a.close()?; - - let result = a.close(); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrAgentClosed, - "a.Close returned {} instead of {}", - Error::ErrAgentClosed, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - let result = a.stop(TransactionId::default()); - if let Err(err) = result { - assert_eq!( - err, - Error::ErrAgentClosed, - "unexpected error: {}, should be {}", - Error::ErrAgentClosed, - err, - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} diff --git a/stun/src/attributes.rs b/stun/src/attributes.rs deleted file mode 100644 index f51a98edb..000000000 --- a/stun/src/attributes.rs +++ /dev/null @@ -1,209 +0,0 @@ -#[cfg(test)] -mod attributes_test; - -use std::fmt; - -use crate::error::*; -use crate::message::*; - -/// Attributes is list of message attributes. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct Attributes(pub Vec); - -impl Attributes { - /// get returns first attribute from list by the type. - /// If attribute is present the RawAttribute is returned and the - /// boolean is true. Otherwise the returned RawAttribute will be - /// empty and boolean will be false. - pub fn get(&self, t: AttrType) -> (RawAttribute, bool) { - for candidate in &self.0 { - if candidate.typ == t { - return (candidate.clone(), true); - } - } - - (RawAttribute::default(), false) - } -} - -/// AttrType is attribute type. -#[derive(PartialEq, Debug, Eq, Default, Copy, Clone)] -pub struct AttrType(pub u16); - -impl fmt::Display for AttrType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let other = format!("0x{:x}", self.0); - - let s = match *self { - ATTR_MAPPED_ADDRESS => "MAPPED-ADDRESS", - ATTR_USERNAME => "USERNAME", - ATTR_ERROR_CODE => "ERROR-CODE", - ATTR_MESSAGE_INTEGRITY => "MESSAGE-INTEGRITY", - ATTR_UNKNOWN_ATTRIBUTES => "UNKNOWN-ATTRIBUTES", - ATTR_REALM => "REALM", - ATTR_NONCE => "NONCE", - ATTR_XORMAPPED_ADDRESS => "XOR-MAPPED-ADDRESS", - ATTR_SOFTWARE => "SOFTWARE", - ATTR_ALTERNATE_SERVER => "ALTERNATE-SERVER", - ATTR_FINGERPRINT => "FINGERPRINT", - ATTR_PRIORITY => "PRIORITY", - ATTR_USE_CANDIDATE => "USE-CANDIDATE", - ATTR_ICE_CONTROLLED => "ICE-CONTROLLED", - ATTR_ICE_CONTROLLING => "ICE-CONTROLLING", - ATTR_CHANNEL_NUMBER => "CHANNEL-NUMBER", - ATTR_LIFETIME => "LIFETIME", - ATTR_XOR_PEER_ADDRESS => "XOR-PEER-ADDRESS", - ATTR_DATA => "DATA", - ATTR_XOR_RELAYED_ADDRESS => "XOR-RELAYED-ADDRESS", - ATTR_EVEN_PORT => "EVEN-PORT", - ATTR_REQUESTED_TRANSPORT => "REQUESTED-TRANSPORT", - ATTR_DONT_FRAGMENT => "DONT-FRAGMENT", - ATTR_RESERVATION_TOKEN => "RESERVATION-TOKEN", - ATTR_CONNECTION_ID => "CONNECTION-ID", - ATTR_REQUESTED_ADDRESS_FAMILY => "REQUESTED-ADDRESS-FAMILY", - ATTR_MESSAGE_INTEGRITY_SHA256 => "MESSAGE-INTEGRITY-SHA256", - ATTR_PASSWORD_ALGORITHM => "PASSWORD-ALGORITHM", - ATTR_USER_HASH => "USERHASH", - ATTR_PASSWORD_ALGORITHMS => "PASSWORD-ALGORITHMS", - ATTR_ALTERNATE_DOMAIN => "ALTERNATE-DOMAIN", - _ => other.as_str(), - }; - - write!(f, "{s}") - } -} - -impl AttrType { - /// required returns true if type is from comprehension-required range (0x0000-0x7FFF). - pub fn required(&self) -> bool { - self.0 <= 0x7FFF - } - - /// optional returns true if type is from comprehension-optional range (0x8000-0xFFFF). - pub fn optional(&self) -> bool { - self.0 >= 0x8000 - } - - /// value returns uint16 representation of attribute type. - pub fn value(&self) -> u16 { - self.0 - } -} - -/// Attributes from comprehension-required range (0x0000-0x7FFF). -pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); // MAPPED-ADDRESS -pub const ATTR_USERNAME: AttrType = AttrType(0x0006); // USERNAME -pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); // MESSAGE-INTEGRITY -pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); // ERROR-CODE -pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); // UNKNOWN-ATTRIBUTES -pub const ATTR_REALM: AttrType = AttrType(0x0014); // REALM -pub const ATTR_NONCE: AttrType = AttrType(0x0015); // NONCE -pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); // XOR-MAPPED-ADDRESS - -/// Attributes from comprehension-optional range (0x8000-0xFFFF). -pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); // SOFTWARE -pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); // ALTERNATE-SERVER -pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); // FINGERPRINT - -/// Attributes from RFC 5245 ICE. -pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); // PRIORITY -pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); // USE-CANDIDATE -pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); // ICE-CONTROLLED -pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); // ICE-CONTROLLING - -/// Attributes from RFC 5766 TURN. -pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); // CHANNEL-NUMBER -pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); // LIFETIME -pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); // XOR-PEER-ADDRESS -pub const ATTR_DATA: AttrType = AttrType(0x0013); // DATA -pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); // XOR-RELAYED-ADDRESS -pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); // EVEN-PORT -pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); // REQUESTED-TRANSPORT -pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); // DONT-FRAGMENT -pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); // RESERVATION-TOKEN - -/// Attributes from RFC 5780 NAT Behavior Discovery -pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); // CHANGE-REQUEST -pub const ATTR_PADDING: AttrType = AttrType(0x0026); // PADDING -pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); // RESPONSE-PORT -pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); // CACHE-TIMEOUT -pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); // RESPONSE-ORIGIN -pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); // OTHER-ADDRESS - -/// Attributes from RFC 3489, removed by RFC 5389, -/// but still used by RFC5389-implementing software like Vovida.org, reTURNServer, etc. -pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); // SOURCE-ADDRESS -pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); // CHANGED-ADDRESS - -/// Attributes from RFC 6062 TURN Extensions for TCP Allocations. -pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); // CONNECTION-ID - -/// Attributes from RFC 6156 TURN IPv6. -pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); // REQUESTED-ADDRESS-FAMILY - -/// Attributes from An Origin Attribute for the STUN Protocol. -pub const ATTR_ORIGIN: AttrType = AttrType(0x802F); - -/// Attributes from RFC 8489 STUN. -pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); // MESSAGE-INTEGRITY-SHA256 -pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); // PASSWORD-ALGORITHM -pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); // USER-HASH -pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); // PASSWORD-ALGORITHMS -pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAIN - -/// RawAttribute is a Type-Length-Value (TLV) object that -/// can be added to a STUN message. Attributes are divided into two -/// types: comprehension-required and comprehension-optional. STUN -/// agents can safely ignore comprehension-optional attributes they -/// don't understand, but cannot successfully process a message if it -/// contains comprehension-required attributes that are not -/// understood. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RawAttribute { - pub typ: AttrType, - pub length: u16, // ignored while encoding - pub value: Vec, -} - -impl fmt::Display for RawAttribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {:?}", self.typ, self.value) - } -} - -impl Setter for RawAttribute { - /// add_to implements Setter, adding attribute as a.Type with a.Value and ignoring - /// the Length field. - fn add_to(&self, m: &mut Message) -> Result<()> { - m.add(self.typ, &self.value); - Ok(()) - } -} - -pub(crate) const PADDING: usize = 4; - -/// STUN aligns attributes on 32-bit boundaries, attributes whose content -/// is not a multiple of 4 bytes are padded with 1, 2, or 3 bytes of -/// padding so that its value contains a multiple of 4 bytes. The -/// padding bits are ignored, and may be any value. -/// -/// https://tools.ietf.org/html/rfc5389#section-15 -pub(crate) fn nearest_padded_value_length(l: usize) -> usize { - let mut n = PADDING * (l / PADDING); - if n < l { - n += PADDING - } - n -} - -/// This method converts uint16 vlue to AttrType. If it finds an old attribute -/// type value, it also translates it to the new value to enable backward -/// compatibility. (See: https://github.com/pion/stun/issues/21) -pub(crate) fn compat_attr_type(val: u16) -> AttrType { - if val == 0x8020 { - // draft-ietf-behave-rfc3489bis-02, MS-TURN - ATTR_XORMAPPED_ADDRESS // new: 0x0020 (from draft-ietf-behave-rfc3489bis-03 on) - } else { - AttrType(val) - } -} diff --git a/stun/src/attributes/attributes_test.rs b/stun/src/attributes/attributes_test.rs deleted file mode 100644 index 3be540f06..000000000 --- a/stun/src/attributes/attributes_test.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::*; -use crate::textattrs::TextAttribute; - -#[test] -fn test_raw_attribute_add_to() -> Result<()> { - let v = vec![1, 2, 3, 4]; - let mut m = Message::new(); - let ra = Box::new(RawAttribute { - typ: ATTR_DATA, - value: v.clone(), - ..Default::default() - }); - m.build(&[ra])?; - let got_v = m.get(ATTR_DATA)?; - assert_eq!(got_v, v, "value mismatch"); - - Ok(()) -} - -#[test] -fn test_message_get_no_allocs() -> Result<()> { - let mut m = Message::new(); - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "c".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - //"Default" - { - m.get(ATTR_SOFTWARE)?; - } - //"Not found" - { - let result = m.get(ATTR_ORIGIN); - assert!(result.is_err(), "should error"); - } - - Ok(()) -} - -#[test] -fn test_padding() -> Result<()> { - let tt = vec![ - (4, 4), // 0 - (2, 4), // 1 - (5, 8), // 2 - (8, 8), // 3 - (11, 12), // 4 - (1, 4), // 5 - (3, 4), // 6 - (6, 8), // 7 - (7, 8), // 8 - (0, 0), // 9 - (40, 40), // 10 - ]; - - for (i, o) in tt { - let got = nearest_padded_value_length(i); - assert_eq!(got, o, "padded({i}) {got} (got) != {o} (expected)",); - } - - Ok(()) -} - -#[test] -fn test_attr_type_range() -> Result<()> { - let tests = vec![ - ATTR_PRIORITY, - ATTR_ERROR_CODE, - ATTR_USE_CANDIDATE, - ATTR_EVEN_PORT, - ATTR_REQUESTED_ADDRESS_FAMILY, - ]; - for a in tests { - assert!(!a.optional() && a.required(), "should be required"); - } - - let tests = vec![ATTR_SOFTWARE, ATTR_ICE_CONTROLLED, ATTR_ORIGIN]; - for a in tests { - assert!(!a.required() && a.optional(), "should be optional"); - } - - Ok(()) -} diff --git a/stun/src/checks.rs b/stun/src/checks.rs deleted file mode 100644 index 9f4c134be..000000000 --- a/stun/src/checks.rs +++ /dev/null @@ -1,48 +0,0 @@ -use subtle::ConstantTimeEq; - -use crate::attributes::*; -use crate::error::*; - -// check_size returns ErrAttrSizeInvalid if got is not equal to expected. -pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { - if got == expected { - Ok(()) - } else { - Err(Error::ErrAttributeSizeInvalid) - } -} - -// is_attr_size_invalid returns true if error means that attribute size is invalid. -pub fn is_attr_size_invalid(err: &Error) -> bool { - Error::ErrAttributeSizeInvalid == *err -} - -pub(crate) fn check_hmac(got: &[u8], expected: &[u8]) -> Result<()> { - if got.ct_eq(expected).unwrap_u8() != 1 { - Err(Error::ErrIntegrityMismatch) - } else { - Ok(()) - } -} - -pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { - if got == expected { - Ok(()) - } else { - Err(Error::ErrFingerprintMismatch) - } -} - -// check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. -pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { - if got <= max { - Ok(()) - } else { - Err(Error::ErrAttributeSizeOverflow) - } -} - -// is_attr_size_overflow returns true if error means that attribute size is too big. -pub fn is_attr_size_overflow(err: &Error) -> bool { - Error::ErrAttributeSizeOverflow == *err -} diff --git a/stun/src/client.rs b/stun/src/client.rs deleted file mode 100644 index 13d8bd627..000000000 --- a/stun/src/client.rs +++ /dev/null @@ -1,473 +0,0 @@ -#[cfg(test)] -mod client_test; - -use std::collections::HashMap; -use std::io::BufReader; -use std::marker::{Send, Sync}; -use std::ops::Add; -use std::sync::Arc; - -use tokio::sync::mpsc; -use tokio::time::{self, Duration, Instant}; -use util::Conn; - -use crate::agent::*; -use crate::error::*; -use crate::message::*; - -const DEFAULT_TIMEOUT_RATE: Duration = Duration::from_millis(5); -const DEFAULT_RTO: Duration = Duration::from_millis(300); -const DEFAULT_MAX_ATTEMPTS: u32 = 7; -const DEFAULT_MAX_BUFFER_SIZE: usize = 8; - -/// Collector calls function f with constant rate. -/// -/// The simple Collector is ticker which calls function on each tick. -pub trait Collector { - fn start( - &mut self, - rate: Duration, - client_agent_tx: Arc>, - ) -> Result<()>; - fn close(&mut self) -> Result<()>; -} - -#[derive(Default)] -struct TickerCollector { - close_tx: Option>, -} - -impl Collector for TickerCollector { - fn start( - &mut self, - rate: Duration, - client_agent_tx: Arc>, - ) -> Result<()> { - let (close_tx, mut close_rx) = mpsc::channel(1); - self.close_tx = Some(close_tx); - - tokio::spawn(async move { - let mut interval = time::interval(rate); - - loop { - tokio::select! { - _ = close_rx.recv() => break, - _ = interval.tick() => { - if client_agent_tx.send(ClientAgent::Collect(Instant::now())).await.is_err() { - break; - } - } - } - } - }); - - Ok(()) - } - - fn close(&mut self) -> Result<()> { - if self.close_tx.is_none() { - return Err(Error::ErrCollectorClosed); - } - self.close_tx.take(); - Ok(()) - } -} - -/// ClientTransaction represents transaction in progress. -/// If transaction is succeed or failed, f will be called -/// provided by event. -/// Concurrent access is invalid. -#[derive(Debug, Clone)] -pub struct ClientTransaction { - id: TransactionId, - attempt: u32, - calls: u32, - handler: Handler, - start: Instant, - rto: Duration, - raw: Vec, -} - -impl ClientTransaction { - pub(crate) fn handle(&mut self, e: Event) -> Result<()> { - self.calls += 1; - if self.calls == 1 { - if let Some(handler) = &self.handler { - handler.send(e)?; - } - } - Ok(()) - } - - pub(crate) fn next_timeout(&self, now: Instant) -> Instant { - now.add((self.attempt + 1) * self.rto) - } -} - -struct ClientSettings { - buffer_size: usize, - rto: Duration, - rto_rate: Duration, - max_attempts: u32, - closed: bool, - //handler: Handler, - collector: Option>, - c: Option>, -} - -impl Default for ClientSettings { - fn default() -> Self { - ClientSettings { - buffer_size: DEFAULT_MAX_BUFFER_SIZE, - rto: DEFAULT_RTO, - rto_rate: DEFAULT_TIMEOUT_RATE, - max_attempts: DEFAULT_MAX_ATTEMPTS, - closed: false, - //handler: None, - collector: None, - c: None, - } - } -} - -#[derive(Default)] -pub struct ClientBuilder { - settings: ClientSettings, -} - -impl ClientBuilder { - // WithHandler sets client handler which is called if Agent emits the Event - // with TransactionID that is not currently registered by Client. - // Useful for handling Data indications from TURN server. - //pub fn with_handler(mut self, handler: Handler) -> Self { - // self.settings.handler = handler; - // self - //} - - /// with_rto sets client RTO as defined in STUN RFC. - pub fn with_rto(mut self, rto: Duration) -> Self { - self.settings.rto = rto; - self - } - - /// with_timeout_rate sets RTO timer minimum resolution. - pub fn with_timeout_rate(mut self, d: Duration) -> Self { - self.settings.rto_rate = d; - self - } - - /// with_buffer_size sets buffer size. - pub fn with_buffer_size(mut self, buffer_size: usize) -> Self { - self.settings.buffer_size = buffer_size; - self - } - - /// with_collector rests client timeout collector, the implementation - /// of ticker which calls function on each tick. - pub fn with_collector(mut self, coll: Box) -> Self { - self.settings.collector = Some(coll); - self - } - - /// with_conn sets transport connection - pub fn with_conn(mut self, conn: Arc) -> Self { - self.settings.c = Some(conn); - self - } - - /// with_no_retransmit disables retransmissions and sets RTO to - /// DEFAULT_MAX_ATTEMPTS * DEFAULT_RTO which will be effectively time out - /// if not set. - /// Useful for TCP connections where transport handles RTO. - pub fn with_no_retransmit(mut self) -> Self { - self.settings.max_attempts = 0; - if self.settings.rto == Duration::from_secs(0) { - self.settings.rto = DEFAULT_MAX_ATTEMPTS * DEFAULT_RTO; - } - self - } - - pub fn new() -> Self { - ClientBuilder { - settings: ClientSettings::default(), - } - } - - pub fn build(self) -> Result { - if self.settings.c.is_none() { - return Err(Error::ErrNoConnection); - } - - let client = Client { - settings: self.settings, - ..Default::default() - } - .run()?; - - Ok(client) - } -} - -/// Client simulates "connection" to STUN server. -#[derive(Default)] -pub struct Client { - settings: ClientSettings, - close_tx: Option>, - client_agent_tx: Option>>, - handler_tx: Option>>, -} - -impl Client { - async fn read_until_closed( - mut close_rx: mpsc::Receiver<()>, - c: Arc, - client_agent_tx: Arc>, - ) { - let mut msg = Message::new(); - let mut buf = vec![0; 1024]; - - loop { - tokio::select! { - _ = close_rx.recv() => return, - res = c.recv(&mut buf) => { - if let Ok(n) = res { - let mut reader = BufReader::new(&buf[..n]); - let result = msg.read_from(&mut reader); - if result.is_err() { - continue; - } - - if client_agent_tx.send(ClientAgent::Process(msg.clone())).await.is_err(){ - return; - } - } - } - } - } - } - - fn insert(&mut self, ct: ClientTransaction) -> Result<()> { - if self.settings.closed { - return Err(Error::ErrClientClosed); - } - - if let Some(handler_tx) = &mut self.handler_tx { - handler_tx.send(Event { - event_type: EventType::Insert(ct), - ..Default::default() - })?; - } - - Ok(()) - } - - fn remove(&mut self, id: TransactionId) -> Result<()> { - if self.settings.closed { - return Err(Error::ErrClientClosed); - } - - if let Some(handler_tx) = &mut self.handler_tx { - handler_tx.send(Event { - event_type: EventType::Remove(id), - ..Default::default() - })?; - } - - Ok(()) - } - - fn start( - conn: Option>, - mut handler_rx: mpsc::UnboundedReceiver, - client_agent_tx: Arc>, - mut t: HashMap, - max_attempts: u32, - ) { - tokio::spawn(async move { - while let Some(event) = handler_rx.recv().await { - match event.event_type { - EventType::Close => { - break; - } - EventType::Insert(ct) => { - if t.contains_key(&ct.id) { - continue; - } - t.insert(ct.id, ct); - } - EventType::Remove(id) => { - t.remove(&id); - } - EventType::Callback(id) => { - let mut ct = if t.contains_key(&id) { - t.remove(&id).unwrap() - } else { - /*if c.handler != nil && !errors.Is(e.Error, ErrTransactionStopped) { - c.handler(e) - }*/ - continue; - }; - - if ct.attempt >= max_attempts || event.event_body.is_ok() { - if let Some(handler) = ct.handler { - let _ = handler.send(event); - } - continue; - } - - // Doing re-transmission. - ct.attempt += 1; - - let raw = ct.raw.clone(); - let timeout = ct.next_timeout(Instant::now()); - let id = ct.id; - - // Starting client transaction. - t.insert(ct.id, ct); - - // Starting agent transaction. - if client_agent_tx - .send(ClientAgent::Start(id, timeout)) - .await - .is_err() - { - let ct = t.remove(&id).unwrap(); - if let Some(handler) = ct.handler { - let _ = handler.send(event); - } - continue; - } - - // Writing message to connection again. - if let Some(c) = &conn { - if c.send(&raw).await.is_err() { - let _ = client_agent_tx.send(ClientAgent::Stop(id)).await; - - let ct = t.remove(&id).unwrap(); - if let Some(handler) = ct.handler { - let _ = handler.send(event); - } - continue; - } - } - } - }; - } - }); - } - - /// close stops internal connection and agent, returning CloseErr on error. - pub async fn close(&mut self) -> Result<()> { - if self.settings.closed { - return Err(Error::ErrClientClosed); - } - - self.settings.closed = true; - - if let Some(collector) = &mut self.settings.collector { - let _ = collector.close(); - } - self.settings.collector.take(); - - self.close_tx.take(); //drop close channel - if let Some(client_agent_tx) = &mut self.client_agent_tx { - let _ = client_agent_tx.send(ClientAgent::Close).await; - } - self.client_agent_tx.take(); - - if let Some(c) = self.settings.c.take() { - c.close().await?; - } - - Ok(()) - } - - fn run(mut self) -> Result { - let (close_tx, close_rx) = mpsc::channel(1); - let (client_agent_tx, client_agent_rx) = mpsc::channel(self.settings.buffer_size); - let (handler_tx, handler_rx) = mpsc::unbounded_channel(); - let t: HashMap = HashMap::new(); - - let client_agent_tx = Arc::new(client_agent_tx); - let handler_tx = Arc::new(handler_tx); - self.client_agent_tx = Some(Arc::clone(&client_agent_tx)); - self.handler_tx = Some(Arc::clone(&handler_tx)); - self.close_tx = Some(close_tx); - - let conn = if let Some(conn) = &self.settings.c { - Arc::clone(conn) - } else { - return Err(Error::ErrNoConnection); - }; - - Client::start( - self.settings.c.clone(), - handler_rx, - Arc::clone(&client_agent_tx), - t, - self.settings.max_attempts, - ); - - let agent = Agent::new(Some(handler_tx)); - tokio::spawn(async move { Agent::run(agent, client_agent_rx).await }); - - if self.settings.collector.is_none() { - self.settings.collector = Some(Box::::default()); - } - if let Some(collector) = &mut self.settings.collector { - collector.start(self.settings.rto_rate, Arc::clone(&client_agent_tx))?; - } - - let conn_rx = Arc::clone(&conn); - tokio::spawn( - async move { Client::read_until_closed(close_rx, conn_rx, client_agent_tx).await }, - ); - - Ok(self) - } - - pub async fn send(&mut self, m: &Message, handler: Handler) -> Result<()> { - if self.settings.closed { - return Err(Error::ErrClientClosed); - } - - let has_handler = handler.is_some(); - - if handler.is_some() { - let t = ClientTransaction { - id: m.transaction_id, - attempt: 0, - calls: 0, - handler, - start: Instant::now(), - rto: self.settings.rto, - raw: m.raw.clone(), - }; - let d = t.next_timeout(t.start); - self.insert(t)?; - - if let Some(client_agent_tx) = &mut self.client_agent_tx { - client_agent_tx - .send(ClientAgent::Start(m.transaction_id, d)) - .await?; - } - } - - if let Some(c) = &self.settings.c { - let result = c.send(&m.raw).await; - if result.is_err() && has_handler { - self.remove(m.transaction_id)?; - - if let Some(client_agent_tx) = &mut self.client_agent_tx { - client_agent_tx - .send(ClientAgent::Stop(m.transaction_id)) - .await?; - } - } else if let Err(err) = result { - return Err(Error::Other(err.to_string())); - } - } - - Ok(()) - } -} diff --git a/stun/src/client/client_test.rs b/stun/src/client/client_test.rs deleted file mode 100644 index c7bad84ef..000000000 --- a/stun/src/client/client_test.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; - -#[test] -fn ensure_client_settings_is_send() { - let client = ClientSettings::default(); - - ensure_send(client); -} - -fn ensure_send(_: T) {} - -//TODO: add more client tests diff --git a/stun/src/error.rs b/stun/src/error.rs deleted file mode 100644 index 083b3adb0..000000000 --- a/stun/src/error.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::io; -use std::string::FromUtf8Error; - -use thiserror::Error; -use tokio::sync::mpsc::error::SendError as MpscSendError; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("attribute not found")] - ErrAttributeNotFound, - #[error("transaction is stopped")] - ErrTransactionStopped, - #[error("transaction not exists")] - ErrTransactionNotExists, - #[error("transaction exists with same id")] - ErrTransactionExists, - #[error("agent is closed")] - ErrAgentClosed, - #[error("transaction is timed out")] - ErrTransactionTimeOut, - #[error("no default reason for ErrorCode")] - ErrNoDefaultReason, - #[error("unexpected EOF")] - ErrUnexpectedEof, - #[error("attribute size is invalid")] - ErrAttributeSizeInvalid, - #[error("attribute size overflow")] - ErrAttributeSizeOverflow, - #[error("attempt to decode to nil message")] - ErrDecodeToNil, - #[error("unexpected EOF: not enough bytes to read header")] - ErrUnexpectedHeaderEof, - #[error("integrity check failed")] - ErrIntegrityMismatch, - #[error("fingerprint check failed")] - ErrFingerprintMismatch, - #[error("FINGERPRINT before MESSAGE-INTEGRITY attribute")] - ErrFingerprintBeforeIntegrity, - #[error("bad UNKNOWN-ATTRIBUTES size")] - ErrBadUnknownAttrsSize, - #[error("invalid length of IP value")] - ErrBadIpLength, - #[error("no connection provided")] - ErrNoConnection, - #[error("client is closed")] - ErrClientClosed, - #[error("no agent is set")] - ErrNoAgent, - #[error("collector is closed")] - ErrCollectorClosed, - #[error("unsupported network")] - ErrUnsupportedNetwork, - #[error("invalid url")] - ErrInvalidUrl, - #[error("unknown scheme type")] - ErrSchemeType, - #[error("invalid hostname")] - ErrHost, - #[error("{0}")] - Other(String), - #[error("url parse: {0}")] - Url(#[from] url::ParseError), - #[error("utf8: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("{0}")] - Io(#[source] IoError), - #[error("mpsc send: {0}")] - MpscSend(String), - #[error("{0}")] - Util(#[from] util::Error), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -// Because Tokio SendError is parameterized, we sadly lose the backtrace. -impl From> for Error { - fn from(e: MpscSendError) -> Self { - Error::MpscSend(e.to_string()) - } -} diff --git a/stun/src/error_code.rs b/stun/src/error_code.rs deleted file mode 100644 index 8142a54cc..000000000 --- a/stun/src/error_code.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::collections::HashMap; -use std::fmt; - -use crate::attributes::*; -use crate::checks::*; -use crate::error::*; -use crate::message::*; - -// ErrorCodeAttribute represents ERROR-CODE attribute. -// -// RFC 5389 Section 15.6 -#[derive(Default)] -pub struct ErrorCodeAttribute { - pub code: ErrorCode, - pub reason: Vec, -} - -impl fmt::Display for ErrorCodeAttribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let reason = match String::from_utf8(self.reason.clone()) { - Ok(reason) => reason, - Err(_) => return Err(fmt::Error {}), - }; - - write!(f, "{}: {}", self.code.0, reason) - } -} - -// constants for ERROR-CODE encoding. -const ERROR_CODE_CLASS_BYTE: usize = 2; -const ERROR_CODE_NUMBER_BYTE: usize = 3; -const ERROR_CODE_REASON_START: usize = 4; -const ERROR_CODE_REASON_MAX_B: usize = 763; -const ERROR_CODE_MODULO: u16 = 100; - -impl Setter for ErrorCodeAttribute { - // add_to adds ERROR-CODE to m. - fn add_to(&self, m: &mut Message) -> Result<()> { - check_overflow( - ATTR_ERROR_CODE, - self.reason.len() + ERROR_CODE_REASON_START, - ERROR_CODE_REASON_MAX_B + ERROR_CODE_REASON_START, - )?; - - let mut value: Vec = Vec::with_capacity(ERROR_CODE_REASON_MAX_B); - - let number = (self.code.0 % ERROR_CODE_MODULO) as u8; // error code modulo 100 - let class = (self.code.0 / ERROR_CODE_MODULO) as u8; // hundred digit - value.extend_from_slice(&[0, 0]); - value.push(class); // [ERROR_CODE_CLASS_BYTE] - value.push(number); //[ERROR_CODE_NUMBER_BYTE] = - value.extend_from_slice(&self.reason); //[ERROR_CODE_REASON_START:] - - m.add(ATTR_ERROR_CODE, &value); - - Ok(()) - } -} - -impl Getter for ErrorCodeAttribute { - // GetFrom decodes ERROR-CODE from m. Reason is valid until m.Raw is valid. - fn get_from(&mut self, m: &Message) -> Result<()> { - let v = m.get(ATTR_ERROR_CODE)?; - - if v.len() < ERROR_CODE_REASON_START { - return Err(Error::ErrUnexpectedEof); - } - - let class = v[ERROR_CODE_CLASS_BYTE] as u16; - let number = v[ERROR_CODE_NUMBER_BYTE] as u16; - let code = class * ERROR_CODE_MODULO + number; - self.code = ErrorCode(code); - self.reason = v[ERROR_CODE_REASON_START..].to_vec(); - - Ok(()) - } -} - -// ErrorCode is code for ERROR-CODE attribute. -#[derive(PartialEq, Eq, Hash, Copy, Clone, Default)] -pub struct ErrorCode(pub u16); - -impl Setter for ErrorCode { - // add_to adds ERROR-CODE with default reason to m. If there - // is no default reason, returns ErrNoDefaultReason. - fn add_to(&self, m: &mut Message) -> Result<()> { - if let Some(reason) = ERROR_REASONS.get(self) { - let a = ErrorCodeAttribute { - code: *self, - reason: reason.clone(), - }; - a.add_to(m) - } else { - Err(Error::ErrNoDefaultReason) - } - } -} - -// Possible error codes. -pub const CODE_TRY_ALTERNATE: ErrorCode = ErrorCode(300); -pub const CODE_BAD_REQUEST: ErrorCode = ErrorCode(400); -pub const CODE_UNAUTHORIZED: ErrorCode = ErrorCode(401); -pub const CODE_UNKNOWN_ATTRIBUTE: ErrorCode = ErrorCode(420); -pub const CODE_STALE_NONCE: ErrorCode = ErrorCode(438); -pub const CODE_ROLE_CONFLICT: ErrorCode = ErrorCode(487); -pub const CODE_SERVER_ERROR: ErrorCode = ErrorCode(500); - -// DEPRECATED constants. -// DEPRECATED, use CODE_UNAUTHORIZED. -pub const CODE_UNAUTHORISED: ErrorCode = CODE_UNAUTHORIZED; - -// Error codes from RFC 5766. -// -// RFC 5766 Section 15 -pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); // Forbidden -pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); // Allocation Mismatch -pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); // Wrong Credentials -pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); // Unsupported Transport Protocol -pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); // Allocation Quota Reached -pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); // Insufficient Capacity - -// Error codes from RFC 6062. -// -// RFC 6062 Section 6.3 -pub const CODE_CONN_ALREADY_EXISTS: ErrorCode = ErrorCode(446); -pub const CODE_CONN_TIMEOUT_OR_FAILURE: ErrorCode = ErrorCode(447); - -// Error codes from RFC 6156. -// -// RFC 6156 Section 10.2 -pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); // Address Family not Supported -pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); // Peer Address Family Mismatch - -lazy_static! { - pub static ref ERROR_REASONS:HashMap> = - [ - (CODE_TRY_ALTERNATE, b"Try Alternate".to_vec()), - (CODE_BAD_REQUEST, b"Bad Request".to_vec()), - (CODE_UNAUTHORIZED, b"Unauthorized".to_vec()), - (CODE_UNKNOWN_ATTRIBUTE, b"Unknown Attribute".to_vec()), - (CODE_STALE_NONCE, b"Stale Nonce".to_vec()), - (CODE_SERVER_ERROR, b"Server Error".to_vec()), - (CODE_ROLE_CONFLICT, b"Role Conflict".to_vec()), - - // RFC 5766. - (CODE_FORBIDDEN, b"Forbidden".to_vec()), - (CODE_ALLOC_MISMATCH, b"Allocation Mismatch".to_vec()), - (CODE_WRONG_CREDENTIALS, b"Wrong Credentials".to_vec()), - (CODE_UNSUPPORTED_TRANS_PROTO, b"Unsupported Transport Protocol".to_vec()), - (CODE_ALLOC_QUOTA_REACHED, b"Allocation Quota Reached".to_vec()), - (CODE_INSUFFICIENT_CAPACITY, b"Insufficient Capacity".to_vec()), - - // RFC 6062. - (CODE_CONN_ALREADY_EXISTS, b"Connection Already Exists".to_vec()), - (CODE_CONN_TIMEOUT_OR_FAILURE, b"Connection Timeout or Failure".to_vec()), - - // RFC 6156. - (CODE_ADDR_FAMILY_NOT_SUPPORTED, b"Address Family not Supported".to_vec()), - (CODE_PEER_ADDR_FAMILY_MISMATCH, b"Peer Address Family Mismatch".to_vec()), - ].iter().cloned().collect(); - -} diff --git a/stun/src/fingerprint.rs b/stun/src/fingerprint.rs deleted file mode 100644 index 648c288ec..000000000 --- a/stun/src/fingerprint.rs +++ /dev/null @@ -1,64 +0,0 @@ -#[cfg(test)] -mod fingerprint_test; - -use crc::{Crc, CRC_32_ISO_HDLC}; - -use crate::attributes::ATTR_FINGERPRINT; -use crate::checks::*; -use crate::error::*; -use crate::message::*; - -// FingerprintAttr represents FINGERPRINT attribute. -// -// RFC 5389 Section 15.5 -pub struct FingerprintAttr; - -// FINGERPRINT is shorthand for FingerprintAttr. -// -// Example: -// -// m := New() -// FINGERPRINT.add_to(m) -pub const FINGERPRINT: FingerprintAttr = FingerprintAttr {}; - -pub const FINGERPRINT_XOR_VALUE: u32 = 0x5354554e; -pub const FINGERPRINT_SIZE: usize = 4; // 32 bit - -// FingerprintValue returns CRC-32 of b XOR-ed by 0x5354554e. -// -// The value of the attribute is computed as the CRC-32 of the STUN message -// up to (but excluding) the FINGERPRINT attribute itself, XOR'ed with -// the 32-bit value 0x5354554e (the XOR helps in cases where an -// application packet is also using CRC-32 in it). -pub fn fingerprint_value(b: &[u8]) -> u32 { - let checksum = Crc::::new(&CRC_32_ISO_HDLC).checksum(b); - checksum ^ FINGERPRINT_XOR_VALUE // XOR -} - -impl Setter for FingerprintAttr { - // add_to adds fingerprint to message. - fn add_to(&self, m: &mut Message) -> Result<()> { - let l = m.length; - // length in header should include size of fingerprint attribute - m.length += (FINGERPRINT_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; // increasing length - m.write_length(); // writing Length to Raw - let val = fingerprint_value(&m.raw); - let b = val.to_be_bytes(); - m.length = l; - m.add(ATTR_FINGERPRINT, &b); - Ok(()) - } -} - -impl FingerprintAttr { - // Check reads fingerprint value from m and checks it, returning error if any. - // Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. - pub fn check(&self, m: &Message) -> Result<()> { - let b = m.get(ATTR_FINGERPRINT)?; - check_size(ATTR_FINGERPRINT, b.len(), FINGERPRINT_SIZE)?; - let val = u32::from_be_bytes([b[0], b[1], b[2], b[3]]); - let attr_start = m.raw.len() - (FINGERPRINT_SIZE + ATTRIBUTE_HEADER_SIZE); - let expected = fingerprint_value(&m.raw[..attr_start]); - check_fingerprint(val, expected) - } -} diff --git a/stun/src/fingerprint/fingerprint_test.rs b/stun/src/fingerprint/fingerprint_test.rs deleted file mode 100644 index 1ac589d54..000000000 --- a/stun/src/fingerprint/fingerprint_test.rs +++ /dev/null @@ -1,73 +0,0 @@ -use super::*; -use crate::attributes::ATTR_SOFTWARE; -use crate::textattrs::TextAttribute; - -#[test] -fn fingerprint_uses_crc_32_iso_hdlc() -> Result<()> { - let mut m = Message::new(); - - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - FINGERPRINT.add_to(&mut m)?; - m.write_header(); - - assert_eq!(&m.raw[0..m.raw.len()-8], b"\x00\x00\x00\x14\x21\x12\xA4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x22\x00\x08\x73\x6F\x66\x74\x77\x61\x72\x65"); - - assert_eq!(m.raw[m.raw.len() - 4..], [0xe4, 0x4c, 0x33, 0xd9]); - - Ok(()) -} - -#[test] -fn test_fingerprint_check() -> Result<()> { - let mut m = Message::new(); - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - FINGERPRINT.add_to(&mut m)?; - m.write_header(); - FINGERPRINT.check(&m)?; - m.raw[3] += 1; - - let result = FINGERPRINT.check(&m); - assert!(result.is_err(), "should error"); - - Ok(()) -} - -#[test] -fn test_fingerprint_check_bad() -> Result<()> { - let mut m = Message::new(); - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - let result = FINGERPRINT.check(&m); - assert!(result.is_err(), "should error"); - - m.add(ATTR_FINGERPRINT, &[1, 2, 3]); - - let result = FINGERPRINT.check(&m); - if let Err(err) = result { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("Expected error, but got ok"); - } - - Ok(()) -} diff --git a/stun/src/integrity.rs b/stun/src/integrity.rs deleted file mode 100644 index cd692da18..000000000 --- a/stun/src/integrity.rs +++ /dev/null @@ -1,118 +0,0 @@ -#[cfg(test)] -mod integrity_test; - -use std::fmt; - -use md5::{Digest, Md5}; -use ring::hmac; - -use crate::attributes::*; -use crate::checks::*; -use crate::error::*; -use crate::message::*; - -// separator for credentials. -pub(crate) const CREDENTIALS_SEP: &str = ":"; - -// MessageIntegrity represents MESSAGE-INTEGRITY attribute. -// -// add_to and Check methods are using zero-allocation version of hmac, see -// newHMAC function and internal/hmac/pool.go. -// -// RFC 5389 Section 15.4 -#[derive(Default, Clone)] -pub struct MessageIntegrity(pub Vec); - -fn new_hmac(key: &[u8], message: &[u8]) -> Vec { - let mac = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key); - hmac::sign(&mac, message).as_ref().to_vec() -} - -impl fmt::Display for MessageIntegrity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "KEY: 0x{:x?}", self.0) - } -} - -impl Setter for MessageIntegrity { - // add_to adds MESSAGE-INTEGRITY attribute to message. - // - // CPU costly, see BenchmarkMessageIntegrity_AddTo. - fn add_to(&self, m: &mut Message) -> Result<()> { - for a in &m.attributes.0 { - // Message should not contain FINGERPRINT attribute - // before MESSAGE-INTEGRITY. - if a.typ == ATTR_FINGERPRINT { - return Err(Error::ErrFingerprintBeforeIntegrity); - } - } - // The text used as input to HMAC is the STUN message, - // including the header, up to and including the attribute preceding the - // MESSAGE-INTEGRITY attribute. - let length = m.length; - // Adjusting m.Length to contain MESSAGE-INTEGRITY TLV. - m.length += (MESSAGE_INTEGRITY_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; - m.write_length(); // writing length to m.Raw - let v = new_hmac(&self.0, &m.raw); // calculating HMAC for adjusted m.Raw - m.length = length; // changing m.Length back - - m.add(ATTR_MESSAGE_INTEGRITY, &v); - - Ok(()) - } -} - -pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; - -impl MessageIntegrity { - // new_long_term_integrity returns new MessageIntegrity with key for long-term - // credentials. Password, username, and realm must be SASL-prepared. - pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { - let s = [username, realm, password].join(CREDENTIALS_SEP); - - let mut h = Md5::new(); - h.update(s.as_bytes()); - - MessageIntegrity(h.finalize().as_slice().to_vec()) - } - - // new_short_term_integrity returns new MessageIntegrity with key for short-term - // credentials. Password must be SASL-prepared. - pub fn new_short_term_integrity(password: String) -> Self { - MessageIntegrity(password.as_bytes().to_vec()) - } - - // Check checks MESSAGE-INTEGRITY attribute. - // - // CPU costly, see BenchmarkMessageIntegrity_Check. - pub fn check(&self, m: &mut Message) -> Result<()> { - let v = m.get(ATTR_MESSAGE_INTEGRITY)?; - - // Adjusting length in header to match m.Raw that was - // used when computing HMAC. - - let length = m.length as usize; - let mut after_integrity = false; - let mut size_reduced = 0; - - for a in &m.attributes.0 { - if after_integrity { - size_reduced += nearest_padded_value_length(a.length as usize); - size_reduced += ATTRIBUTE_HEADER_SIZE; - } - if a.typ == ATTR_MESSAGE_INTEGRITY { - after_integrity = true; - } - } - m.length -= size_reduced as u32; - m.write_length(); - // start_of_hmac should be first byte of integrity attribute. - let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize - - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); - let b = &m.raw[..start_of_hmac]; // data before integrity attribute - let expected = new_hmac(&self.0, b); - m.length = length as u32; - m.write_length(); // writing length back - check_hmac(&v, &expected) - } -} diff --git a/stun/src/integrity/integrity_test.rs b/stun/src/integrity/integrity_test.rs deleted file mode 100644 index e085cfa26..000000000 --- a/stun/src/integrity/integrity_test.rs +++ /dev/null @@ -1,93 +0,0 @@ -use super::*; -use crate::agent::TransactionId; -use crate::attributes::ATTR_SOFTWARE; -use crate::fingerprint::FINGERPRINT; -use crate::textattrs::TextAttribute; - -#[test] -fn test_message_integrity_add_to_simple() -> Result<()> { - let i = MessageIntegrity::new_long_term_integrity( - "user".to_owned(), - "realm".to_owned(), - "pass".to_owned(), - ); - let expected = vec![ - 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, - 0xeb, - ]; - assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); - - //"Check" - { - let mut m = Message::new(); - m.write_header(); - i.add_to(&mut m)?; - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - let mut d_m = Message::new(); - d_m.raw.clone_from(&m.raw); - d_m.decode()?; - i.check(&mut d_m)?; - - d_m.raw[24] += 12; // HMAC now invalid - d_m.decode()?; - let result = i.check(&mut d_m); - assert!(result.is_err(), "should be invalid"); - } - - Ok(()) -} - -#[test] -fn test_message_integrity_with_fingerprint() -> Result<()> { - let mut m = Message::new(); - m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]); - m.write_header(); - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - - let i = MessageIntegrity::new_short_term_integrity("pwd".to_owned()); - assert_eq!(i.to_string(), "KEY: 0x[70, 77, 64]", "bad string {i}"); - let result = i.check(&mut m); - assert!(result.is_err(), "should error"); - - i.add_to(&mut m)?; - FINGERPRINT.add_to(&mut m)?; - i.check(&mut m)?; - m.raw[24] = 33; - m.decode()?; - let result = i.check(&mut m); - assert!(result.is_err(), "mismatch expected"); - - Ok(()) -} - -#[test] -fn test_message_integrity() -> Result<()> { - let mut m = Message::new(); - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); - m.write_header(); - i.add_to(&mut m)?; - m.get(ATTR_MESSAGE_INTEGRITY)?; - Ok(()) -} - -#[test] -fn test_message_integrity_before_fingerprint() -> Result<()> { - let mut m = Message::new(); - m.write_header(); - FINGERPRINT.add_to(&mut m)?; - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); - let result = i.add_to(&mut m); - assert!(result.is_err(), "should error"); - - Ok(()) -} diff --git a/stun/src/lib.rs b/stun/src/lib.rs deleted file mode 100644 index f13ff34d4..000000000 --- a/stun/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -#[macro_use] -extern crate lazy_static; - -pub mod addr; -pub mod agent; -pub mod attributes; -pub mod checks; -pub mod client; -mod error; -pub mod error_code; -pub mod fingerprint; -pub mod integrity; -pub mod message; -pub mod textattrs; -pub mod uattrs; -pub mod uri; -pub mod xoraddr; - -// IANA assigned ports for "stun" protocol. -pub const DEFAULT_PORT: u16 = 3478; -pub const DEFAULT_TLS_PORT: u16 = 5349; - -pub use error::Error; diff --git a/stun/src/message.rs b/stun/src/message.rs deleted file mode 100644 index 6ac245e08..000000000 --- a/stun/src/message.rs +++ /dev/null @@ -1,626 +0,0 @@ -#[cfg(test)] -mod message_test; - -use std::fmt; -use std::io::{Read, Write}; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use rand::Rng; - -use crate::agent::*; -use crate::attributes::*; -use crate::error::*; - -// MAGIC_COOKIE is fixed value that aids in distinguishing STUN packets -// from packets of other protocols when STUN is multiplexed with those -// other protocols on the same Port. -// -// The magic cookie field MUST contain the fixed value 0x2112A442 in -// network byte order. -// -// Defined in "STUN Message Structure", section 6. -pub const MAGIC_COOKIE: u32 = 0x2112A442; -pub const ATTRIBUTE_HEADER_SIZE: usize = 4; -pub const MESSAGE_HEADER_SIZE: usize = 20; - -// TRANSACTION_ID_SIZE is length of transaction id array (in bytes). -pub const TRANSACTION_ID_SIZE: usize = 12; // 96 bit - -// Interfaces that are implemented by message attributes, shorthands for them, -// or helpers for message fields as type or transaction id. -pub trait Setter { - // Setter sets *Message attribute. - fn add_to(&self, m: &mut Message) -> Result<()>; -} - -// Getter parses attribute from *Message. -pub trait Getter { - fn get_from(&mut self, m: &Message) -> Result<()>; -} - -// Checker checks *Message attribute. -pub trait Checker { - fn check(&self, m: &Message) -> Result<()>; -} - -// is_message returns true if b looks like STUN message. -// Useful for multiplexing. is_message does not guarantee -// that decoding will be successful. -pub fn is_message(b: &[u8]) -> bool { - b.len() >= MESSAGE_HEADER_SIZE && u32::from_be_bytes([b[4], b[5], b[6], b[7]]) == MAGIC_COOKIE -} -// Message represents a single STUN packet. It uses aggressive internal -// buffering to enable zero-allocation encoding and decoding, -// so there are some usage constraints: -// -// Message, its fields, results of m.Get or any attribute a.GetFrom -// are valid only until Message.Raw is not modified. -#[derive(Default, Debug, Clone)] -pub struct Message { - pub typ: MessageType, - pub length: u32, // len(Raw) not including header - pub transaction_id: TransactionId, - pub attributes: Attributes, - pub raw: Vec, -} - -impl fmt::Display for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let t_id = BASE64_STANDARD.encode(self.transaction_id.0); - write!( - f, - "{} l={} attrs={} id={}", - self.typ, - self.length, - self.attributes.0.len(), - t_id - ) - } -} - -// Equal returns true if Message b equals to m. -// Ignores m.Raw. -impl PartialEq for Message { - fn eq(&self, other: &Self) -> bool { - if self.typ != other.typ { - return false; - } - if self.transaction_id != other.transaction_id { - return false; - } - if self.length != other.length { - return false; - } - if self.attributes != other.attributes { - return false; - } - true - } -} - -const DEFAULT_RAW_CAPACITY: usize = 120; - -impl Setter for Message { - // add_to sets b.TransactionID to m.TransactionID. - // - // Implements Setter to aid in crafting responses. - fn add_to(&self, b: &mut Message) -> Result<()> { - b.transaction_id = self.transaction_id; - b.write_transaction_id(); - Ok(()) - } -} - -impl Message { - // New returns *Message with pre-allocated Raw. - pub fn new() -> Self { - Message { - raw: { - let mut raw = Vec::with_capacity(DEFAULT_RAW_CAPACITY); - raw.extend_from_slice(&[0; MESSAGE_HEADER_SIZE]); - raw - }, - ..Default::default() - } - } - - // marshal_binary implements the encoding.BinaryMarshaler interface. - pub fn marshal_binary(&self) -> Result> { - // We can't return m.Raw, allocation is expected by implicit interface - // contract induced by other implementations. - Ok(self.raw.clone()) - } - - // unmarshal_binary implements the encoding.BinaryUnmarshaler interface. - pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { - // We can't retain data, copy is expected by interface contract. - self.raw.clear(); - self.raw.extend_from_slice(data); - self.decode() - } - - // NewTransactionID sets m.TransactionID to random value from crypto/rand - // and returns error if any. - pub fn new_transaction_id(&mut self) -> Result<()> { - rand::thread_rng().fill(&mut self.transaction_id.0); - self.write_transaction_id(); - Ok(()) - } - - // Reset resets Message, attributes and underlying buffer length. - pub fn reset(&mut self) { - self.raw.clear(); - self.length = 0; - self.attributes.0.clear(); - } - - // grow ensures that internal buffer has n length. - fn grow(&mut self, n: usize, resize: bool) { - if self.raw.len() >= n { - if resize { - self.raw.resize(n, 0); - } - return; - } - self.raw.extend_from_slice(&vec![0; n - self.raw.len()]); - } - - // Add appends new attribute to message. Not goroutine-safe. - // - // Value of attribute is copied to internal buffer so - // it is safe to reuse v. - pub fn add(&mut self, t: AttrType, v: &[u8]) { - // Allocating buffer for TLV (type-length-value). - // T = t, L = len(v), V = v. - // m.Raw will look like: - // [0:20] <- message header - // [20:20+m.Length] <- existing message attributes - // [20+m.Length:20+m.Length+len(v) + 4] <- allocated buffer for new TLV - // [first:last] <- same as previous - // [0 1|2 3|4 4 + len(v)] <- mapping for allocated buffer - // T L V - let alloc_size = ATTRIBUTE_HEADER_SIZE + v.len(); // ~ len(TLV) = len(TL) + len(V) - let first = MESSAGE_HEADER_SIZE + self.length as usize; // first byte number - let mut last = first + alloc_size; // last byte number - self.grow(last, true); // growing cap(Raw) to fit TLV - self.length += alloc_size as u32; // rendering length change - - // Encoding attribute TLV to allocated buffer. - let buf = &mut self.raw[first..last]; - buf[0..2].copy_from_slice(&t.value().to_be_bytes()); // T - buf[2..4].copy_from_slice(&(v.len() as u16).to_be_bytes()); // L - - let value = &mut buf[ATTRIBUTE_HEADER_SIZE..]; - value.copy_from_slice(v); // V - - let attr = RawAttribute { - typ: t, // T - length: v.len() as u16, // L - value: value.to_vec(), // V - }; - - // Checking that attribute value needs padding. - if attr.length as usize % PADDING != 0 { - // Performing padding. - let bytes_to_add = nearest_padded_value_length(v.len()) - v.len(); - last += bytes_to_add; - self.grow(last, true); - // setting all padding bytes to zero - // to prevent data leak from previous - // data in next bytes_to_add bytes - let buf = &mut self.raw[last - bytes_to_add..last]; - for b in buf { - *b = 0; - } - self.length += bytes_to_add as u32; // rendering length change - } - self.attributes.0.push(attr); - self.write_length(); - } - - // WriteLength writes m.Length to m.Raw. - pub fn write_length(&mut self) { - self.grow(4, false); - self.raw[2..4].copy_from_slice(&(self.length as u16).to_be_bytes()); - } - - // WriteHeader writes header to underlying buffer. Not goroutine-safe. - pub fn write_header(&mut self) { - self.grow(MESSAGE_HEADER_SIZE, false); - - self.write_type(); - self.write_length(); - self.raw[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); // magic cookie - self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); - // transaction ID - } - - // WriteTransactionID writes m.TransactionID to m.Raw. - pub fn write_transaction_id(&mut self) { - self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); - // transaction ID - } - - // WriteAttributes encodes all m.Attributes to m. - pub fn write_attributes(&mut self) { - let attributes: Vec = self.attributes.0.drain(..).collect(); - for a in &attributes { - self.add(a.typ, &a.value); - } - self.attributes = Attributes(attributes); - } - - // WriteType writes m.Type to m.Raw. - pub fn write_type(&mut self) { - self.grow(2, false); - self.raw[..2].copy_from_slice(&self.typ.value().to_be_bytes()); // message type - } - - // SetType sets m.Type and writes it to m.Raw. - pub fn set_type(&mut self, t: MessageType) { - self.typ = t; - self.write_type(); - } - - // Encode re-encodes message into m.Raw. - pub fn encode(&mut self) { - self.raw.clear(); - self.write_header(); - self.length = 0; - self.write_attributes(); - } - - // Decode decodes m.Raw into m. - pub fn decode(&mut self) -> Result<()> { - // decoding message header - let buf = &self.raw; - if buf.len() < MESSAGE_HEADER_SIZE { - return Err(Error::ErrUnexpectedHeaderEof); - } - - let t = u16::from_be_bytes([buf[0], buf[1]]); // first 2 bytes - let size = u16::from_be_bytes([buf[2], buf[3]]) as usize; // second 2 bytes - let cookie = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); // last 4 bytes - let full_size = MESSAGE_HEADER_SIZE + size; // len(m.Raw) - - if cookie != MAGIC_COOKIE { - return Err(Error::Other(format!( - "{cookie:x} is invalid magic cookie (should be {MAGIC_COOKIE:x})" - ))); - } - if buf.len() < full_size { - return Err(Error::Other(format!( - "buffer length {} is less than {} (expected message size)", - buf.len(), - full_size - ))); - } - - // saving header data - self.typ.read_value(t); - self.length = size as u32; - self.transaction_id - .0 - .copy_from_slice(&buf[8..MESSAGE_HEADER_SIZE]); - - self.attributes.0.clear(); - let mut offset = 0; - let mut b = &buf[MESSAGE_HEADER_SIZE..full_size]; - - while offset < size { - // checking that we have enough bytes to read header - if b.len() < ATTRIBUTE_HEADER_SIZE { - return Err(Error::Other(format!( - "buffer length {} is less than {} (expected header size)", - b.len(), - ATTRIBUTE_HEADER_SIZE - ))); - } - - let mut a = RawAttribute { - typ: compat_attr_type(u16::from_be_bytes([b[0], b[1]])), // first 2 bytes - length: u16::from_be_bytes([b[2], b[3]]), // second 2 bytes - ..Default::default() - }; - let a_l = a.length as usize; // attribute length - let a_buff_l = nearest_padded_value_length(a_l); // expected buffer length (with padding) - - b = &b[ATTRIBUTE_HEADER_SIZE..]; // slicing again to simplify value read - offset += ATTRIBUTE_HEADER_SIZE; - if b.len() < a_buff_l { - // checking size - return Err(Error::Other(format!( - "buffer length {} is less than {} (expected value size for {})", - b.len(), - a_buff_l, - a.typ - ))); - } - a.value = b[..a_l].to_vec(); - offset += a_buff_l; - b = &b[a_buff_l..]; - - self.attributes.0.push(a); - } - - Ok(()) - } - - // WriteTo implements WriterTo via calling Write(m.Raw) on w and returning - // call result. - pub fn write_to(&self, writer: &mut W) -> Result { - let n = writer.write(&self.raw)?; - Ok(n) - } - - // ReadFrom implements ReaderFrom. Reads message from r into m.Raw, - // Decodes it and return error if any. If m.Raw is too small, will return - // ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. - // - // Can return *DecodeErr while decoding too. - pub fn read_from(&mut self, reader: &mut R) -> Result { - let mut t_buf = vec![0; DEFAULT_RAW_CAPACITY]; - let n = reader.read(&mut t_buf)?; - self.raw = t_buf[..n].to_vec(); - self.decode()?; - Ok(n) - } - - // Write decodes message and return error if any. - // - // Any error is unrecoverable, but message could be partially decoded. - pub fn write(&mut self, t_buf: &[u8]) -> Result { - self.raw.clear(); - self.raw.extend_from_slice(t_buf); - self.decode()?; - Ok(t_buf.len()) - } - - // CloneTo clones m to b securing any further m mutations. - pub fn clone_to(&self, b: &mut Message) -> Result<()> { - b.raw.clear(); - b.raw.extend_from_slice(&self.raw); - b.decode() - } - - // Contains return true if message contain t attribute. - pub fn contains(&self, t: AttrType) -> bool { - for a in &self.attributes.0 { - if a.typ == t { - return true; - } - } - false - } - - // get returns byte slice that represents attribute value, - // if there is no attribute with such type, - // ErrAttributeNotFound is returned. - pub fn get(&self, t: AttrType) -> Result> { - let (v, ok) = self.attributes.get(t); - if ok { - Ok(v.value) - } else { - Err(Error::ErrAttributeNotFound) - } - } - - // Build resets message and applies setters to it in batch, returning on - // first error. To prevent allocations, pass pointers to values. - // - // Example: - // var ( - // t = BindingRequest - // username = NewUsername("username") - // nonce = NewNonce("nonce") - // realm = NewRealm("example.org") - // ) - // m := new(Message) - // m.Build(t, username, nonce, realm) // 4 allocations - // m.Build(&t, &username, &nonce, &realm) // 0 allocations - // - // See BenchmarkBuildOverhead. - pub fn build(&mut self, setters: &[Box]) -> Result<()> { - self.reset(); - self.write_header(); - for s in setters { - s.add_to(self)?; - } - Ok(()) - } - - // Check applies checkers to message in batch, returning on first error. - pub fn check(&self, checkers: &[C]) -> Result<()> { - for c in checkers { - c.check(self)?; - } - Ok(()) - } - - // Parse applies getters to message in batch, returning on first error. - pub fn parse(&self, getters: &mut [G]) -> Result<()> { - for c in getters { - c.get_from(self)?; - } - Ok(()) - } -} - -// MessageClass is 8-bit representation of 2-bit class of STUN Message Class. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct MessageClass(u8); - -// Possible values for message class in STUN Message Type. -pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); // 0b00 -pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); // 0b01 -pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); // 0b10 -pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); // 0b11 - -impl fmt::Display for MessageClass { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - CLASS_REQUEST => "request", - CLASS_INDICATION => "indication", - CLASS_SUCCESS_RESPONSE => "success response", - CLASS_ERROR_RESPONSE => "error response", - _ => "unknown message class", - }; - - write!(f, "{s}") - } -} - -// Method is uint16 representation of 12-bit STUN method. -#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] -pub struct Method(u16); - -// Possible methods for STUN Message. -pub const METHOD_BINDING: Method = Method(0x001); -pub const METHOD_ALLOCATE: Method = Method(0x003); -pub const METHOD_REFRESH: Method = Method(0x004); -pub const METHOD_SEND: Method = Method(0x006); -pub const METHOD_DATA: Method = Method(0x007); -pub const METHOD_CREATE_PERMISSION: Method = Method(0x008); -pub const METHOD_CHANNEL_BIND: Method = Method(0x009); - -// Methods from RFC 6062. -pub const METHOD_CONNECT: Method = Method(0x000a); -pub const METHOD_CONNECTION_BIND: Method = Method(0x000b); -pub const METHOD_CONNECTION_ATTEMPT: Method = Method(0x000c); - -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let unknown = format!("0x{:x}", self.0); - - let s = match *self { - METHOD_BINDING => "Binding", - METHOD_ALLOCATE => "Allocate", - METHOD_REFRESH => "Refresh", - METHOD_SEND => "Send", - METHOD_DATA => "Data", - METHOD_CREATE_PERMISSION => "CreatePermission", - METHOD_CHANNEL_BIND => "ChannelBind", - - // RFC 6062. - METHOD_CONNECT => "Connect", - METHOD_CONNECTION_BIND => "ConnectionBind", - METHOD_CONNECTION_ATTEMPT => "ConnectionAttempt", - _ => unknown.as_str(), - }; - - write!(f, "{s}") - } -} - -// MessageType is STUN Message Type Field. -#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] -pub struct MessageType { - pub method: Method, // e.g. binding - pub class: MessageClass, // e.g. request -} - -// Common STUN message types. -// Binding request message type. -pub const BINDING_REQUEST: MessageType = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, -}; -// Binding success response message type -pub const BINDING_SUCCESS: MessageType = MessageType { - method: METHOD_BINDING, - class: CLASS_SUCCESS_RESPONSE, -}; -// Binding error response message type. -pub const BINDING_ERROR: MessageType = MessageType { - method: METHOD_BINDING, - class: CLASS_ERROR_RESPONSE, -}; - -impl fmt::Display for MessageType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.method, self.class) - } -} - -const METHOD_ABITS: u16 = 0xf; // 0b0000000000001111 -const METHOD_BBITS: u16 = 0x70; // 0b0000000001110000 -const METHOD_DBITS: u16 = 0xf80; // 0b0000111110000000 - -const METHOD_BSHIFT: u16 = 1; -const METHOD_DSHIFT: u16 = 2; - -const FIRST_BIT: u16 = 0x1; -const SECOND_BIT: u16 = 0x2; - -const C0BIT: u16 = FIRST_BIT; -const C1BIT: u16 = SECOND_BIT; - -const CLASS_C0SHIFT: u16 = 4; -const CLASS_C1SHIFT: u16 = 7; - -impl Setter for MessageType { - // add_to sets m type to t. - fn add_to(&self, m: &mut Message) -> Result<()> { - m.set_type(*self); - Ok(()) - } -} - -impl MessageType { - // NewType returns new message type with provided method and class. - pub fn new(method: Method, class: MessageClass) -> Self { - MessageType { method, class } - } - - // Value returns bit representation of messageType. - pub fn value(&self) -> u16 { - // 0 1 - // 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ - // |M |M |M|M|M|C|M|M|M|C|M|M|M|M| - // |11|10|9|8|7|1|6|5|4|0|3|2|1|0| - // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ - // Figure 3: Format of STUN Message Type Field - - // Warning: Abandon all hope ye who enter here. - // Splitting M into A(M0-M3), B(M4-M6), D(M7-M11). - let method = self.method.0; - let a = method & METHOD_ABITS; // A = M * 0b0000000000001111 (right 4 bits) - let b = method & METHOD_BBITS; // B = M * 0b0000000001110000 (3 bits after A) - let d = method & METHOD_DBITS; // D = M * 0b0000111110000000 (5 bits after B) - - // Shifting to add "holes" for C0 (at 4 bit) and C1 (8 bit). - let method = a + (b << METHOD_BSHIFT) + (d << METHOD_DSHIFT); - - // C0 is zero bit of C, C1 is first bit. - // C0 = C * 0b01, C1 = (C * 0b10) >> 1 - // Ct = C0 << 4 + C1 << 8. - // Optimizations: "((C * 0b10) >> 1) << 8" as "(C * 0b10) << 7" - // We need C0 shifted by 4, and C1 by 8 to fit "11" and "7" positions - // (see figure 3). - let c = self.class.0 as u16; - let c0 = (c & C0BIT) << CLASS_C0SHIFT; - let c1 = (c & C1BIT) << CLASS_C1SHIFT; - let class = c0 + c1; - - method + class - } - - // ReadValue decodes uint16 into MessageType. - pub fn read_value(&mut self, value: u16) { - // Decoding class. - // We are taking first bit from v >> 4 and second from v >> 7. - let c0 = (value >> CLASS_C0SHIFT) & C0BIT; - let c1 = (value >> CLASS_C1SHIFT) & C1BIT; - let class = c0 + c1; - self.class = MessageClass(class as u8); - - // Decoding method. - let a = value & METHOD_ABITS; // A(M0-M3) - let b = (value >> METHOD_BSHIFT) & METHOD_BBITS; // B(M4-M6) - let d = (value >> METHOD_DSHIFT) & METHOD_DBITS; // D(M7-M11) - let m = a + b + d; - self.method = Method(m); - } -} diff --git a/stun/src/message/message_test.rs b/stun/src/message/message_test.rs deleted file mode 100644 index afd388ef5..000000000 --- a/stun/src/message/message_test.rs +++ /dev/null @@ -1,744 +0,0 @@ -use std::io::{BufReader, BufWriter}; - -use super::*; -use crate::fingerprint::FINGERPRINT; -use crate::integrity::MessageIntegrity; -use crate::textattrs::TextAttribute; -use crate::xoraddr::*; - -#[test] -fn test_message_buffer() -> Result<()> { - let mut m = Message::new(); - m.typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - m.transaction_id = TransactionId::new(); - m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); - m.write_header(); - - let mut m_decoded = Message::new(); - let mut reader = BufReader::new(m.raw.as_slice()); - m_decoded.read_from(&mut reader)?; - - assert_eq!(m_decoded, m, "{m_decoded} != {m}"); - - Ok(()) -} - -#[test] -fn test_message_type_value() -> Result<()> { - let tests = vec![ - ( - MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }, - 0x0001, - ), - ( - MessageType { - method: METHOD_BINDING, - class: CLASS_SUCCESS_RESPONSE, - }, - 0x0101, - ), - ( - MessageType { - method: METHOD_BINDING, - class: CLASS_ERROR_RESPONSE, - }, - 0x0111, - ), - ( - MessageType { - method: Method(0xb6d), - class: MessageClass(0x3), - }, - 0x2ddd, - ), - ]; - - for (input, output) in tests { - let b = input.value(); - assert_eq!(b, output, "Value({input}) -> {b}, want {output}"); - } - - Ok(()) -} - -#[test] -fn test_message_type_read_value() -> Result<()> { - let tests = vec![ - ( - 0x0001, - MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }, - ), - ( - 0x0101, - MessageType { - method: METHOD_BINDING, - class: CLASS_SUCCESS_RESPONSE, - }, - ), - ( - 0x0111, - MessageType { - method: METHOD_BINDING, - class: CLASS_ERROR_RESPONSE, - }, - ), - ]; - - for (input, output) in tests { - let mut m = MessageType::default(); - m.read_value(input); - assert_eq!(m, output, "ReadValue({input}) -> {m}, want {output}"); - } - - Ok(()) -} - -#[test] -fn test_message_type_read_write_value() -> Result<()> { - let tests = vec![ - MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }, - MessageType { - method: METHOD_BINDING, - class: CLASS_SUCCESS_RESPONSE, - }, - MessageType { - method: METHOD_BINDING, - class: CLASS_ERROR_RESPONSE, - }, - MessageType { - method: Method(0x12), - class: CLASS_ERROR_RESPONSE, - }, - ]; - - for test in tests { - let mut m = MessageType::default(); - let v = test.value(); - m.read_value(v); - assert_eq!(m, test, "ReadValue({test} -> {v}) = {m}, should be {test}"); - } - - Ok(()) -} - -#[test] -fn test_message_write_to() -> Result<()> { - let mut m = Message::new(); - m.typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - m.transaction_id = TransactionId::new(); - m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); - m.write_header(); - let mut buf = vec![]; - { - let mut writer = BufWriter::<&mut Vec>::new(buf.as_mut()); - m.write_to(&mut writer)?; - } - - let mut m_decoded = Message::new(); - let mut reader = BufReader::new(buf.as_slice()); - m_decoded.read_from(&mut reader)?; - assert_eq!(m_decoded, m, "{m_decoded} != {m}"); - - Ok(()) -} - -#[test] -fn test_message_cookie() -> Result<()> { - let buf = vec![0; 20]; - let mut m_decoded = Message::new(); - let mut reader = BufReader::new(buf.as_slice()); - let result = m_decoded.read_from(&mut reader); - assert!(result.is_err(), "should error"); - - Ok(()) -} - -#[test] -fn test_message_length_less_header_size() -> Result<()> { - let buf = vec![0; 8]; - let mut m_decoded = Message::new(); - let mut reader = BufReader::new(buf.as_slice()); - let result = m_decoded.read_from(&mut reader); - assert!(result.is_err(), "should error"); - - Ok(()) -} - -#[test] -fn test_message_bad_length() -> Result<()> { - let m_type = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let mut m = Message { - typ: m_type, - length: 4, - transaction_id: TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), - ..Default::default() - }; - m.add(AttrType(0x1), &[1, 2]); - m.write_header(); - m.raw[20 + 3] = 10; // set attr length = 10 - - let mut m_decoded = Message::new(); - let result = m_decoded.write(&m.raw); - assert!(result.is_err(), "should error"); - - Ok(()) -} - -#[test] -fn test_message_attr_length_less_than_header() -> Result<()> { - let m_type = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let message_attribute = RawAttribute { - length: 2, - value: vec![1, 2], - typ: AttrType(0x1), - }; - let message_attributes = Attributes(vec![message_attribute]); - let mut m = Message { - typ: m_type, - transaction_id: TransactionId::new(), - attributes: message_attributes, - ..Default::default() - }; - m.encode(); - - let mut m_decoded = Message::new(); - m.raw[3] = 2; // rewrite to bad length - - let mut reader = BufReader::new(&m.raw[..20 + 2]); - let result = m_decoded.read_from(&mut reader); - assert!(result.is_err(), "should be error"); - - Ok(()) -} - -#[test] -fn test_message_attr_size_less_than_length() -> Result<()> { - let m_type = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - let message_attribute = RawAttribute { - length: 4, - value: vec![1, 2, 3, 4], - typ: AttrType(0x1), - }; - let message_attributes = Attributes(vec![message_attribute]); - let mut m = Message { - typ: m_type, - transaction_id: TransactionId::new(), - attributes: message_attributes, - ..Default::default() - }; - m.write_attributes(); - m.write_header(); - m.raw[3] = 5; // rewrite to bad length - - let mut m_decoded = Message::new(); - let mut reader = BufReader::new(&m.raw[..20 + 5]); - let result = m_decoded.read_from(&mut reader); - assert!(result.is_err(), "should be error"); - - Ok(()) -} - -#[test] -fn test_message_read_from_error() -> Result<()> { - let mut m_decoded = Message::new(); - let buf = vec![]; - let mut reader = BufReader::new(buf.as_slice()); - let result = m_decoded.read_from(&mut reader); - assert!(result.is_err(), "should be error"); - - Ok(()) -} - -#[test] -fn test_message_class_string() -> Result<()> { - let v = vec![ - CLASS_REQUEST, - CLASS_ERROR_RESPONSE, - CLASS_SUCCESS_RESPONSE, - CLASS_INDICATION, - ]; - - for k in v { - if k.to_string() == *"unknown message class" { - panic!("bad stringer {k}"); - } - } - - // should panic - let p = MessageClass(0x05).to_string(); - assert_eq!(p, "unknown message class", "should be error {p}"); - - Ok(()) -} - -#[test] -fn test_attr_type_string() -> Result<()> { - let v = vec![ - ATTR_MAPPED_ADDRESS, - ATTR_USERNAME, - ATTR_ERROR_CODE, - ATTR_MESSAGE_INTEGRITY, - ATTR_UNKNOWN_ATTRIBUTES, - ATTR_REALM, - ATTR_NONCE, - ATTR_XORMAPPED_ADDRESS, - ATTR_SOFTWARE, - ATTR_ALTERNATE_SERVER, - ATTR_FINGERPRINT, - ]; - for k in v { - assert!(!k.to_string().starts_with("0x"), "bad stringer"); - } - - let v_non_standard = AttrType(0x512); - assert!( - v_non_standard.to_string().starts_with("0x512"), - "bad prefix" - ); - - Ok(()) -} - -#[test] -fn test_method_string() -> Result<()> { - assert_eq!( - METHOD_BINDING.to_string(), - "Binding".to_owned(), - "binding is not binding!" - ); - assert_eq!( - Method(0x616).to_string(), - "0x616".to_owned(), - "Bad stringer {}", - Method(0x616) - ); - - Ok(()) -} - -#[test] -fn test_attribute_equal() -> Result<()> { - let a = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - ..Default::default() - }; - let b = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - ..Default::default() - }; - assert_eq!(a, b, "should equal"); - - assert_ne!( - a, - RawAttribute { - typ: AttrType(0x2), - ..Default::default() - }, - "should not equal" - ); - assert_ne!( - a, - RawAttribute { - length: 0x2, - ..Default::default() - }, - "should not equal" - ); - assert_ne!( - a, - RawAttribute { - length: 0x3, - ..Default::default() - }, - "should not equal" - ); - assert_ne!( - a, - RawAttribute { - length: 0x2, - value: vec![0x1, 0x3], - ..Default::default() - }, - "should not equal" - ); - - Ok(()) -} - -#[test] -fn test_message_equal() -> Result<()> { - let attr = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - typ: AttrType(0x1), - }; - let attrs = Attributes(vec![attr]); - let a = Message { - attributes: attrs.clone(), - length: 4 + 2, - ..Default::default() - }; - let b = Message { - attributes: attrs.clone(), - length: 4 + 2, - ..Default::default() - }; - assert_eq!(a, b, "should equal"); - assert_ne!( - a, - Message { - typ: MessageType { - class: MessageClass(128), - ..Default::default() - }, - ..Default::default() - }, - "should not equal" - ); - - let t_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - - assert_ne!( - a, - Message { - transaction_id: t_id, - ..Default::default() - }, - "should not equal" - ); - assert_ne!( - a, - Message { - length: 3, - ..Default::default() - }, - "should not equal" - ); - - let t_attrs = Attributes(vec![RawAttribute { - length: 1, - value: vec![0x1], - typ: AttrType(0x1), - }]); - assert_ne!( - a, - Message { - attributes: t_attrs, - length: 4 + 2, - ..Default::default() - }, - "should not equal" - ); - - let t_attrs = Attributes(vec![RawAttribute { - length: 2, - value: vec![0x1, 0x1], - typ: AttrType(0x2), - }]); - assert_ne!( - a, - Message { - attributes: t_attrs, - length: 4 + 2, - ..Default::default() - }, - "should not equal" - ); - - //"Nil attributes" - { - let a = Message { - length: 4 + 2, - ..Default::default() - }; - let mut b = Message { - attributes: attrs, - length: 4 + 2, - ..Default::default() - }; - - assert_ne!(a, b, "should not equal"); - assert_ne!(b, a, "should not equal"); - b.attributes = Attributes::default(); - assert_eq!(a, b, "should equal"); - } - - //"Attributes length" - { - let attr = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - typ: AttrType(0x1), - }; - let attr1 = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - typ: AttrType(0x1), - }; - let a = Message { - attributes: Attributes(vec![attr.clone()]), - length: 4 + 2, - ..Default::default() - }; - let b = Message { - attributes: Attributes(vec![attr, attr1]), - length: 4 + 2, - ..Default::default() - }; - assert_ne!(a, b, "should not equal"); - } - - //"Attributes values" - { - let attr = RawAttribute { - length: 2, - value: vec![0x1, 0x2], - typ: AttrType(0x1), - }; - let attr1 = RawAttribute { - length: 2, - value: vec![0x1, 0x1], - typ: AttrType(0x1), - }; - let a = Message { - attributes: Attributes(vec![attr.clone(), attr.clone()]), - length: 4 + 2, - ..Default::default() - }; - let b = Message { - attributes: Attributes(vec![attr, attr1]), - length: 4 + 2, - ..Default::default() - }; - assert_ne!(a, b, "should not equal"); - } - - Ok(()) -} - -#[test] -fn test_message_grow() -> Result<()> { - let mut m = Message::new(); - m.grow(512, false); - assert_eq!(m.raw.len(), 512, "Bad length {}", m.raw.len()); - - Ok(()) -} - -#[test] -fn test_message_grow_smaller() -> Result<()> { - let mut m = Message::new(); - m.grow(2, false); - assert!(m.raw.capacity() >= 20, "Bad capacity {}", m.raw.capacity()); - - assert!(m.raw.len() >= 20, "Bad length {}", m.raw.len()); - - Ok(()) -} - -#[test] -fn test_message_string() -> Result<()> { - let m = Message::new(); - assert_ne!(m.to_string(), "", "bad string"); - - Ok(()) -} - -#[test] -fn test_is_message() -> Result<()> { - let mut m = Message::new(); - let a = TextAttribute { - attr: ATTR_SOFTWARE, - text: "software".to_owned(), - }; - a.add_to(&mut m)?; - m.write_header(); - - let tests = vec![ - (vec![], false), // 0 - (vec![1, 2, 3], false), // 1 - (vec![1, 2, 4], false), // 2 - (vec![1, 2, 4, 5, 6, 7, 8, 9, 20], false), // 3 - (m.raw.to_vec(), true), // 5 - ( - vec![ - 0, 0, 0, 0, 33, 18, 164, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - true, - ), // 6 - ]; - - for (input, output) in tests { - let got = is_message(&input); - assert_eq!(got, output, "IsMessage({input:?}) {got} != {output}"); - } - - Ok(()) -} - -#[test] -fn test_message_contains() -> Result<()> { - let mut m = Message::new(); - m.add(ATTR_SOFTWARE, "value".as_bytes()); - - assert!(m.contains(ATTR_SOFTWARE), "message should contain software"); - assert!(!m.contains(ATTR_NONCE), "message should not contain nonce"); - - Ok(()) -} - -#[test] -fn test_message_full_size() -> Result<()> { - let mut m = Message::new(); - m.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), - Box::new(TextAttribute::new(ATTR_SOFTWARE, "pion/stun".to_owned())), - Box::new(MessageIntegrity::new_long_term_integrity( - "username".to_owned(), - "realm".to_owned(), - "password".to_owned(), - )), - Box::new(FINGERPRINT), - ])?; - let l = m.raw.len(); - m.raw = m.raw[..l - 10].to_vec(); - - let mut decoder = Message::new(); - let l = m.raw.len(); - decoder.raw = m.raw[..l - 10].to_vec(); - let result = decoder.decode(); - assert!(result.is_err(), "decode on truncated buffer should error"); - - Ok(()) -} - -#[test] -fn test_message_clone_to() -> Result<()> { - let mut m = Message::new(); - m.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), - Box::new(TextAttribute::new(ATTR_SOFTWARE, "pion/stun".to_owned())), - Box::new(MessageIntegrity::new_long_term_integrity( - "username".to_owned(), - "realm".to_owned(), - "password".to_owned(), - )), - Box::new(FINGERPRINT), - ])?; - m.encode(); - - let mut b = Message::new(); - m.clone_to(&mut b)?; - assert_eq!(b, m, "not equal"); - - //TODO: Corrupting m and checking that b is not corrupted. - /*let (mut s, ok) = b.attributes.get(ATTR_SOFTWARE); - assert!(ok, "no software attribute"); - s.value[0] = b'k'; - s.add_to(&mut b)?; - assert_ne!(b, m, "should not be equal");*/ - - Ok(()) -} - -#[test] -fn test_message_add_to() -> Result<()> { - let mut m = Message::new(); - m.build(&[ - Box::new(BINDING_REQUEST), - Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), - Box::new(FINGERPRINT), - ])?; - m.encode(); - - let mut b = Message::new(); - m.clone_to(&mut b)?; - - m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, 0]); - assert_ne!(b, m, "should not be equal"); - - m.add_to(&mut b)?; - assert_eq!(b, m, "should be equal"); - - Ok(()) -} - -#[test] -fn test_decode() -> Result<()> { - let mut m = Message::new(); - m.typ = MessageType { - method: METHOD_BINDING, - class: CLASS_REQUEST, - }; - m.transaction_id = TransactionId::new(); - m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); - m.write_header(); - - let mut m_decoded = Message::new(); - m_decoded.raw.clear(); - m_decoded.raw.extend_from_slice(&m.raw); - m_decoded.decode()?; - assert_eq!( - m_decoded, m, - "decoded result is not equal to encoded message" - ); - - Ok(()) -} - -#[test] -fn test_message_marshal_binary() -> Result<()> { - let mut m = Message::new(); - m.build(&[ - Box::new(TextAttribute::new(ATTR_SOFTWARE, "software".to_owned())), - Box::new(XorMappedAddress { - ip: "213.1.223.5".parse().unwrap(), - port: 0, - }), - ])?; - - let mut data = m.marshal_binary()?; - // Reset m.Raw to check retention. - for i in 0..m.raw.len() { - m.raw[i] = 0; - } - m.unmarshal_binary(&data)?; - - // Reset data to check retention. - #[allow(clippy::needless_range_loop)] - for i in 0..data.len() { - data[i] = 0; - } - - m.decode()?; - - Ok(()) -} diff --git a/stun/src/textattrs.rs b/stun/src/textattrs.rs deleted file mode 100644 index 54c5e77c7..000000000 --- a/stun/src/textattrs.rs +++ /dev/null @@ -1,95 +0,0 @@ -#[cfg(test)] -mod textattrs_test; - -use std::fmt; - -use crate::attributes::*; -use crate::checks::*; -use crate::error::*; -use crate::message::*; - -const MAX_USERNAME_B: usize = 513; -const MAX_REALM_B: usize = 763; -const MAX_SOFTWARE_B: usize = 763; -const MAX_NONCE_B: usize = 763; - -// Username represents USERNAME attribute. -// -// RFC 5389 Section 15.3 -pub type Username = TextAttribute; - -// Realm represents REALM attribute. -// -// RFC 5389 Section 15.7 -pub type Realm = TextAttribute; - -// Nonce represents NONCE attribute. -// -// RFC 5389 Section 15.8 -pub type Nonce = TextAttribute; - -// Software is SOFTWARE attribute. -// -// RFC 5389 Section 15.10 -pub type Software = TextAttribute; - -// TextAttribute is helper for adding and getting text attributes. -#[derive(Clone, Default)] -pub struct TextAttribute { - pub attr: AttrType, - pub text: String, -} - -impl fmt::Display for TextAttribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.text) - } -} - -impl Setter for TextAttribute { - // add_to_as adds attribute with type t to m, checking maximum length. If max_len - // is less than 0, no check is performed. - fn add_to(&self, m: &mut Message) -> Result<()> { - let text = self.text.as_bytes(); - let max_len = match self.attr { - ATTR_USERNAME => MAX_USERNAME_B, - ATTR_REALM => MAX_REALM_B, - ATTR_SOFTWARE => MAX_SOFTWARE_B, - ATTR_NONCE => MAX_NONCE_B, - _ => return Err(Error::Other(format!("Unsupported AttrType {}", self.attr))), - }; - - check_overflow(self.attr, text.len(), max_len)?; - m.add(self.attr, text); - Ok(()) - } -} - -impl Getter for TextAttribute { - fn get_from(&mut self, m: &Message) -> Result<()> { - let attr = self.attr; - *self = TextAttribute::get_from_as(m, attr)?; - Ok(()) - } -} - -impl TextAttribute { - pub fn new(attr: AttrType, text: String) -> Self { - TextAttribute { attr, text } - } - - // get_from_as gets t attribute from m and appends its value to reset v. - pub fn get_from_as(m: &Message, attr: AttrType) -> Result { - match attr { - ATTR_USERNAME => {} - ATTR_REALM => {} - ATTR_SOFTWARE => {} - ATTR_NONCE => {} - _ => return Err(Error::Other(format!("Unsupported AttrType {attr}"))), - }; - - let a = m.get(attr)?; - let text = String::from_utf8(a)?; - Ok(TextAttribute { attr, text }) - } -} diff --git a/stun/src/textattrs/textattrs_test.rs b/stun/src/textattrs/textattrs_test.rs deleted file mode 100644 index e0a01aba1..000000000 --- a/stun/src/textattrs/textattrs_test.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::io::BufReader; - -use super::*; -use crate::checks::*; -use crate::error::*; - -#[test] -fn test_software_get_from() -> Result<()> { - let mut m = Message::new(); - let v = "Client v0.0.1".to_owned(); - m.add(ATTR_SOFTWARE, v.as_bytes()); - m.write_header(); - - let mut m2 = Message { - raw: Vec::with_capacity(256), - ..Default::default() - }; - - let mut reader = BufReader::new(m.raw.as_slice()); - m2.read_from(&mut reader)?; - let software = TextAttribute::get_from_as(&m, ATTR_SOFTWARE)?; - assert_eq!(software.to_string(), v, "Expected {v}, got {software}."); - - let (s_attr, ok) = m.attributes.get(ATTR_SOFTWARE); - assert!(ok, "sowfware attribute should be found"); - - let s = s_attr.to_string(); - assert!(s.starts_with("SOFTWARE:"), "bad string representation {s}"); - - Ok(()) -} - -#[test] -fn test_software_add_to_invalid() -> Result<()> { - let mut m = Message::new(); - let s = TextAttribute { - attr: ATTR_SOFTWARE, - text: String::from_utf8(vec![0; 1024]).unwrap(), - }; - let result = s.add_to(&mut m); - if let Err(err) = result { - assert!( - is_attr_size_overflow(&err), - "add_to should return AttrOverflowErr, got: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - let result = TextAttribute::get_from_as(&m, ATTR_SOFTWARE); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "GetFrom should return {}, got: {}", - Error::ErrAttributeNotFound, - err - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_software_add_to_regression() -> Result<()> { - // s.add_to checked len(m.Raw) instead of len(s.Raw). - let mut m = Message { - raw: vec![0u8; 2048], - ..Default::default() - }; - let s = TextAttribute { - attr: ATTR_SOFTWARE, - text: String::from_utf8(vec![0; 100]).unwrap(), - }; - s.add_to(&mut m)?; - - Ok(()) -} - -#[test] -fn test_username() -> Result<()> { - let username = "username".to_owned(); - let u = TextAttribute { - attr: ATTR_USERNAME, - text: username.clone(), - }; - let mut m = Message::new(); - m.write_header(); - //"Bad length" - { - let bad_u = TextAttribute { - attr: ATTR_USERNAME, - text: String::from_utf8(vec![0; 600]).unwrap(), - }; - let result = bad_u.add_to(&mut m); - if let Err(err) = result { - assert!( - is_attr_size_overflow(&err), - "add_to should return *AttrOverflowErr, got: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - } - //"add_to" - { - u.add_to(&mut m)?; - - //"GetFrom" - { - let got = TextAttribute::get_from_as(&m, ATTR_USERNAME)?; - assert_eq!( - got.to_string(), - username, - "expedted: {username}, got: {got}" - ); - //"Not found" - { - let m = Message::new(); - let result = TextAttribute::get_from_as(&m, ATTR_USERNAME); - if let Err(err) = result { - assert_eq!(Error::ErrAttributeNotFound, err, "Should error"); - } else { - panic!("expected error, but got ok"); - } - } - } - } - - //"No allocations" - { - let mut m = Message::new(); - m.write_header(); - let u = TextAttribute { - attr: ATTR_USERNAME, - text: "username".to_owned(), - }; - - u.add_to(&mut m)?; - m.reset(); - } - - Ok(()) -} - -#[test] -fn test_realm_get_from() -> Result<()> { - let mut m = Message::new(); - let v = "realm".to_owned(); - m.add(ATTR_REALM, v.as_bytes()); - m.write_header(); - - let mut m2 = Message { - raw: Vec::with_capacity(256), - ..Default::default() - }; - - let result = TextAttribute::get_from_as(&m2, ATTR_REALM); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "GetFrom should return {}, got: {}", - Error::ErrAttributeNotFound, - err - ); - } else { - panic!("Expected error, but got ok"); - } - - let mut reader = BufReader::new(m.raw.as_slice()); - m2.read_from(&mut reader)?; - - let r = TextAttribute::get_from_as(&m, ATTR_REALM)?; - assert_eq!(r.to_string(), v, "Expected {v}, got {r}."); - - let (r_attr, ok) = m.attributes.get(ATTR_REALM); - assert!(ok, "realm attribute should be found"); - - let s = r_attr.to_string(); - assert!(s.starts_with("REALM:"), "bad string representation {s}"); - - Ok(()) -} - -#[test] -fn test_realm_add_to_invalid() -> Result<()> { - let mut m = Message::new(); - let s = TextAttribute { - attr: ATTR_REALM, - text: String::from_utf8(vec![0; 1024]).unwrap(), - }; - let result = s.add_to(&mut m); - if let Err(err) = result { - assert!( - is_attr_size_overflow(&err), - "add_to should return AttrOverflowErr, got: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - let result = TextAttribute::get_from_as(&m, ATTR_REALM); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "GetFrom should return {}, got: {}", - Error::ErrAttributeNotFound, - err - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_nonce_get_from() -> Result<()> { - let mut m = Message::new(); - let v = "example.org".to_owned(); - m.add(ATTR_NONCE, v.as_bytes()); - m.write_header(); - - let mut m2 = Message { - raw: Vec::with_capacity(256), - ..Default::default() - }; - - let result = TextAttribute::get_from_as(&m2, ATTR_NONCE); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "GetFrom should return {}, got: {}", - Error::ErrAttributeNotFound, - err - ); - } else { - panic!("Expected error, but got ok"); - } - - let mut reader = BufReader::new(m.raw.as_slice()); - m2.read_from(&mut reader)?; - - let r = TextAttribute::get_from_as(&m, ATTR_NONCE)?; - assert_eq!(r.to_string(), v, "Expected {v}, got {r}."); - - let (r_attr, ok) = m.attributes.get(ATTR_NONCE); - assert!(ok, "realm attribute should be found"); - - let s = r_attr.to_string(); - assert!(s.starts_with("NONCE:"), "bad string representation {s}"); - - Ok(()) -} - -#[test] -fn test_nonce_add_to_invalid() -> Result<()> { - let mut m = Message::new(); - let s = TextAttribute { - attr: ATTR_NONCE, - text: String::from_utf8(vec![0; 1024]).unwrap(), - }; - let result = s.add_to(&mut m); - if let Err(err) = result { - assert!( - is_attr_size_overflow(&err), - "add_to should return AttrOverflowErr, got: {err}" - ); - } else { - panic!("expected error, but got ok"); - } - - let result = TextAttribute::get_from_as(&m, ATTR_NONCE); - if let Err(err) = result { - assert_eq!( - Error::ErrAttributeNotFound, - err, - "GetFrom should return {}, got: {}", - Error::ErrAttributeNotFound, - err - ); - } else { - panic!("expected error, but got ok"); - } - - Ok(()) -} - -#[test] -fn test_nonce_add_to() -> Result<()> { - let mut m = Message::new(); - let n = TextAttribute { - attr: ATTR_NONCE, - text: "example.org".to_owned(), - }; - n.add_to(&mut m)?; - - let v = m.get(ATTR_NONCE)?; - assert_eq!(v.as_slice(), b"example.org", "bad nonce {v:?}"); - - Ok(()) -} diff --git a/stun/src/uattrs.rs b/stun/src/uattrs.rs deleted file mode 100644 index 087d8099f..000000000 --- a/stun/src/uattrs.rs +++ /dev/null @@ -1,62 +0,0 @@ -#[cfg(test)] -mod uattrs_test; - -use std::fmt; - -use crate::attributes::*; -use crate::error::*; -use crate::message::*; - -// UnknownAttributes represents UNKNOWN-ATTRIBUTES attribute. -// -// RFC 5389 Section 15.9 -pub struct UnknownAttributes(pub Vec); - -impl fmt::Display for UnknownAttributes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.0.is_empty() { - write!(f, "") - } else { - let mut s = vec![]; - for t in &self.0 { - s.push(t.to_string()); - } - write!(f, "{}", s.join(", ")) - } - } -} - -// type size is 16 bit. -const ATTR_TYPE_SIZE: usize = 2; - -impl Setter for UnknownAttributes { - // add_to adds UNKNOWN-ATTRIBUTES attribute to message. - fn add_to(&self, m: &mut Message) -> Result<()> { - let mut v = Vec::with_capacity(ATTR_TYPE_SIZE * 20); // 20 should be enough - // If len(a.Types) > 20, there will be allocations. - for t in &self.0 { - v.extend_from_slice(&t.value().to_be_bytes()); - } - m.add(ATTR_UNKNOWN_ATTRIBUTES, &v); - Ok(()) - } -} - -impl Getter for UnknownAttributes { - // GetFrom parses UNKNOWN-ATTRIBUTES from message. - fn get_from(&mut self, m: &Message) -> Result<()> { - let v = m.get(ATTR_UNKNOWN_ATTRIBUTES)?; - if v.len() % ATTR_TYPE_SIZE != 0 { - return Err(Error::ErrBadUnknownAttrsSize); - } - self.0.clear(); - let mut first = 0usize; - while first < v.len() { - let last = first + ATTR_TYPE_SIZE; - self.0 - .push(AttrType(u16::from_be_bytes([v[first], v[first + 1]]))); - first = last; - } - Ok(()) - } -} diff --git a/stun/src/uattrs/uattrs_test.rs b/stun/src/uattrs/uattrs_test.rs deleted file mode 100644 index 2351d555d..000000000 --- a/stun/src/uattrs/uattrs_test.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::*; - -#[test] -fn test_unknown_attributes() -> Result<()> { - let mut m = Message::new(); - let a = UnknownAttributes(vec![ATTR_DONT_FRAGMENT, ATTR_CHANNEL_NUMBER]); - assert_eq!( - a.to_string(), - "DONT-FRAGMENT, CHANNEL-NUMBER", - "bad String:{a}" - ); - assert_eq!( - UnknownAttributes(vec![]).to_string(), - "", - "bad blank string" - ); - - a.add_to(&mut m)?; - - //"GetFrom" - { - let mut attrs = UnknownAttributes(Vec::with_capacity(10)); - attrs.get_from(&m)?; - for i in 0..a.0.len() { - assert_eq!(a.0[i], attrs.0[i], "expected {} != {}", a.0[i], attrs.0[i]); - } - let mut m_blank = Message::new(); - let result = attrs.get_from(&m_blank); - assert!(result.is_err(), "should error"); - - m_blank.add(ATTR_UNKNOWN_ATTRIBUTES, &[1, 2, 3]); - let result = attrs.get_from(&m_blank); - assert!(result.is_err(), "should error"); - } - - Ok(()) -} diff --git a/stun/src/uri.rs b/stun/src/uri.rs deleted file mode 100644 index 5dce476d9..000000000 --- a/stun/src/uri.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[cfg(test)] -mod uri_test; - -use std::fmt; - -use crate::error::*; - -// SCHEME definitions from RFC 7064 Section 3.2. - -pub const SCHEME: &str = "stun"; -pub const SCHEME_SECURE: &str = "stuns"; - -// URI as defined in RFC 7064. -#[derive(PartialEq, Eq, Debug)] -pub struct Uri { - pub scheme: String, - pub host: String, - pub port: Option, -} - -impl fmt::Display for Uri { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let host = if self.host.contains("::") { - "[".to_owned() + self.host.as_str() + "]" - } else { - self.host.clone() - }; - - if let Some(port) = self.port { - write!(f, "{}:{}:{}", self.scheme, host, port) - } else { - write!(f, "{}:{}", self.scheme, host) - } - } -} - -impl Uri { - // parse_uri parses URI from string. - pub fn parse_uri(raw: &str) -> Result { - // work around for url crate - if raw.contains("//") { - return Err(Error::ErrInvalidUrl); - } - - let mut s = raw.to_string(); - let pos = raw.find(':'); - if let Some(p) = pos { - s.replace_range(p..p + 1, "://"); - } else { - return Err(Error::ErrSchemeType); - } - - let raw_parts = url::Url::parse(&s)?; - - let scheme = raw_parts.scheme().into(); - if scheme != SCHEME && scheme != SCHEME_SECURE { - return Err(Error::ErrSchemeType); - } - - let host = if let Some(host) = raw_parts.host_str() { - host.trim() - .trim_start_matches('[') - .trim_end_matches(']') - .to_owned() - } else { - return Err(Error::ErrHost); - }; - - let port = raw_parts.port(); - - Ok(Uri { scheme, host, port }) - } -} diff --git a/stun/src/uri/uri_test.rs b/stun/src/uri/uri_test.rs deleted file mode 100644 index 20f13d17e..000000000 --- a/stun/src/uri/uri_test.rs +++ /dev/null @@ -1,68 +0,0 @@ -use super::*; - -#[test] -fn test_parse_uri() -> Result<()> { - let tests = vec![ - ( - "default", - "stun:example.org", - Uri { - host: "example.org".to_owned(), - scheme: SCHEME.to_owned(), - port: None, - }, - "stun:example.org", - ), - ( - "secure", - "stuns:example.org", - Uri { - host: "example.org".to_owned(), - scheme: SCHEME_SECURE.to_owned(), - port: None, - }, - "stuns:example.org", - ), - ( - "with port", - "stun:example.org:8000", - Uri { - host: "example.org".to_owned(), - scheme: SCHEME.to_owned(), - port: Some(8000), - }, - "stun:example.org:8000", - ), - ( - "ipv6 address", - "stun:[::1]:123", - Uri { - host: "::1".to_owned(), - scheme: SCHEME.to_owned(), - port: Some(123), - }, - "stun:[::1]:123", - ), - ]; - - for (name, input, output, expected_str) in tests { - let out = Uri::parse_uri(input)?; - assert_eq!(out, output, "{name}: {out} != {output}"); - assert_eq!(out.to_string(), expected_str, "{name}"); - } - - //"MustFail" - { - let tests = vec![ - ("hierarchical", "stun://example.org"), - ("bad scheme", "tcp:example.org"), - ("invalid uri scheme", "stun_s:test"), - ]; - for (name, input) in tests { - let result = Uri::parse_uri(input); - assert!(result.is_err(), "{name} should fail, but did not"); - } - } - - Ok(()) -} diff --git a/stun/src/xoraddr.rs b/stun/src/xoraddr.rs deleted file mode 100644 index 0a86bb35c..000000000 --- a/stun/src/xoraddr.rs +++ /dev/null @@ -1,173 +0,0 @@ -#[cfg(test)] -mod xoraddr_test; - -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::{fmt, mem}; - -use crate::addr::*; -use crate::attributes::*; -use crate::checks::*; -use crate::error::*; -use crate::message::*; - -const WORD_SIZE: usize = mem::size_of::(); - -//var supportsUnaligned = runtime.GOARCH == "386" || runtime.GOARCH == "amd64" // nolint:gochecknoglobals - -// fast_xor_bytes xors in bulk. It only works on architectures that -// support unaligned read/writes. -/*TODO: fn fast_xor_bytes(dst:&[u8], a:&[u8], b:&[u8]) ->usize { - let mut n = a.len(); - if b.len() < n { - n = b.len(); - } - - let w = n / WORD_SIZE; - if w > 0 { - let dw = *(*[]uintptr)(unsafe.Pointer(&dst)) - let aw = *(*[]uintptr)(unsafe.Pointer(&a)) - let bw = *(*[]uintptr)(unsafe.Pointer(&b)) - for i := 0; i < w; i++ { - dw[i] = aw[i] ^ bw[i] - } - } - - for i := n - n%WORD_SIZE; i < n; i++ { - dst[i] = a[i] ^ b[i] - } - - return n -}*/ - -fn safe_xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { - let mut n = a.len(); - if b.len() < n { - n = b.len(); - } - if dst.len() < n { - n = dst.len(); - } - for i in 0..n { - dst[i] = a[i] ^ b[i]; - } - n -} - -/// xor_bytes xors the bytes in a and b. The destination is assumed to have enough -/// space. Returns the number of bytes xor'd. -pub fn xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { - //TODO: if supportsUnaligned { - // return fastXORBytes(dst, a, b) - //} - safe_xor_bytes(dst, a, b) -} - -/// XORMappedAddress implements XOR-MAPPED-ADDRESS attribute. -/// -/// RFC 5389 Section 15.2 -pub struct XorMappedAddress { - pub ip: IpAddr, - pub port: u16, -} - -impl Default for XorMappedAddress { - fn default() -> Self { - XorMappedAddress { - ip: IpAddr::V4(Ipv4Addr::from(0)), - port: 0, - } - } -} - -impl fmt::Display for XorMappedAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let family = match self.ip { - IpAddr::V4(_) => FAMILY_IPV4, - IpAddr::V6(_) => FAMILY_IPV6, - }; - if family == FAMILY_IPV4 { - write!(f, "{}:{}", self.ip, self.port) - } else { - write!(f, "[{}]:{}", self.ip, self.port) - } - } -} - -impl Setter for XorMappedAddress { - /// add_to adds XOR-MAPPED-ADDRESS to m. Can return ErrBadIPLength - /// if len(a.IP) is invalid. - fn add_to(&self, m: &mut Message) -> Result<()> { - self.add_to_as(m, ATTR_XORMAPPED_ADDRESS) - } -} - -impl Getter for XorMappedAddress { - /// get_from decodes XOR-MAPPED-ADDRESS attribute in message and returns - /// error if any. While decoding, a.IP is reused if possible and can be - /// rendered to invalid state (e.g. if a.IP was set to IPv6 and then - /// IPv4 value were decoded into it), be careful. - fn get_from(&mut self, m: &Message) -> Result<()> { - self.get_from_as(m, ATTR_XORMAPPED_ADDRESS) - } -} - -impl XorMappedAddress { - /// add_to_as adds XOR-MAPPED-ADDRESS value to m as t attribute. - pub fn add_to_as(&self, m: &mut Message, t: AttrType) -> Result<()> { - let (family, ip_len, ip) = match self.ip { - IpAddr::V4(ipv4) => (FAMILY_IPV4, IPV4LEN, ipv4.octets().to_vec()), - IpAddr::V6(ipv6) => (FAMILY_IPV6, IPV6LEN, ipv6.octets().to_vec()), - }; - - let mut value = [0; 32 + 128]; - //value[0] = 0 // first 8 bits are zeroes - let mut xor_value = vec![0; IPV6LEN]; - xor_value[4..].copy_from_slice(&m.transaction_id.0); - xor_value[0..4].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); - value[0..2].copy_from_slice(&family.to_be_bytes()); - value[2..4].copy_from_slice(&(self.port ^ (MAGIC_COOKIE >> 16) as u16).to_be_bytes()); - xor_bytes(&mut value[4..4 + ip_len], &ip, &xor_value); - m.add(t, &value[..4 + ip_len]); - Ok(()) - } - - /// get_from_as decodes XOR-MAPPED-ADDRESS attribute value in message - /// getting it as for t type. - pub fn get_from_as(&mut self, m: &Message, t: AttrType) -> Result<()> { - let v = m.get(t)?; - if v.len() <= 4 { - return Err(Error::ErrUnexpectedEof); - } - - let family = u16::from_be_bytes([v[0], v[1]]); - if family != FAMILY_IPV6 && family != FAMILY_IPV4 { - return Err(Error::Other(format!("bad value {family}"))); - } - - check_overflow( - t, - v[4..].len(), - if family == FAMILY_IPV4 { - IPV4LEN - } else { - IPV6LEN - }, - )?; - self.port = u16::from_be_bytes([v[2], v[3]]) ^ (MAGIC_COOKIE >> 16) as u16; - let mut xor_value = vec![0; 4 + TRANSACTION_ID_SIZE]; - xor_value[0..4].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); - xor_value[4..].copy_from_slice(&m.transaction_id.0); - - if family == FAMILY_IPV6 { - let mut ip = [0; IPV6LEN]; - xor_bytes(&mut ip, &v[4..], &xor_value); - self.ip = IpAddr::V6(Ipv6Addr::from(ip)); - } else { - let mut ip = [0; IPV4LEN]; - xor_bytes(&mut ip, &v[4..], &xor_value); - self.ip = IpAddr::V4(Ipv4Addr::from(ip)); - }; - - Ok(()) - } -} diff --git a/stun/src/xoraddr/xoraddr_test.rs b/stun/src/xoraddr/xoraddr_test.rs deleted file mode 100644 index 2d5544a33..000000000 --- a/stun/src/xoraddr/xoraddr_test.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::io::BufReader; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; - -use super::*; -use crate::checks::*; - -#[test] -fn test_xor_safe() -> Result<()> { - let mut dst = vec![0; 8]; - let a = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let b = vec![8, 7, 7, 6, 6, 3, 4, 1]; - safe_xor_bytes(&mut dst, &a, &b); - let c = dst.clone(); - safe_xor_bytes(&mut dst, &c, &a); - for i in 0..dst.len() { - assert_eq!(b[i], dst[i], "{} != {}", b[i], dst[i]); - } - - Ok(()) -} - -#[test] -fn test_xor_safe_bsmaller() -> Result<()> { - let mut dst = vec![0; 5]; - let a = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let b = vec![8, 7, 7, 6, 6]; - safe_xor_bytes(&mut dst, &a, &b); - let c = dst.clone(); - safe_xor_bytes(&mut dst, &c, &a); - for i in 0..dst.len() { - assert_eq!(b[i], dst[i], "{} != {}", b[i], dst[i]); - } - - Ok(()) -} - -#[test] -fn test_xormapped_address_get_from() -> Result<()> { - let mut m = Message::new(); - let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); - m.transaction_id.0.copy_from_slice(&transaction_id); - let addr_value = vec![0x00, 0x01, 0x9c, 0xd5, 0xf4, 0x9f, 0x38, 0xae]; - m.add(ATTR_XORMAPPED_ADDRESS, &addr_value); - let mut addr = XorMappedAddress { - ip: "0.0.0.0".parse().unwrap(), - port: 0, - }; - addr.get_from(&m)?; - assert_eq!( - addr.ip.to_string(), - "213.141.156.236", - "bad IP {} != 213.141.156.236", - addr.ip - ); - assert_eq!(addr.port, 48583, "bad Port {} != 48583", addr.port); - - //"UnexpectedEOF" - { - let mut m = Message::new(); - // {0, 1} is correct addr family. - m.add(ATTR_XORMAPPED_ADDRESS, &[0, 1, 3, 4]); - let mut addr = XorMappedAddress { - ip: "0.0.0.0".parse().unwrap(), - port: 0, - }; - let result = addr.get_from(&m); - if let Err(err) = result { - assert_eq!( - Error::ErrUnexpectedEof, - err, - "len(v) = 4 should render <{}> error, got <{}>", - Error::ErrUnexpectedEof, - err - ); - } else { - panic!("expected error, got ok"); - } - } - //"AttrOverflowErr" - { - let mut m = Message::new(); - // {0, 1} is correct addr family. - m.add( - ATTR_XORMAPPED_ADDRESS, - &[0, 1, 3, 4, 5, 6, 7, 8, 9, 1, 1, 1, 1, 1, 2, 3, 4], - ); - let mut addr = XorMappedAddress { - ip: "0.0.0.0".parse().unwrap(), - port: 0, - }; - let result = addr.get_from(&m); - if let Err(err) = result { - assert!( - is_attr_size_overflow(&err), - "AddTo should return AttrOverflowErr, got: {err}" - ); - } else { - panic!("expected error, got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_xormapped_address_get_from_invalid() -> Result<()> { - let mut m = Message::new(); - let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); - m.transaction_id.0.copy_from_slice(&transaction_id); - let expected_ip: IpAddr = "213.141.156.236".parse().unwrap(); - let expected_port = 21254u16; - let mut addr = XorMappedAddress { - ip: "0.0.0.0".parse().unwrap(), - port: 0, - }; - let result = addr.get_from(&m); - assert!(result.is_err(), "should be error"); - - addr.ip = expected_ip; - addr.port = expected_port; - addr.add_to(&mut m)?; - m.write_header(); - - let mut m_res = Message::new(); - m.raw[20 + 4 + 1] = 0x21; - m.decode()?; - let mut reader = BufReader::new(m.raw.as_slice()); - m_res.read_from(&mut reader)?; - let result = addr.get_from(&m); - assert!(result.is_err(), "should be error"); - - Ok(()) -} - -#[test] -fn test_xormapped_address_add_to() -> Result<()> { - let mut m = Message::new(); - let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); - m.transaction_id.0.copy_from_slice(&transaction_id); - let expected_ip: IpAddr = "213.141.156.236".parse().unwrap(); - let expected_port = 21254u16; - let mut addr = XorMappedAddress { - ip: "213.141.156.236".parse().unwrap(), - port: expected_port, - }; - addr.add_to(&mut m)?; - m.write_header(); - - let mut m_res = Message::new(); - m_res.write(&m.raw)?; - addr.get_from(&m_res)?; - assert_eq!( - addr.ip, expected_ip, - "{} (got) != {} (expected)", - addr.ip, expected_ip - ); - - assert_eq!( - addr.port, expected_port, - "bad Port {} != {}", - addr.port, expected_port - ); - - Ok(()) -} - -#[test] -fn test_xormapped_address_add_to_ipv6() -> Result<()> { - let mut m = Message::new(); - let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); - m.transaction_id.0.copy_from_slice(&transaction_id); - let expected_ip: IpAddr = "fe80::dc2b:44ff:fe20:6009".parse().unwrap(); - let expected_port = 21254u16; - let addr = XorMappedAddress { - ip: "fe80::dc2b:44ff:fe20:6009".parse().unwrap(), - port: 21254, - }; - addr.add_to(&mut m)?; - m.write_header(); - - let mut m_res = Message::new(); - let mut reader = BufReader::new(m.raw.as_slice()); - m_res.read_from(&mut reader)?; - - let mut got_addr = XorMappedAddress { - ip: "0.0.0.0".parse().unwrap(), - port: 0, - }; - got_addr.get_from(&m)?; - - assert_eq!( - got_addr.ip, expected_ip, - "bad IP {} != {}", - got_addr.ip, expected_ip - ); - assert_eq!( - got_addr.port, expected_port, - "bad Port {} != {}", - got_addr.port, expected_port - ); - - Ok(()) -} - -/* -#[test] -fn TestXORMappedAddress_AddTo_Invalid() -> Result<()> { - let mut m = Message::new(); - let mut addr = XORMappedAddress{ - ip: 1, 2, 3, 4, 5, 6, 7, 8}, - port: 21254, - } - if err := addr.AddTo(m); !errors.Is(err, ErrBadIPLength) { - t.Errorf("AddTo should return %q, got: %v", ErrBadIPLength, err) - } -}*/ - -#[test] -fn test_xormapped_address_string() -> Result<()> { - let tests = vec![ - ( - // 0 - XorMappedAddress { - ip: "fe80::dc2b:44ff:fe20:6009".parse().unwrap(), - port: 124, - }, - "[fe80::dc2b:44ff:fe20:6009]:124", - ), - ( - // 1 - XorMappedAddress { - ip: "213.141.156.236".parse().unwrap(), - port: 8147, - }, - "213.141.156.236:8147", - ), - ]; - - for (addr, ip) in tests { - assert_eq!( - addr.to_string(), - ip, - " XORMappesAddress.String() {addr} (got) != {ip} (expected)", - ); - } - - Ok(()) -} diff --git a/turn/.gitignore b/turn/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/turn/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/turn/Cargo.toml b/turn/Cargo.toml deleted file mode 100644 index 7f54faacb..000000000 --- a/turn/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "turn" -version = "0.8.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of TURN" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/turn" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/turn" - -[dependencies] -util = { version = "0.9.0", path = "../util", package = "webrtc-util", default-features = false, features = ["conn", "vnet"] } -stun = { version = "0.6.0", path = "../stun" } - -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -tokio-util = "0.7" -futures = "0.3" -async-trait = "0.1" -log = "0.4" -base64 = "0.21" -rand = "0.8" -ring = "0.17" -md-5 = "0.10" -thiserror = "1" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -env_logger = "0.10" -chrono = "0.4.28" -hex = "0.4" -clap = "3" -criterion = "0.5" - -[features] -metrics = [] - -[[bench]] -name = "bench" -harness = false - -[[example]] -name = "turn_client_udp" -path = "examples/turn_client_udp.rs" -bench = false - -[[example]] -name = "turn_server_udp" -path = "examples/turn_server_udp.rs" -bench = false diff --git a/turn/LICENSE-APACHE b/turn/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/turn/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/turn/LICENSE-MIT b/turn/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/turn/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/turn/README.md b/turn/README.md deleted file mode 100644 index 13218f8c6..000000000 --- a/turn/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- A pure Rust implementation of TURN. Rewrite Pion TURN in Rust -

diff --git a/turn/benches/bench.rs b/turn/benches/bench.rs deleted file mode 100644 index 48485ba58..000000000 --- a/turn/benches/bench.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::time::Duration; - -use criterion::{criterion_group, criterion_main, Criterion}; -use stun::attributes::ATTR_DATA; -use stun::message::{Getter, Message, Setter}; -use turn::proto::chandata::ChannelData; -use turn::proto::channum::{ChannelNumber, MIN_CHANNEL_NUMBER}; -use turn::proto::data::Data; -use turn::proto::lifetime::Lifetime; - -fn benchmark_chan_data(c: &mut Criterion) { - { - let buf = [64, 0, 0, 0, 0, 4, 0, 0, 1, 2, 3]; - c.bench_function("BenchmarkIsChannelData", |b| { - b.iter(|| { - assert!(ChannelData::is_channel_data(&buf)); - }) - }); - } - - { - let mut d = ChannelData { - data: vec![1, 2, 3, 4], - number: ChannelNumber(MIN_CHANNEL_NUMBER + 1), - raw: vec![], - }; - c.bench_function("BenchmarkChannelData_Encode", |b| { - b.iter(|| { - d.encode(); - d.reset(); - }) - }); - } - - { - let mut d = ChannelData { - data: vec![1, 2, 3, 4], - number: ChannelNumber(MIN_CHANNEL_NUMBER + 1), - raw: vec![], - }; - d.encode(); - let mut buf = vec![0u8; d.raw.len()]; - buf.copy_from_slice(&d.raw); - c.bench_function("BenchmarkChannelData_Decode", |b| { - b.iter(|| { - d.reset(); - d.raw.clone_from(&buf); - d.decode().unwrap(); - }) - }); - } -} - -fn benchmark_chan(c: &mut Criterion) { - { - let mut m = Message::new(); - c.bench_function("BenchmarkChannelNumber/AddTo", |b| { - b.iter(|| { - let n = ChannelNumber(12); - n.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let expected = ChannelNumber(12); - expected.add_to(&mut m).unwrap(); - let mut n = ChannelNumber::default(); - c.bench_function("BenchmarkChannelNumber/GetFrom", |b| { - b.iter(|| { - n.get_from(&m).unwrap(); - assert_eq!(n, expected); - }) - }); - } -} - -fn benchmark_data(c: &mut Criterion) { - { - let mut m = Message::new(); - let d = Data(vec![0u8; 10]); - c.bench_function("BenchmarkData/AddTo", |b| { - b.iter(|| { - d.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let d = Data(vec![0u8; 10]); - c.bench_function("BenchmarkData/AddToRaw", |b| { - b.iter(|| { - m.add(ATTR_DATA, &d.0); - m.reset(); - }) - }); - } -} - -fn benchmark_lifetime(c: &mut Criterion) { - { - let mut m = Message::new(); - let l = Lifetime(Duration::from_secs(1)); - c.bench_function("BenchmarkLifetime/AddTo", |b| { - b.iter(|| { - l.add_to(&mut m).unwrap(); - m.reset(); - }) - }); - } - - { - let mut m = Message::new(); - let expected = Lifetime(Duration::from_secs(60)); - expected.add_to(&mut m).unwrap(); - let mut l = Lifetime::default(); - c.bench_function("BenchmarkLifetime/GetFrom", |b| { - b.iter(|| { - l.get_from(&m).unwrap(); - assert_eq!(l, expected); - }) - }); - } -} - -criterion_group!( - benches, - benchmark_chan_data, - benchmark_chan, - benchmark_data, - benchmark_lifetime -); -criterion_main!(benches); diff --git a/turn/codecov.yml b/turn/codecov.yml deleted file mode 100644 index bf7afa148..000000000 --- a/turn/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 640e45ed-ce83-43e1-9eee-473aa65dc136 - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/turn/doc/webrtc.rs.png b/turn/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z 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 G`0_t~Oa}h| diff --git a/turn/examples/turn_client_udp.rs b/turn/examples/turn_client_udp.rs deleted file mode 100644 index ed71976fd..000000000 --- a/turn/examples/turn_client_udp.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use tokio::time::Duration; -use turn::client::*; -use turn::Error; -use util::Conn; - -// RUST_LOG=trace cargo run --color=always --package turn --example turn_client_udp -- --host 0.0.0.0 --user user=pass --ping - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::init(); - - let mut app = App::new("TURN Client UDP") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of TURN Client UDP") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("host") - .required_unless("FULLHELP") - .takes_value(true) - .long("host") - .help("TURN Server name."), - ) - .arg( - Arg::with_name("user") - .required_unless("FULLHELP") - .takes_value(true) - .long("user") - .help("A pair of username and password (e.g. \"user=pass\")"), - ) - .arg( - Arg::with_name("realm") - .default_value("webrtc.rs") - .takes_value(true) - .long("realm") - .help("Realm (defaults to \"webrtc.rs\")"), - ) - .arg( - Arg::with_name("port") - .takes_value(true) - .default_value("3478") - .long("port") - .help("Listening port."), - ) - .arg( - Arg::with_name("ping") - .long("ping") - .takes_value(false) - .help("Run ping test"), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let host = matches.value_of("host").unwrap(); - let port = matches.value_of("port").unwrap(); - let user = matches.value_of("user").unwrap(); - let cred: Vec<&str> = user.splitn(2, '=').collect(); - let ping = matches.is_present("ping"); - let realm = matches.value_of("realm").unwrap(); - - // TURN client won't create a local listening socket by itself. - let conn = UdpSocket::bind("0.0.0.0:0").await?; - - let turn_server_addr = format!("{host}:{port}"); - - let cfg = ClientConfig { - stun_serv_addr: turn_server_addr.clone(), - turn_serv_addr: turn_server_addr, - username: cred[0].to_string(), - password: cred[1].to_string(), - realm: realm.to_string(), - software: String::new(), - rto_in_ms: 0, - conn: Arc::new(conn), - vnet: None, - }; - - let client = Client::new(cfg).await?; - - // Start listening on the conn provided. - client.listen().await?; - - // Allocate a relay socket on the TURN server. On success, it - // will return a net.PacketConn which represents the remote - // socket. - let relay_conn = client.allocate().await?; - - // The relayConn's local address is actually the transport - // address assigned on the TURN server. - println!("relayed-address={}", relay_conn.local_addr()?); - - // If you provided `-ping`, perform a ping test against the - // relayConn we have just allocated. - if ping { - do_ping_test(&client, relay_conn).await?; - } - - client.close().await?; - - Ok(()) -} - -async fn do_ping_test( - client: &Client, - relay_conn: impl Conn + std::marker::Send + std::marker::Sync + 'static, -) -> Result<(), Error> { - // Send BindingRequest to learn our external IP - let mapped_addr = client.send_binding_request().await?; - - // Set up pinger socket (pingerConn) - //println!("bind..."); - let pinger_conn_tx = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - // Punch a UDP hole for the relay_conn by sending a data to the mapped_addr. - // This will trigger a TURN client to generate a permission request to the - // TURN server. After this, packets from the IP address will be accepted by - // the TURN server. - //println!("relay_conn send hello to mapped_addr {}", mapped_addr); - relay_conn.send_to("Hello".as_bytes(), mapped_addr).await?; - let relay_addr = relay_conn.local_addr()?; - - let pinger_conn_rx = Arc::clone(&pinger_conn_tx); - - // Start read-loop on pingerConn - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - let (n, from) = match pinger_conn_rx.recv_from(&mut buf).await { - Ok((n, from)) => (n, from), - Err(_) => break, - }; - - let msg = match String::from_utf8(buf[..n].to_vec()) { - Ok(msg) => msg, - Err(_) => break, - }; - - println!("pingerConn read-loop: {msg} from {from}"); - /*if sentAt, pingerErr := time.Parse(time.RFC3339Nano, msg); pingerErr == nil { - rtt := time.Since(sentAt) - log.Printf("%d bytes from from %s time=%d ms\n", n, from.String(), int(rtt.Seconds()*1000)) - }*/ - } - }); - - // Start read-loop on relay_conn - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - let (n, from) = match relay_conn.recv_from(&mut buf).await { - Err(_) => break, - Ok((n, from)) => (n, from), - }; - - println!("relay_conn read-loop: {:?} from {}", &buf[..n], from); - - // Echo back - if relay_conn.send_to(&buf[..n], from).await.is_err() { - break; - } - } - }); - - tokio::time::sleep(Duration::from_millis(500)).await; - - /*println!( - "pinger_conn_tx send 10 packets to relay addr {}...", - relay_addr - );*/ - // Send 10 packets from relay_conn to the echo server - for _ in 0..2 { - let msg = "12345678910".to_owned(); //format!("{:?}", tokio::time::Instant::now()); - println!("sending msg={} with size={}", msg, msg.as_bytes().len()); - pinger_conn_tx.send_to(msg.as_bytes(), relay_addr).await?; - - // For simplicity, this example does not wait for the pong (reply). - // Instead, sleep 1 second. - tokio::time::sleep(Duration::from_secs(1)).await; - } - - Ok(()) -} diff --git a/turn/examples/turn_server_udp.rs b/turn/examples/turn_server_udp.rs deleted file mode 100644 index ae8f88c00..000000000 --- a/turn/examples/turn_server_udp.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::collections::HashMap; -use std::net::{IpAddr, SocketAddr}; -use std::str::FromStr; -use std::sync::Arc; - -use clap::{App, AppSettings, Arg}; -use tokio::net::UdpSocket; -use tokio::signal; -use tokio::time::Duration; -use turn::auth::*; -use turn::relay::relay_static::*; -use turn::server::config::*; -use turn::server::*; -use turn::Error; -use util::vnet::net::*; - -struct MyAuthHandler { - cred_map: HashMap>, -} - -impl MyAuthHandler { - fn new(cred_map: HashMap>) -> Self { - MyAuthHandler { cred_map } - } -} - -impl AuthHandler for MyAuthHandler { - fn auth_handle( - &self, - username: &str, - _realm: &str, - _src_addr: SocketAddr, - ) -> Result, Error> { - if let Some(pw) = self.cred_map.get(username) { - //log::debug!("username={}, password={:?}", username, pw); - Ok(pw.to_vec()) - } else { - Err(Error::ErrFakeErr) - } - } -} - -// RUST_LOG=trace cargo run --color=always --package turn --example turn_server_udp -- --public-ip 0.0.0.0 --users user=pass - -#[tokio::main] -async fn main() -> Result<(), Error> { - env_logger::init(); - - let mut app = App::new("TURN Server UDP") - .version("0.1.0") - .author("Rain Liu ") - .about("An example of TURN Server UDP") - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandsNegateReqs) - .arg( - Arg::with_name("FULLHELP") - .help("Prints more detailed help information") - .long("fullhelp"), - ) - .arg( - Arg::with_name("public-ip") - .required_unless("FULLHELP") - .takes_value(true) - .long("public-ip") - .help("IP Address that TURN can be contacted by."), - ) - .arg( - Arg::with_name("users") - .required_unless("FULLHELP") - .takes_value(true) - .long("users") - .help("List of username and password (e.g. \"user=pass,user=pass\")"), - ) - .arg( - Arg::with_name("realm") - .default_value("webrtc.rs") - .takes_value(true) - .long("realm") - .help("Realm (defaults to \"webrtc.rs\")"), - ) - .arg( - Arg::with_name("port") - .takes_value(true) - .default_value("3478") - .long("port") - .help("Listening port."), - ); - - let matches = app.clone().get_matches(); - - if matches.is_present("FULLHELP") { - app.print_long_help().unwrap(); - std::process::exit(0); - } - - let public_ip = matches.value_of("public-ip").unwrap(); - let port = matches.value_of("port").unwrap(); - let users = matches.value_of("users").unwrap(); - let realm = matches.value_of("realm").unwrap(); - - // Cache -users flag for easy lookup later - // If passwords are stored they should be saved to your DB hashed using turn.GenerateAuthKey - let creds: Vec<&str> = users.split(',').collect(); - let mut cred_map = HashMap::new(); - for user in creds { - let cred: Vec<&str> = user.splitn(2, '=').collect(); - let key = generate_auth_key(cred[0], realm, cred[1]); - cred_map.insert(cred[0].to_owned(), key); - } - - // Create a UDP listener to pass into pion/turn - // turn itself doesn't allocate any UDP sockets, but lets the user pass them in - // this allows us to add logging, storage or modify inbound/outbound traffic - let conn = Arc::new(UdpSocket::bind(format!("0.0.0.0:{port}")).await?); - println!("listening {}...", conn.local_addr()?); - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str(public_ip)?, - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - }], - realm: realm.to_owned(), - auth_handler: Arc::new(MyAuthHandler::new(cred_map)), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - println!("Waiting for Ctrl-C..."); - signal::ctrl_c().await.expect("failed to listen for event"); - println!("\nClosing connection now..."); - server.close().await?; - - Ok(()) -} diff --git a/turn/src/allocation/allocation_manager.rs b/turn/src/allocation/allocation_manager.rs deleted file mode 100644 index f3c443689..000000000 --- a/turn/src/allocation/allocation_manager.rs +++ /dev/null @@ -1,198 +0,0 @@ -#[cfg(test)] -mod allocation_manager_test; - -use std::collections::HashMap; - -use futures::future; -use stun::textattrs::Username; -use tokio::sync::mpsc; -use util::Conn; - -use super::*; -use crate::error::*; -use crate::relay::*; - -/// `ManagerConfig` a bag of config params for `Manager`. -pub struct ManagerConfig { - pub relay_addr_generator: Box, - pub alloc_close_notify: Option>, -} - -/// `Manager` is used to hold active allocations. -pub struct Manager { - allocations: AllocationMap, - reservations: Arc>>, - relay_addr_generator: Box, - alloc_close_notify: Option>, -} - -impl Manager { - /// Creates a new [`Manager`]. - pub fn new(config: ManagerConfig) -> Self { - Manager { - allocations: Arc::new(Mutex::new(HashMap::new())), - reservations: Arc::new(Mutex::new(HashMap::new())), - relay_addr_generator: config.relay_addr_generator, - alloc_close_notify: config.alloc_close_notify, - } - } - - /// Closes this [`manager`] and closes all [`Allocation`]s it manages. - pub async fn close(&self) -> Result<()> { - let allocations = self.allocations.lock().await; - for a in allocations.values() { - a.close().await?; - } - Ok(()) - } - - /// Returns the information about the all [`Allocation`]s associated with - /// the specified [`FiveTuple`]s. - pub async fn get_allocations_info( - &self, - five_tuples: Option>, - ) -> HashMap { - let mut infos = HashMap::new(); - - let guarded = self.allocations.lock().await; - - guarded.iter().for_each(|(five_tuple, alloc)| { - if five_tuples.is_none() || five_tuples.as_ref().unwrap().contains(five_tuple) { - infos.insert( - *five_tuple, - AllocationInfo::new( - *five_tuple, - alloc.username.text.clone(), - #[cfg(feature = "metrics")] - alloc.relayed_bytes.load(Ordering::Acquire), - ), - ); - } - }); - - infos - } - - /// Fetches the [`Allocation`] matching the passed [`FiveTuple`]. - pub async fn get_allocation(&self, five_tuple: &FiveTuple) -> Option> { - let allocations = self.allocations.lock().await; - allocations.get(five_tuple).cloned() - } - - /// Creates a new [`Allocation`] and starts relaying. - pub async fn create_allocation( - &self, - five_tuple: FiveTuple, - turn_socket: Arc, - requested_port: u16, - lifetime: Duration, - username: Username, - use_ipv4: bool, - ) -> Result> { - if lifetime == Duration::from_secs(0) { - return Err(Error::ErrLifetimeZero); - } - - if self.get_allocation(&five_tuple).await.is_some() { - return Err(Error::ErrDupeFiveTuple); - } - - let (relay_socket, relay_addr) = self - .relay_addr_generator - .allocate_conn(use_ipv4, requested_port) - .await?; - let mut a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - five_tuple, - username, - self.alloc_close_notify.clone(), - ); - a.allocations = Some(Arc::clone(&self.allocations)); - - log::debug!("listening on relay addr: {:?}", a.relay_addr); - a.start(lifetime).await; - a.packet_handler().await; - - let a = Arc::new(a); - { - let mut allocations = self.allocations.lock().await; - allocations.insert(five_tuple, Arc::clone(&a)); - } - - Ok(a) - } - - /// Removes an [`Allocation`]. - pub async fn delete_allocation(&self, five_tuple: &FiveTuple) { - let allocation = self.allocations.lock().await.remove(five_tuple); - - if let Some(a) = allocation { - if let Err(err) = a.close().await { - log::error!("Failed to close allocation: {}", err); - } - } - } - - /// Deletes the [`Allocation`]s according to the specified username `name`. - pub async fn delete_allocations_by_username(&self, name: &str) { - let to_delete = { - let mut allocations = self.allocations.lock().await; - - let mut to_delete = Vec::new(); - - // TODO(logist322): Use `.drain_filter()` once stabilized. - allocations.retain(|_, allocation| { - let match_name = allocation.username.text == name; - - if match_name { - to_delete.push(Arc::clone(allocation)); - } - - !match_name - }); - - to_delete - }; - - future::join_all(to_delete.iter().map(|a| async move { - if let Err(err) = a.close().await { - log::error!("Failed to close allocation: {}", err); - } - })) - .await; - } - - /// Stores the reservation for the token+port. - pub async fn create_reservation(&self, reservation_token: String, port: u16) { - let reservations = Arc::clone(&self.reservations); - let reservation_token2 = reservation_token.clone(); - - tokio::spawn(async move { - let sleep = tokio::time::sleep(Duration::from_secs(30)); - tokio::pin!(sleep); - tokio::select! { - _ = &mut sleep => { - let mut reservations = reservations.lock().await; - reservations.remove(&reservation_token2); - }, - } - }); - - let mut reservations = self.reservations.lock().await; - reservations.insert(reservation_token, port); - } - - /// Returns the port for a given reservation if it exists. - pub async fn get_reservation(&self, reservation_token: &str) -> Option { - let reservations = self.reservations.lock().await; - reservations.get(reservation_token).copied() - } - - /// Returns a random un-allocated udp4 port. - pub async fn get_random_even_port(&self) -> Result { - let (_, addr) = self.relay_addr_generator.allocate_conn(true, 0).await?; - Ok(addr.port()) - } -} diff --git a/turn/src/allocation/allocation_manager/allocation_manager_test.rs b/turn/src/allocation/allocation_manager/allocation_manager_test.rs deleted file mode 100644 index c994d6e55..000000000 --- a/turn/src/allocation/allocation_manager/allocation_manager_test.rs +++ /dev/null @@ -1,613 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr}; -use std::str::FromStr; - -use stun::attributes::ATTR_USERNAME; -use stun::textattrs::TextAttribute; -use tokio::net::UdpSocket; -use tokio::sync::mpsc::Sender; -use util::vnet::net::*; - -use super::*; -use crate::auth::{generate_auth_key, AuthHandler}; -use crate::client::{Client, ClientConfig}; -use crate::error::Result; -use crate::proto::lifetime::DEFAULT_LIFETIME; -use crate::relay::relay_none::*; -use crate::relay::relay_static::RelayAddressGeneratorStatic; -use crate::server::config::{ConnConfig, ServerConfig}; -use crate::server::Server; - -fn new_test_manager() -> Manager { - let config = ManagerConfig { - relay_addr_generator: Box::new(RelayAddressGeneratorNone { - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - alloc_close_notify: None, - }; - Manager::new(config) -} - -fn random_five_tuple() -> FiveTuple { - /* #nosec */ - FiveTuple { - src_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), rand::random()), - dst_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), rand::random()), - ..Default::default() - } -} - -#[tokio::test] -async fn test_packet_handler() -> Result<()> { - //env_logger::init(); - - // turn server initialization - let turn_socket = UdpSocket::bind("127.0.0.1:0").await?; - - // client listener initialization - let client_listener = UdpSocket::bind("127.0.0.1:0").await?; - let src_addr = client_listener.local_addr()?; - let (data_ch_tx, mut data_ch_rx) = mpsc::channel(1); - // client listener read data - tokio::spawn(async move { - let mut buffer = vec![0u8; RTP_MTU]; - loop { - let n = match client_listener.recv_from(&mut buffer).await { - Ok((n, _)) => n, - Err(_) => break, - }; - - let _ = data_ch_tx.send(buffer[..n].to_vec()).await; - } - }); - - let m = new_test_manager(); - let a = m - .create_allocation( - FiveTuple { - src_addr, - dst_addr: turn_socket.local_addr()?, - ..Default::default() - }, - Arc::new(turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - - let peer_listener1 = UdpSocket::bind("127.0.0.1:0").await?; - let peer_listener2 = UdpSocket::bind("127.0.0.1:0").await?; - - let channel_bind = ChannelBind::new( - ChannelNumber(MIN_CHANNEL_NUMBER), - peer_listener2.local_addr()?, - ); - - let port = { - // add permission with peer1 address - a.add_permission(Permission::new(peer_listener1.local_addr()?)) - .await; - // add channel with min channel number and peer2 address - a.add_channel_bind(channel_bind.clone(), DEFAULT_LIFETIME) - .await?; - - a.relay_socket.local_addr()?.port() - }; - - let relay_addr_with_host_str = format!("127.0.0.1:{port}"); - let relay_addr_with_host = SocketAddr::from_str(&relay_addr_with_host_str)?; - - // test for permission and data message - let target_text = "permission"; - let _ = peer_listener1 - .send_to(target_text.as_bytes(), relay_addr_with_host) - .await?; - let data = data_ch_rx - .recv() - .await - .ok_or(Error::Other("data ch closed".to_owned()))?; - - // resolve stun data message - assert!(is_message(&data), "should be stun message"); - - let mut msg = Message::new(); - msg.raw = data; - msg.decode()?; - - let mut msg_data = Data::default(); - msg_data.get_from(&msg)?; - assert_eq!( - target_text.as_bytes(), - &msg_data.0, - "get message doesn't equal the target text" - ); - - // test for channel bind and channel data - let target_text2 = "channel bind"; - let _ = peer_listener2 - .send_to(target_text2.as_bytes(), relay_addr_with_host) - .await?; - let data = data_ch_rx - .recv() - .await - .ok_or(Error::Other("data ch closed".to_owned()))?; - - // resolve channel data - assert!( - ChannelData::is_channel_data(&data), - "should be channel data" - ); - - let mut channel_data = ChannelData { - raw: data, - ..Default::default() - }; - channel_data.decode()?; - assert_eq!( - channel_bind.number, channel_data.number, - "get channel data's number is invalid" - ); - assert_eq!( - target_text2.as_bytes(), - &channel_data.data, - "get data doesn't equal the target text." - ); - - // listeners close - m.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_create_allocation_duplicate_five_tuple() -> Result<()> { - //env_logger::init(); - - // turn server initialization - let turn_socket: Arc = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let m = new_test_manager(); - - let five_tuple = random_five_tuple(); - - let _ = m - .create_allocation( - five_tuple, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - - let result = m - .create_allocation( - five_tuple, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await; - assert!(result.is_err(), "expected error, but got ok"); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_allocation() -> Result<()> { - //env_logger::init(); - - // turn server initialization - let turn_socket: Arc = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let m = new_test_manager(); - - let five_tuple = random_five_tuple(); - - let _ = m - .create_allocation( - five_tuple, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - - assert!( - m.get_allocation(&five_tuple).await.is_some(), - "Failed to get allocation right after creation" - ); - - m.delete_allocation(&five_tuple).await; - - assert!( - m.get_allocation(&five_tuple).await.is_none(), - "Get allocation with {five_tuple} should be nil after delete" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_allocation_timeout() -> Result<()> { - //env_logger::init(); - - // turn server initialization - let turn_socket: Arc = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let m = new_test_manager(); - - let mut allocations = vec![]; - let lifetime = Duration::from_millis(100); - - for _ in 0..5 { - let five_tuple = random_five_tuple(); - - let a = m - .create_allocation( - five_tuple, - Arc::clone(&turn_socket), - 0, - lifetime, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - - allocations.push(a); - } - - let mut count = 0; - - 'outer: loop { - count += 1; - - if count >= 10 { - panic!("Allocations didn't timeout"); - } - - tokio::time::sleep(lifetime + Duration::from_millis(100)).await; - - let any_outstanding = false; - - for a in &allocations { - if a.close().await.is_ok() { - continue 'outer; - } - } - - if !any_outstanding { - return Ok(()); - } - } -} - -#[tokio::test] -async fn test_manager_close() -> Result<()> { - // env_logger::init(); - - // turn server initialization - let turn_socket: Arc = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let m = new_test_manager(); - - let mut allocations = vec![]; - - let a1 = m - .create_allocation( - random_five_tuple(), - Arc::clone(&turn_socket), - 0, - Duration::from_millis(100), - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - allocations.push(a1); - - let a2 = m - .create_allocation( - random_five_tuple(), - Arc::clone(&turn_socket), - 0, - Duration::from_millis(200), - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - allocations.push(a2); - - tokio::time::sleep(Duration::from_millis(150)).await; - - log::trace!("Mgr is going to be closed..."); - - m.close().await?; - - for a in allocations { - assert!( - a.close().await.is_err(), - "Allocation should be closed if lifetime timeout" - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_delete_allocation_by_username() -> Result<()> { - let turn_socket: Arc = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let m = new_test_manager(); - - let five_tuple1 = random_five_tuple(); - let five_tuple2 = random_five_tuple(); - let five_tuple3 = random_five_tuple(); - - let _ = m - .create_allocation( - five_tuple1, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - let _ = m - .create_allocation( - five_tuple2, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - let _ = m - .create_allocation( - five_tuple3, - Arc::clone(&turn_socket), - 0, - DEFAULT_LIFETIME, - TextAttribute::new(ATTR_USERNAME, "user2".into()), - true, - ) - .await?; - - assert_eq!(m.allocations.lock().await.len(), 3); - - m.delete_allocations_by_username("user").await; - - assert_eq!(m.allocations.lock().await.len(), 1); - - assert!( - m.get_allocation(&five_tuple1).await.is_none() - && m.get_allocation(&five_tuple2).await.is_none() - && m.get_allocation(&five_tuple3).await.is_some() - ); - - Ok(()) -} - -struct TestAuthHandler; -impl AuthHandler for TestAuthHandler { - fn auth_handle(&self, username: &str, realm: &str, _src_addr: SocketAddr) -> Result> { - Ok(generate_auth_key(username, realm, "pass")) - } -} - -async fn create_server( - alloc_close_notify: Option>, -) -> Result<(Server, u16)> { - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let server_port = conn.local_addr()?.port(); - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str("127.0.0.1")?, - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(TestAuthHandler {}), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify, - }) - .await?; - - Ok((server, server_port)) -} - -async fn create_client(username: String, server_port: u16) -> Result { - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - Client::new(ClientConfig { - stun_serv_addr: format!("127.0.0.1:{server_port}"), - turn_serv_addr: format!("127.0.0.1:{server_port}"), - username, - password: "pass".to_owned(), - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn, - vnet: None, - }) - .await -} - -#[cfg(feature = "metrics")] -#[tokio::test] -async fn test_get_allocations_info() -> Result<()> { - let (server, server_port) = create_server(None).await?; - - let client1 = create_client("user1".to_owned(), server_port).await?; - client1.listen().await?; - - let client2 = create_client("user2".to_owned(), server_port).await?; - client2.listen().await?; - - let client3 = create_client("user3".to_owned(), server_port).await?; - client3.listen().await?; - - assert!(server.get_allocations_info(None).await?.is_empty()); - - let user1 = client1.allocate().await?; - let user2 = client2.allocate().await?; - let user3 = client3.allocate().await?; - - assert_eq!(server.get_allocations_info(None).await?.len(), 3); - - let addr1 = client1 - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - let addr2 = client2 - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - let addr3 = client3 - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - - user1.send_to(b"1", addr1).await?; - user2.send_to(b"12", addr2).await?; - user3.send_to(b"123", addr3).await?; - - tokio::time::sleep(Duration::from_millis(100)).await; - - server - .get_allocations_info(None) - .await? - .iter() - .for_each(|(_, ai)| match ai.username.as_str() { - "user1" => assert_eq!(ai.relayed_bytes, 1), - "user2" => assert_eq!(ai.relayed_bytes, 2), - "user3" => assert_eq!(ai.relayed_bytes, 3), - _ => unreachable!(), - }); - - Ok(()) -} - -#[cfg(feature = "metrics")] -#[tokio::test] -async fn test_get_allocations_info_bytes_count() -> Result<()> { - let (server, server_port) = create_server(None).await?; - - let client = create_client("foo".to_owned(), server_port).await?; - - client.listen().await?; - - assert!(server.get_allocations_info(None).await?.is_empty()); - - let conn = client.allocate().await?; - let addr = client - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - - assert!(!server.get_allocations_info(None).await?.is_empty()); - - assert_eq!( - server - .get_allocations_info(None) - .await? - .values() - .last() - .unwrap() - .relayed_bytes, - 0 - ); - - for _ in 0..10 { - conn.send_to(b"Hello", addr).await?; - - tokio::time::sleep(Duration::from_millis(100)).await; - } - - tokio::time::sleep(Duration::from_millis(1000)).await; - - assert_eq!( - server - .get_allocations_info(None) - .await? - .values() - .last() - .unwrap() - .relayed_bytes, - 50 - ); - - for _ in 0..10 { - conn.send_to(b"Hello", addr).await?; - - tokio::time::sleep(Duration::from_millis(100)).await; - } - - tokio::time::sleep(Duration::from_millis(1000)).await; - - assert_eq!( - server - .get_allocations_info(None) - .await? - .values() - .last() - .unwrap() - .relayed_bytes, - 100 - ); - - client.close().await?; - server.close().await?; - - Ok(()) -} - -#[cfg(feature = "metrics")] -#[tokio::test] -async fn test_alloc_close_notify() -> Result<()> { - let (tx, mut rx) = mpsc::channel::(1); - - tokio::spawn(async move { - if let Some(alloc) = rx.recv().await { - assert_eq!(alloc.relayed_bytes, 50); - } - }); - - let (server, server_port) = create_server(Some(tx)).await?; - - let client = create_client("foo".to_owned(), server_port).await?; - - client.listen().await?; - - assert!(server.get_allocations_info(None).await?.is_empty()); - - let conn = client.allocate().await?; - let addr = client - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - - assert!(!server.get_allocations_info(None).await?.is_empty()); - - for _ in 0..10 { - conn.send_to(b"Hello", addr).await?; - - tokio::time::sleep(Duration::from_millis(100)).await; - } - - tokio::time::sleep(Duration::from_millis(1000)).await; - - client.close().await?; - server.close().await?; - - tokio::time::sleep(Duration::from_millis(1000)).await; - - Ok(()) -} diff --git a/turn/src/allocation/allocation_test.rs b/turn/src/allocation/allocation_test.rs deleted file mode 100644 index fc7caf386..000000000 --- a/turn/src/allocation/allocation_test.rs +++ /dev/null @@ -1,296 +0,0 @@ -use std::str::FromStr; - -use stun::attributes::ATTR_USERNAME; -use stun::textattrs::TextAttribute; -use tokio::net::UdpSocket; - -use super::*; -use crate::proto::lifetime::DEFAULT_LIFETIME; - -#[tokio::test] -async fn test_has_permission() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr1 = SocketAddr::from_str("127.0.0.1:3478")?; - let addr2 = SocketAddr::from_str("127.0.0.1:3479")?; - let addr3 = SocketAddr::from_str("127.0.0.2:3478")?; - - let p1 = Permission::new(addr1); - let p2 = Permission::new(addr2); - let p3 = Permission::new(addr3); - - a.add_permission(p1).await; - a.add_permission(p2).await; - a.add_permission(p3).await; - - let found_p1 = a.has_permission(&addr1).await; - assert!(found_p1, "Should keep the first one."); - - let found_p2 = a.has_permission(&addr2).await; - assert!(found_p2, "Second one should be ignored."); - - let found_p3 = a.has_permission(&addr3).await; - assert!(found_p3, "Permission with another IP should be found"); - - Ok(()) -} - -#[tokio::test] -async fn test_add_permission() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let p = Permission::new(addr); - a.add_permission(p).await; - - let found_p = a.has_permission(&addr).await; - assert!(found_p, "Should keep the first one."); - - Ok(()) -} - -#[tokio::test] -async fn test_remove_permission() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - - let p = Permission::new(addr); - a.add_permission(p).await; - - let found_p = a.has_permission(&addr).await; - assert!(found_p, "Should keep the first one."); - - a.remove_permission(&addr).await; - - let found_permission = a.has_permission(&addr).await; - assert!( - !found_permission, - "Got permission should be nil after removed." - ); - - Ok(()) -} - -#[tokio::test] -async fn test_add_channel_bind() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let c = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER), addr); - - a.add_channel_bind(c, DEFAULT_LIFETIME).await?; - - let c2 = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER + 1), addr); - let result = a.add_channel_bind(c2, DEFAULT_LIFETIME).await; - assert!( - result.is_err(), - "should failed with conflicted peer address" - ); - - let addr2 = SocketAddr::from_str("127.0.0.1:3479")?; - let c3 = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER), addr2); - let result = a.add_channel_bind(c3, DEFAULT_LIFETIME).await; - assert!(result.is_err(), "should fail with conflicted number."); - - Ok(()) -} - -#[tokio::test] -async fn test_get_channel_by_number() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let c = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER), addr); - - a.add_channel_bind(c, DEFAULT_LIFETIME).await?; - - let exist_channel_addr = a - .get_channel_addr(&ChannelNumber(MIN_CHANNEL_NUMBER)) - .await - .unwrap(); - assert_eq!(addr, exist_channel_addr); - - let not_exist_channel = a - .get_channel_addr(&ChannelNumber(MIN_CHANNEL_NUMBER + 1)) - .await; - assert!( - not_exist_channel.is_none(), - "should be nil for not existed channel." - ); - - Ok(()) -} - -#[tokio::test] -async fn test_get_channel_by_addr() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let addr2 = SocketAddr::from_str("127.0.0.1:3479")?; - let c = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER), addr); - - a.add_channel_bind(c, DEFAULT_LIFETIME).await?; - - let exist_channel_number = a.get_channel_number(&addr).await.unwrap(); - assert_eq!(ChannelNumber(MIN_CHANNEL_NUMBER), exist_channel_number); - - let not_exist_channel = a.get_channel_number(&addr2).await; - assert!( - not_exist_channel.is_none(), - "should be nil for not existed channel." - ); - - Ok(()) -} - -#[tokio::test] -async fn test_remove_channel_bind() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let number = ChannelNumber(MIN_CHANNEL_NUMBER); - let c = ChannelBind::new(number, addr); - - a.add_channel_bind(c, DEFAULT_LIFETIME).await?; - - a.remove_channel_bind(number).await; - - let not_exist_channel = a.get_channel_addr(&number).await; - assert!( - not_exist_channel.is_none(), - "should be nil for not existed channel." - ); - - let not_exist_channel = a.get_channel_number(&addr).await; - assert!( - not_exist_channel.is_none(), - "should be nil for not existed channel." - ); - - Ok(()) -} - -#[tokio::test] -async fn test_allocation_refresh() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - a.start(DEFAULT_LIFETIME).await; - a.refresh(Duration::from_secs(0)).await; - - assert!(!a.stop(), "lifetimeTimer has expired"); - - Ok(()) -} - -#[tokio::test] -async fn test_allocation_close() -> Result<()> { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - // add mock lifetimeTimer - a.start(DEFAULT_LIFETIME).await; - - // add channel - let addr = SocketAddr::from_str("127.0.0.1:3478")?; - let number = ChannelNumber(MIN_CHANNEL_NUMBER); - let c = ChannelBind::new(number, addr); - - a.add_channel_bind(c, DEFAULT_LIFETIME).await?; - - // add permission - a.add_permission(Permission::new(addr)).await; - - a.close().await?; - - Ok(()) -} diff --git a/turn/src/allocation/channel_bind.rs b/turn/src/allocation/channel_bind.rs deleted file mode 100644 index e613d07d4..000000000 --- a/turn/src/allocation/channel_bind.rs +++ /dev/null @@ -1,87 +0,0 @@ -#[cfg(test)] -mod channel_bind_test; - -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicBool; -use tokio::sync::Mutex; -use tokio::time::{Duration, Instant}; - -use super::*; -use crate::proto::channum::*; - -/// `ChannelBind` represents a TURN Channel. -/// -/// https://tools.ietf.org/html/rfc5766#section-2.5. -#[derive(Clone)] -pub struct ChannelBind { - pub(crate) peer: SocketAddr, - pub(crate) number: ChannelNumber, - pub(crate) channel_bindings: Option>>>, - reset_tx: Option>, - timer_expired: Arc, -} - -impl ChannelBind { - /// Creates a new [`ChannelBind`] - pub fn new(number: ChannelNumber, peer: SocketAddr) -> Self { - ChannelBind { - number, - peer, - channel_bindings: None, - reset_tx: None, - timer_expired: Arc::new(AtomicBool::new(false)), - } - } - - pub(crate) async fn start(&mut self, lifetime: Duration) { - let (reset_tx, mut reset_rx) = mpsc::channel(1); - self.reset_tx = Some(reset_tx); - - let channel_bindings = self.channel_bindings.clone(); - let number = self.number; - let timer_expired = Arc::clone(&self.timer_expired); - - tokio::spawn(async move { - let timer = tokio::time::sleep(lifetime); - tokio::pin!(timer); - let mut done = false; - - while !done { - tokio::select! { - _ = &mut timer => { - if let Some(cbs) = &channel_bindings{ - let mut cb = cbs.lock().await; - if cb.remove(&number).is_none() { - log::error!("Failed to remove ChannelBind for {}", number); - } - } - done = true; - }, - result = reset_rx.recv() => { - if let Some(d) = result { - timer.as_mut().reset(Instant::now() + d); - } else { - done = true; - } - }, - } - } - - timer_expired.store(true, Ordering::SeqCst); - }); - } - - pub(crate) fn stop(&mut self) -> bool { - let expired = self.reset_tx.is_none() || self.timer_expired.load(Ordering::SeqCst); - self.reset_tx.take(); - expired - } - - pub(crate) async fn refresh(&self, lifetime: Duration) { - if let Some(tx) = &self.reset_tx { - let _ = tx.send(lifetime).await; - } - } -} diff --git a/turn/src/allocation/channel_bind/channel_bind_test.rs b/turn/src/allocation/channel_bind/channel_bind_test.rs deleted file mode 100644 index 365c3744d..000000000 --- a/turn/src/allocation/channel_bind/channel_bind_test.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::net::Ipv4Addr; - -use stun::attributes::ATTR_USERNAME; -use stun::textattrs::TextAttribute; -use tokio::net::UdpSocket; - -use super::*; -use crate::allocation::*; -use crate::error::Result; - -async fn create_channel_bind(lifetime: Duration) -> Result { - let turn_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let relay_socket = Arc::clone(&turn_socket); - let relay_addr = relay_socket.local_addr()?; - let a = Allocation::new( - turn_socket, - relay_socket, - relay_addr, - FiveTuple::default(), - TextAttribute::new(ATTR_USERNAME, "user".into()), - None, - ); - - let addr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0); - let c = ChannelBind::new(ChannelNumber(MIN_CHANNEL_NUMBER), addr); - - a.add_channel_bind(c, lifetime).await?; - - Ok(a) -} - -#[tokio::test] -async fn test_channel_bind() -> Result<()> { - let a = create_channel_bind(Duration::from_millis(20)).await?; - - let result = a.get_channel_addr(&ChannelNumber(MIN_CHANNEL_NUMBER)).await; - if let Some(addr) = result { - assert_eq!(addr.ip().to_string(), "0.0.0.0"); - } else { - panic!("expected some, but got none"); - } - - Ok(()) -} - -async fn test_channel_bind_start() -> Result<()> { - let a = create_channel_bind(Duration::from_millis(20)).await?; - tokio::time::sleep(Duration::from_millis(30)).await; - - assert!(a - .get_channel_addr(&ChannelNumber(MIN_CHANNEL_NUMBER)) - .await - .is_none()); - - Ok(()) -} - -async fn test_channel_bind_reset() -> Result<()> { - let a = create_channel_bind(Duration::from_millis(30)).await?; - - tokio::time::sleep(Duration::from_millis(20)).await; - { - let channel_bindings = a.channel_bindings.lock().await; - if let Some(c) = channel_bindings.get(&ChannelNumber(MIN_CHANNEL_NUMBER)) { - c.refresh(Duration::from_millis(30)).await; - } - } - tokio::time::sleep(Duration::from_millis(20)).await; - - assert!(a - .get_channel_addr(&ChannelNumber(MIN_CHANNEL_NUMBER)) - .await - .is_some()); - - Ok(()) -} diff --git a/turn/src/allocation/five_tuple.rs b/turn/src/allocation/five_tuple.rs deleted file mode 100644 index d28eb48ff..000000000 --- a/turn/src/allocation/five_tuple.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[cfg(test)] -mod five_tuple_test; - -use std::fmt; -use std::net::{Ipv4Addr, SocketAddr}; - -use crate::proto::*; - -/// `FiveTuple` is the combination (client IP address and port, server IP -/// address and port, and transport protocol (currently one of UDP, -/// TCP, or TLS)) used to communicate between the client and the -/// server. The 5-tuple uniquely identifies this communication -/// stream. The 5-tuple also uniquely identifies the Allocation on -/// the server. -#[derive(PartialEq, Eq, Clone, Copy, Hash)] -pub struct FiveTuple { - pub protocol: Protocol, - pub src_addr: SocketAddr, - pub dst_addr: SocketAddr, -} - -impl Default for FiveTuple { - fn default() -> Self { - FiveTuple { - protocol: PROTO_UDP, - src_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), - dst_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), - } - } -} - -impl fmt::Display for FiveTuple { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}_{}_{}", self.protocol, self.src_addr, self.dst_addr) - } -} - -impl fmt::Debug for FiveTuple { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FiveTuple") - .field("protocol", &self.protocol) - .field("src_addr", &self.src_addr) - .field("dst_addr", &self.dst_addr) - .finish() - } -} diff --git a/turn/src/allocation/five_tuple/five_tuple_test.rs b/turn/src/allocation/five_tuple/five_tuple_test.rs deleted file mode 100644 index 3e33e5d52..000000000 --- a/turn/src/allocation/five_tuple/five_tuple_test.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_five_tuple_protocol() -> Result<()> { - let udp_expect = PROTO_UDP; - let tcp_expect = PROTO_TCP; - - assert_eq!( - udp_expect, PROTO_UDP, - "Invalid UDP Protocol value, expect {udp_expect} but {PROTO_UDP}" - ); - assert_eq!( - tcp_expect, PROTO_TCP, - "Invalid TCP Protocol value, expect {tcp_expect} but {PROTO_TCP}" - ); - - assert_eq!(udp_expect.to_string(), "UDP"); - assert_eq!(tcp_expect.to_string(), "TCP"); - - Ok(()) -} - -#[test] -fn test_five_tuple_equal() -> Result<()> { - let src_addr1: SocketAddr = "0.0.0.0:3478".parse::()?; - let src_addr2: SocketAddr = "0.0.0.0:3479".parse::()?; - - let dst_addr1: SocketAddr = "0.0.0.0:3480".parse::()?; - let dst_addr2: SocketAddr = "0.0.0.0:3481".parse::()?; - - let tests = vec![ - ( - "Equal", - true, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - ), - ( - "DifferentProtocol", - false, - FiveTuple { - protocol: PROTO_TCP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - ), - ( - "DifferentSrcAddr", - false, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr2, - dst_addr: dst_addr1, - }, - ), - ( - "DifferentDstAddr", - false, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr1, - }, - FiveTuple { - protocol: PROTO_UDP, - src_addr: src_addr1, - dst_addr: dst_addr2, - }, - ), - ]; - - for (name, expect, a, b) in tests { - let fact = a == b; - assert_eq!( - expect, fact, - "{name}: {a}, {b} equal check should be {expect}, but {fact}" - ); - } - - Ok(()) -} diff --git a/turn/src/allocation/mod.rs b/turn/src/allocation/mod.rs deleted file mode 100644 index b8b758bd6..000000000 --- a/turn/src/allocation/mod.rs +++ /dev/null @@ -1,468 +0,0 @@ -#[cfg(test)] -mod allocation_test; - -pub mod allocation_manager; -pub mod channel_bind; -pub mod five_tuple; -pub mod permission; - -use std::collections::HashMap; -use std::marker::{Send, Sync}; -use std::net::SocketAddr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use channel_bind::*; -use five_tuple::*; -use permission::*; -use portable_atomic::{AtomicBool, AtomicUsize}; -use stun::agent::*; -use stun::message::*; -use stun::textattrs::Username; -use tokio::sync::oneshot::{self, Sender}; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::{Duration, Instant}; -use util::sync::Mutex as SyncMutex; -use util::Conn; - -use crate::error::*; -use crate::proto::chandata::*; -use crate::proto::channum::*; -use crate::proto::data::*; -use crate::proto::peeraddr::*; -use crate::proto::*; - -const RTP_MTU: usize = 1500; - -pub type AllocationMap = Arc>>>; - -/// Information about an [`Allocation`]. -#[derive(Debug, Clone)] -pub struct AllocationInfo { - /// [`FiveTuple`] of this [`Allocation`]. - pub five_tuple: FiveTuple, - - /// Username of this [`Allocation`]. - pub username: String, - - /// Relayed bytes with this [`Allocation`]. - #[cfg(feature = "metrics")] - pub relayed_bytes: usize, -} - -impl AllocationInfo { - /// Creates a new [`AllocationInfo`]. - pub fn new( - five_tuple: FiveTuple, - username: String, - #[cfg(feature = "metrics")] relayed_bytes: usize, - ) -> Self { - Self { - five_tuple, - username, - #[cfg(feature = "metrics")] - relayed_bytes, - } - } -} - -/// `Allocation` is tied to a FiveTuple and relays traffic -/// use create_allocation and get_allocation to operate. -pub struct Allocation { - protocol: Protocol, - turn_socket: Arc, - pub(crate) relay_addr: SocketAddr, - pub(crate) relay_socket: Arc, - five_tuple: FiveTuple, - username: Username, - permissions: Arc>>, - channel_bindings: Arc>>, - pub(crate) allocations: Option, - reset_tx: SyncMutex>>, - timer_expired: Arc, - closed: AtomicBool, // Option>, - pub(crate) relayed_bytes: AtomicUsize, - drop_tx: Option>, - alloc_close_notify: Option>, -} - -fn addr2ipfingerprint(addr: &SocketAddr) -> String { - addr.ip().to_string() -} - -impl Allocation { - /// Creates a new [`Allocation`]. - pub fn new( - turn_socket: Arc, - relay_socket: Arc, - relay_addr: SocketAddr, - five_tuple: FiveTuple, - username: Username, - alloc_close_notify: Option>, - ) -> Self { - Allocation { - protocol: PROTO_UDP, - turn_socket, - relay_addr, - relay_socket, - five_tuple, - username, - permissions: Arc::new(Mutex::new(HashMap::new())), - channel_bindings: Arc::new(Mutex::new(HashMap::new())), - allocations: None, - reset_tx: SyncMutex::new(None), - timer_expired: Arc::new(AtomicBool::new(false)), - closed: AtomicBool::new(false), - relayed_bytes: Default::default(), - drop_tx: None, - alloc_close_notify, - } - } - - /// Checks the Permission for the `addr`. - pub async fn has_permission(&self, addr: &SocketAddr) -> bool { - let permissions = self.permissions.lock().await; - permissions.get(&addr2ipfingerprint(addr)).is_some() - } - - /// Adds a new [`Permission`] to this [`Allocation`]. - pub async fn add_permission(&self, mut p: Permission) { - let fingerprint = addr2ipfingerprint(&p.addr); - - { - let permissions = self.permissions.lock().await; - if let Some(existed_permission) = permissions.get(&fingerprint) { - existed_permission.refresh(PERMISSION_TIMEOUT).await; - return; - } - } - - p.permissions = Some(Arc::clone(&self.permissions)); - p.start(PERMISSION_TIMEOUT).await; - - { - let mut permissions = self.permissions.lock().await; - permissions.insert(fingerprint, p); - } - } - - /// Removes the `addr`'s fingerprint from this [`Allocation`]'s permissions. - pub async fn remove_permission(&self, addr: &SocketAddr) -> bool { - let mut permissions = self.permissions.lock().await; - permissions.remove(&addr2ipfingerprint(addr)).is_some() - } - - /// Adds a new [`ChannelBind`] to this [`Allocation`], it also updates the - /// permissions needed for this [`ChannelBind`]. - pub async fn add_channel_bind(&self, mut c: ChannelBind, lifetime: Duration) -> Result<()> { - { - if let Some(addr) = self.get_channel_addr(&c.number).await { - if addr != c.peer { - return Err(Error::ErrSameChannelDifferentPeer); - } - } - - if let Some(number) = self.get_channel_number(&c.peer).await { - if number != c.number { - return Err(Error::ErrSameChannelDifferentPeer); - } - } - } - - { - let channel_bindings = self.channel_bindings.lock().await; - if let Some(cb) = channel_bindings.get(&c.number) { - cb.refresh(lifetime).await; - - // Channel binds also refresh permissions. - self.add_permission(Permission::new(cb.peer)).await; - - return Ok(()); - } - } - - let peer = c.peer; - - // Add or refresh this channel. - c.channel_bindings = Some(Arc::clone(&self.channel_bindings)); - c.start(lifetime).await; - - { - let mut channel_bindings = self.channel_bindings.lock().await; - channel_bindings.insert(c.number, c); - } - - // Channel binds also refresh permissions. - self.add_permission(Permission::new(peer)).await; - - Ok(()) - } - - /// Removes the [`ChannelBind`] from this [`Allocation`] by `number`. - pub async fn remove_channel_bind(&self, number: ChannelNumber) -> bool { - let mut channel_bindings = self.channel_bindings.lock().await; - channel_bindings.remove(&number).is_some() - } - - /// Gets the [`ChannelBind`]'s address by `number`. - pub async fn get_channel_addr(&self, number: &ChannelNumber) -> Option { - let channel_bindings = self.channel_bindings.lock().await; - channel_bindings.get(number).map(|cb| cb.peer) - } - - /// Gets the [`ChannelBind`]'s number from this [`Allocation`] by `addr`. - pub async fn get_channel_number(&self, addr: &SocketAddr) -> Option { - let channel_bindings = self.channel_bindings.lock().await; - for cb in channel_bindings.values() { - if cb.peer == *addr { - return Some(cb.number); - } - } - None - } - - /// Closes the [`Allocation`]. - pub async fn close(&self) -> Result<()> { - if self.closed.load(Ordering::Acquire) { - return Err(Error::ErrClosed); - } - - self.closed.store(true, Ordering::Release); - self.stop(); - - { - let mut permissions = self.permissions.lock().await; - for p in permissions.values_mut() { - p.stop(); - } - } - - { - let mut channel_bindings = self.channel_bindings.lock().await; - for c in channel_bindings.values_mut() { - c.stop(); - } - } - - log::trace!("allocation with {} closed!", self.five_tuple); - - let _ = self.turn_socket.close().await; - let _ = self.relay_socket.close().await; - - if let Some(notify_tx) = &self.alloc_close_notify { - let _ = notify_tx - .send(AllocationInfo { - five_tuple: self.five_tuple, - username: self.username.text.clone(), - #[cfg(feature = "metrics")] - relayed_bytes: self.relayed_bytes.load(Ordering::Acquire), - }) - .await; - } - - Ok(()) - } - - pub async fn start(&self, lifetime: Duration) { - let (reset_tx, mut reset_rx) = mpsc::channel(1); - self.reset_tx.lock().replace(reset_tx); - - let allocations = self.allocations.clone(); - let five_tuple = self.five_tuple; - let timer_expired = Arc::clone(&self.timer_expired); - - tokio::spawn(async move { - let timer = tokio::time::sleep(lifetime); - tokio::pin!(timer); - let mut done = false; - - while !done { - tokio::select! { - _ = &mut timer => { - if let Some(allocs) = &allocations{ - let mut allocs = allocs.lock().await; - if let Some(a) = allocs.remove(&five_tuple) { - let _ = a.close().await; - } - } - done = true; - }, - result = reset_rx.recv() => { - if let Some(d) = result { - timer.as_mut().reset(Instant::now() + d); - } else { - done = true; - } - }, - } - } - - timer_expired.store(true, Ordering::SeqCst); - }); - } - - fn stop(&self) -> bool { - let reset_tx = self.reset_tx.lock().take(); - reset_tx.is_none() || self.timer_expired.load(Ordering::SeqCst) - } - - /// Updates the allocations lifetime. - pub async fn refresh(&self, lifetime: Duration) { - let reset_tx = self.reset_tx.lock().clone(); - if let Some(tx) = reset_tx { - let _ = tx.send(lifetime).await; - } - } - - // https://tools.ietf.org/html/rfc5766#section-10.3 - // When the server receives a UDP datagram at a currently allocated - // relayed transport address, the server looks up the allocation - // associated with the relayed transport address. The server then - // checks to see whether the set of permissions for the allocation allow - // the relaying of the UDP datagram as described in Section 8. - // - // If relaying is permitted, then the server checks if there is a - // channel bound to the peer that sent the UDP datagram (see - // Section 11). If a channel is bound, then processing proceeds as - // described in Section 11.7. - // - // If relaying is permitted but no channel is bound to the peer, then - // the server forms and sends a Data indication. The Data indication - // MUST contain both an XOR-PEER-ADDRESS and a DATA attribute. The DATA - // attribute is set to the value of the 'data octets' field from the - // datagram, and the XOR-PEER-ADDRESS attribute is set to the source - // transport address of the received UDP datagram. The Data indication - // is then sent on the 5-tuple associated with the allocation. - async fn packet_handler(&mut self) { - let five_tuple = self.five_tuple; - let relay_addr = self.relay_addr; - let relay_socket = Arc::clone(&self.relay_socket); - let turn_socket = Arc::clone(&self.turn_socket); - let allocations = self.allocations.clone(); - let channel_bindings = Arc::clone(&self.channel_bindings); - let permissions = Arc::clone(&self.permissions); - let (drop_tx, drop_rx) = oneshot::channel::(); - self.drop_tx = Some(drop_tx); - - tokio::spawn(async move { - let mut buffer = vec![0u8; RTP_MTU]; - - tokio::pin!(drop_rx); - - loop { - let (n, src_addr) = tokio::select! { - result = relay_socket.recv_from(&mut buffer) => { - match result { - Ok((n, src_addr)) => (n, src_addr), - Err(_) => { - if let Some(allocs) = &allocations { - let mut allocs = allocs.lock().await; - allocs.remove(&five_tuple); - } - break; - } - } - } - _ = drop_rx.as_mut() => { - log::trace!("allocation has stopped, stop packet_handler. five_tuple: {:?}", five_tuple); - break; - } - }; - - log::debug!( - "relay socket {:?} received {} bytes from {}", - relay_socket.local_addr(), - n, - src_addr - ); - - let cb_number = { - let mut cb_number = None; - let cbs = channel_bindings.lock().await; - for cb in cbs.values() { - if cb.peer == src_addr { - cb_number = Some(cb.number); - break; - } - } - cb_number - }; - - if let Some(number) = cb_number { - let mut channel_data = ChannelData { - data: buffer[..n].to_vec(), - number, - raw: vec![], - }; - channel_data.encode(); - - if let Err(err) = turn_socket - .send_to(&channel_data.raw, five_tuple.src_addr) - .await - { - log::error!( - "Failed to send ChannelData from allocation {} {}", - src_addr, - err - ); - } - } else { - let exist = { - let ps = permissions.lock().await; - ps.get(&addr2ipfingerprint(&src_addr)).is_some() - }; - - if exist { - let msg = { - let peer_address_attr = PeerAddress { - ip: src_addr.ip(), - port: src_addr.port(), - }; - let data_attr = Data(buffer[..n].to_vec()); - - let mut msg = Message::new(); - if let Err(err) = msg.build(&[ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_DATA, CLASS_INDICATION)), - Box::new(peer_address_attr), - Box::new(data_attr), - ]) { - log::error!( - "Failed to send DataIndication from allocation {} {}", - src_addr, - err - ); - None - } else { - Some(msg) - } - }; - - if let Some(msg) = msg { - log::debug!( - "relaying message from {} to client at {}", - src_addr, - five_tuple.src_addr - ); - if let Err(err) = - turn_socket.send_to(&msg.raw, five_tuple.src_addr).await - { - log::error!( - "Failed to send DataIndication from allocation {} {}", - src_addr, - err - ); - } - } - } else { - log::info!( - "No Permission or Channel exists for {} on allocation {}", - src_addr, - relay_addr - ); - } - } - } - }); - } -} diff --git a/turn/src/allocation/permission.rs b/turn/src/allocation/permission.rs deleted file mode 100644 index 08013523b..000000000 --- a/turn/src/allocation/permission.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicBool; -use tokio::sync::Mutex; -use tokio::time::{Duration, Instant}; - -use super::*; - -pub(crate) const PERMISSION_TIMEOUT: Duration = Duration::from_secs(5 * 60); - -/// `Permission` represents a TURN permission. TURN permissions mimic the address-restricted -/// filtering mechanism of NATs that comply with [RFC4787]. -/// -/// https://tools.ietf.org/html/rfc5766#section-2.3 -pub struct Permission { - pub(crate) addr: SocketAddr, - pub(crate) permissions: Option>>>, - reset_tx: Option>, - timer_expired: Arc, -} - -impl Permission { - /// Creates a new [`Permission`]. - pub fn new(addr: SocketAddr) -> Self { - Permission { - addr, - permissions: None, - reset_tx: None, - timer_expired: Arc::new(AtomicBool::new(false)), - } - } - - pub(crate) async fn start(&mut self, lifetime: Duration) { - let (reset_tx, mut reset_rx) = mpsc::channel(1); - self.reset_tx = Some(reset_tx); - - let permissions = self.permissions.clone(); - let addr = self.addr; - let timer_expired = Arc::clone(&self.timer_expired); - - tokio::spawn(async move { - let timer = tokio::time::sleep(lifetime); - tokio::pin!(timer); - let mut done = false; - - while !done { - tokio::select! { - _ = &mut timer => { - if let Some(perms) = &permissions{ - let mut p = perms.lock().await; - p.remove(&addr2ipfingerprint(&addr)); - } - done = true; - }, - result = reset_rx.recv() => { - if let Some(d) = result { - timer.as_mut().reset(Instant::now() + d); - } else { - done = true; - } - }, - } - } - - timer_expired.store(true, Ordering::SeqCst); - }); - } - - pub(crate) fn stop(&mut self) -> bool { - let expired = self.reset_tx.is_none() || self.timer_expired.load(Ordering::SeqCst); - self.reset_tx.take(); - expired - } - - pub(crate) async fn refresh(&self, lifetime: Duration) { - if let Some(tx) = &self.reset_tx { - let _ = tx.send(lifetime).await; - } - } -} diff --git a/turn/src/auth/auth_test.rs b/turn/src/auth/auth_test.rs deleted file mode 100644 index df1e51d8b..000000000 --- a/turn/src/auth/auth_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::*; - -#[test] -fn test_lt_cred() -> Result<()> { - let username = "1599491771"; - let shared_secret = "foobar"; - - let expected_password = "Tpz/nKkyvX/vMSLKvL4sbtBt8Vs="; - let actual_password = long_term_credentials(username, shared_secret); - assert_eq!( - expected_password, actual_password, - "Expected {expected_password}, got {actual_password}" - ); - - Ok(()) -} - -#[test] -fn test_generate_auth_key() -> Result<()> { - let username = "60"; - let password = "HWbnm25GwSj6jiHTEDMTO5D7aBw="; - let realm = "webrtc.rs"; - - let expected_key = vec![ - 56, 22, 47, 139, 198, 127, 13, 188, 171, 80, 23, 29, 195, 148, 216, 224, - ]; - let actual_key = generate_auth_key(username, realm, password); - assert_eq!( - expected_key, actual_key, - "Expected {expected_key:?}, got {actual_key:?}" - ); - - Ok(()) -} - -#[cfg(target_family = "unix")] -#[tokio::test] -async fn test_new_long_term_auth_handler() -> Result<()> { - use std::net::IpAddr; - use std::str::FromStr; - use std::sync::Arc; - - use tokio::net::UdpSocket; - use util::vnet::net::*; - - use crate::client::*; - use crate::relay::relay_static::*; - use crate::server::config::*; - use crate::server::*; - - //env_logger::init(); - - const SHARED_SECRET: &str = "HELLO_WORLD"; - - // here, it should use static port, like "0.0.0.0:3478", - // but, due to different test environment, let's fake it by using "0.0.0.0:0" - // to auto assign a "static" port - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let server_port = conn.local_addr()?.port(); - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str("127.0.0.1")?, - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(LongTermAuthHandler::new(SHARED_SECRET.to_string())), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - let (username, password) = - generate_long_term_credentials(SHARED_SECRET, Duration::from_secs(60))?; - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let client = Client::new(ClientConfig { - stun_serv_addr: format!("0.0.0.0:{server_port}"), - turn_serv_addr: format!("0.0.0.0:{server_port}"), - username, - password, - realm: "webrtc.rs".to_owned(), - software: String::new(), - rto_in_ms: 0, - conn, - vnet: None, - }) - .await?; - - client.listen().await?; - - let _allocation = client.allocate().await?; - - client.close().await?; - server.close().await?; - - Ok(()) -} diff --git a/turn/src/auth/mod.rs b/turn/src/auth/mod.rs deleted file mode 100644 index 537983d7d..000000000 --- a/turn/src/auth/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -#[cfg(test)] -mod auth_test; - -use std::net::SocketAddr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use md5::{Digest, Md5}; -use ring::hmac; - -use crate::error::*; - -pub trait AuthHandler { - fn auth_handle(&self, username: &str, realm: &str, src_addr: SocketAddr) -> Result>; -} - -/// `generate_long_term_credentials()` can be used to create credentials valid for `duration` time/ -pub fn generate_long_term_credentials( - shared_secret: &str, - duration: Duration, -) -> Result<(String, String)> { - let t = SystemTime::now().duration_since(UNIX_EPOCH)? + duration; - let username = format!("{}", t.as_secs()); - let password = long_term_credentials(&username, shared_secret); - Ok((username, password)) -} - -fn long_term_credentials(username: &str, shared_secret: &str) -> String { - let mac = hmac::Key::new( - hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, - shared_secret.as_bytes(), - ); - let password = hmac::sign(&mac, username.as_bytes()).as_ref().to_vec(); - BASE64_STANDARD.encode(password) -} - -/// A convenience function to easily generate keys in the format used by [`AuthHandler`]. -pub fn generate_auth_key(username: &str, realm: &str, password: &str) -> Vec { - let s = format!("{username}:{realm}:{password}"); - - let mut h = Md5::new(); - h.update(s.as_bytes()); - h.finalize().as_slice().to_vec() -} - -pub struct LongTermAuthHandler { - shared_secret: String, -} - -impl AuthHandler for LongTermAuthHandler { - fn auth_handle(&self, username: &str, realm: &str, src_addr: SocketAddr) -> Result> { - log::trace!( - "Authentication username={} realm={} src_addr={}", - username, - realm, - src_addr - ); - - let t = Duration::from_secs(username.parse::()?); - if t < SystemTime::now().duration_since(UNIX_EPOCH)? { - return Err(Error::Other(format!( - "Expired time-windowed username {username}" - ))); - } - - let password = long_term_credentials(username, &self.shared_secret); - Ok(generate_auth_key(username, realm, &password)) - } -} - -impl LongTermAuthHandler { - /// https://tools.ietf.org/search/rfc5389#section-10.2 - pub fn new(shared_secret: String) -> Self { - LongTermAuthHandler { shared_secret } - } -} diff --git a/turn/src/client/binding.rs b/turn/src/client/binding.rs deleted file mode 100644 index 2de2a03cb..000000000 --- a/turn/src/client/binding.rs +++ /dev/null @@ -1,136 +0,0 @@ -#[cfg(test)] -mod binding_test; - -use std::collections::HashMap; -use std::net::SocketAddr; - -use tokio::time::Instant; - -// Channel number: -// 0x4000 through 0x7FFF: These values are the allowed channel -// numbers (16,383 possible values). -const MIN_CHANNEL_NUMBER: u16 = 0x4000; -const MAX_CHANNEL_NUMBER: u16 = 0x7fff; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum BindingState { - Idle, - Request, - Ready, - Refresh, - Failed, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) struct Binding { - pub(crate) number: u16, - pub(crate) st: BindingState, - pub(crate) addr: SocketAddr, - pub(crate) refreshed_at: Instant, -} - -impl Binding { - pub(crate) fn set_state(&mut self, state: BindingState) { - //atomic.StoreInt32((*int32)(&b.st), int32(state)) - self.st = state; - } - - pub(crate) fn state(&self) -> BindingState { - //return BindingState(atomic.LoadInt32((*int32)(&b.st))) - self.st - } - - pub(crate) fn set_refreshed_at(&mut self, at: Instant) { - self.refreshed_at = at; - } - - pub(crate) fn refreshed_at(&self) -> Instant { - self.refreshed_at - } -} -/// Thread-safe Binding map. -#[derive(Default)] -pub(crate) struct BindingManager { - chan_map: HashMap, - addr_map: HashMap, - next: u16, -} - -impl BindingManager { - pub(crate) fn new() -> Self { - BindingManager { - chan_map: HashMap::new(), - addr_map: HashMap::new(), - next: MIN_CHANNEL_NUMBER, - } - } - - pub(crate) fn assign_channel_number(&mut self) -> u16 { - let n = self.next; - if self.next == MAX_CHANNEL_NUMBER { - self.next = MIN_CHANNEL_NUMBER; - } else { - self.next += 1; - } - n - } - - pub(crate) fn create(&mut self, addr: SocketAddr) -> Option<&Binding> { - let b = Binding { - number: self.assign_channel_number(), - st: BindingState::Idle, - addr, - refreshed_at: Instant::now(), - }; - - self.chan_map.insert(b.number, b.addr.to_string()); - self.addr_map.insert(b.addr.to_string(), b); - self.addr_map.get(&addr.to_string()) - } - - pub(crate) fn find_by_addr(&self, addr: &SocketAddr) -> Option<&Binding> { - self.addr_map.get(&addr.to_string()) - } - - pub(crate) fn get_by_addr(&mut self, addr: &SocketAddr) -> Option<&mut Binding> { - self.addr_map.get_mut(&addr.to_string()) - } - - pub(crate) fn find_by_number(&self, number: u16) -> Option<&Binding> { - if let Some(s) = self.chan_map.get(&number) { - self.addr_map.get(s) - } else { - None - } - } - - pub(crate) fn get_by_number(&mut self, number: u16) -> Option<&mut Binding> { - if let Some(s) = self.chan_map.get(&number) { - self.addr_map.get_mut(s) - } else { - None - } - } - - pub(crate) fn delete_by_addr(&mut self, addr: &SocketAddr) -> bool { - if let Some(b) = self.addr_map.remove(&addr.to_string()) { - self.chan_map.remove(&b.number); - true - } else { - false - } - } - - pub(crate) fn delete_by_number(&mut self, number: u16) -> bool { - if let Some(s) = self.chan_map.remove(&number) { - self.addr_map.remove(&s); - true - } else { - false - } - } - - pub(crate) fn size(&self) -> usize { - self.addr_map.len() - } -} diff --git a/turn/src/client/binding/binding_test.rs b/turn/src/client/binding/binding_test.rs deleted file mode 100644 index d8bae6863..000000000 --- a/turn/src/client/binding/binding_test.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::net::{Ipv4Addr, SocketAddrV4}; - -use super::*; -use crate::error::Result; - -#[test] -fn test_binding_manager_number_assignment() -> Result<()> { - let mut m = BindingManager::new(); - let mut n: u16; - for i in 0..10 { - n = m.assign_channel_number(); - assert_eq!(MIN_CHANNEL_NUMBER + i, n, "should match"); - } - - m.next = 0x7ff0; - for i in 0..16 { - n = m.assign_channel_number(); - assert_eq!(0x7ff0 + i, n, "should match"); - } - // back to min - n = m.assign_channel_number(); - assert_eq!(MIN_CHANNEL_NUMBER, n, "should match"); - - Ok(()) -} - -#[test] -fn test_binding_manager_method() -> Result<()> { - let lo = Ipv4Addr::new(127, 0, 0, 1); - let count = 100; - let mut m = BindingManager::new(); - for i in 0..count { - let addr = SocketAddr::V4(SocketAddrV4::new(lo, 10000 + i)); - let b0 = { - let b = m.create(addr); - *b.unwrap() - }; - let b1 = m.find_by_addr(&addr); - assert!(b1.is_some(), "should succeed"); - let b2 = m.find_by_number(b0.number); - assert!(b2.is_some(), "should succeed"); - - assert_eq!(b0, *b1.unwrap(), "should match"); - assert_eq!(b0, *b2.unwrap(), "should match"); - } - - assert_eq!(count, m.size() as u16, "should match"); - assert_eq!(count, m.addr_map.len() as u16, "should match"); - - for i in 0..count { - let addr = SocketAddr::V4(SocketAddrV4::new(lo, 10000 + i)); - if i % 2 == 0 { - assert!(m.delete_by_addr(&addr), "should return true"); - } else { - assert!( - m.delete_by_number(MIN_CHANNEL_NUMBER + i), - "should return true" - ); - } - } - - assert_eq!(0, m.size(), "should match"); - assert_eq!(0, m.addr_map.len(), "should match"); - - Ok(()) -} - -#[test] -fn test_binding_manager_failure() -> Result<()> { - let ipv4 = Ipv4Addr::new(127, 0, 0, 1); - let addr = SocketAddr::V4(SocketAddrV4::new(ipv4, 7777)); - let mut m = BindingManager::new(); - let b = m.find_by_addr(&addr); - assert!(b.is_none(), "should fail"); - let b = m.find_by_number(5555); - assert!(b.is_none(), "should fail"); - let ok = m.delete_by_addr(&addr); - assert!(!ok, "should fail"); - let ok = m.delete_by_number(5555); - assert!(!ok, "should fail"); - - Ok(()) -} diff --git a/turn/src/client/client_test.rs b/turn/src/client/client_test.rs deleted file mode 100644 index 20516bd25..000000000 --- a/turn/src/client/client_test.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::net::IpAddr; - -use tokio::net::UdpSocket; -use tokio::time::Duration; -use util::vnet::net::*; - -use super::*; -use crate::auth::*; -use crate::relay::relay_static::*; -use crate::server::config::*; -use crate::server::*; - -async fn create_listening_test_client(rto_in_ms: u16) -> Result { - let conn = UdpSocket::bind("0.0.0.0:0").await?; - - let c = Client::new(ClientConfig { - stun_serv_addr: String::new(), - turn_serv_addr: String::new(), - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms, - conn: Arc::new(conn), - vnet: None, - }) - .await?; - - c.listen().await?; - - Ok(c) -} - -async fn create_listening_test_client_with_stun_serv() -> Result { - let conn = UdpSocket::bind("0.0.0.0:0").await?; - - let c = Client::new(ClientConfig { - stun_serv_addr: "stun1.l.google.com:19302".to_owned(), - turn_serv_addr: String::new(), - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms: 0, - conn: Arc::new(conn), - vnet: None, - }) - .await?; - - c.listen().await?; - - Ok(c) -} - -#[tokio::test] -async fn test_client_with_stun_send_binding_request() -> Result<()> { - //env_logger::init(); - - let c = create_listening_test_client_with_stun_serv().await?; - - let resp = c.send_binding_request().await?; - log::debug!("mapped-addr: {}", resp); - { - let ci = c.client_internal.lock().await; - let tm = ci.tr_map.lock().await; - assert_eq!(0, tm.size(), "should be no transaction left"); - } - - c.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_client_with_stun_send_binding_request_to_parallel() -> Result<()> { - env_logger::init(); - - let c1 = create_listening_test_client(0).await?; - let c2 = c1.clone(); - - let (stared_tx, mut started_rx) = mpsc::channel::<()>(1); - let (finished_tx, mut finished_rx) = mpsc::channel::<()>(1); - - let to = lookup_host(true, "stun1.l.google.com:19302").await?; - - tokio::spawn(async move { - drop(stared_tx); - if let Ok(resp) = c2.send_binding_request_to(&to.to_string()).await { - log::debug!("mapped-addr: {}", resp); - } - drop(finished_tx); - }); - - let _ = started_rx.recv().await; - - let resp = c1.send_binding_request_to(&to.to_string()).await?; - log::debug!("mapped-addr: {}", resp); - - let _ = finished_rx.recv().await; - - c1.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_client_with_stun_send_binding_request_to_timeout() -> Result<()> { - //env_logger::init(); - - let c = create_listening_test_client(10).await?; - - let to = lookup_host(true, "127.0.0.1:9").await?; - - let result = c.send_binding_request_to(&to.to_string()).await; - assert!(result.is_err(), "expected error, but got ok"); - - c.close().await?; - - Ok(()) -} - -struct TestAuthHandler; -impl AuthHandler for TestAuthHandler { - fn auth_handle(&self, username: &str, realm: &str, _src_addr: SocketAddr) -> Result> { - Ok(generate_auth_key(username, realm, "pass")) - } -} - -// Create an allocation, and then delete all nonces -// The subsequent Write on the allocation will cause a CreatePermission -// which will be forced to handle a stale nonce response -#[tokio::test] -async fn test_client_nonce_expiration() -> Result<()> { - // env_logger::init(); - - // here, it should use static port, like "0.0.0.0:3478", - // but, due to different test environment, let's fake it by using "0.0.0.0:0" - // to auto assign a "static" port - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let server_port = conn.local_addr()?.port(); - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str("127.0.0.1")?, - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(TestAuthHandler {}), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let client = Client::new(ClientConfig { - stun_serv_addr: format!("127.0.0.1:{server_port}"), - turn_serv_addr: format!("127.0.0.1:{server_port}"), - username: "foo".to_owned(), - password: "pass".to_owned(), - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn, - vnet: None, - }) - .await?; - - client.listen().await?; - - let allocation = client.allocate().await?; - - { - let mut nonces = server.nonces.lock().await; - nonces.clear(); - } - - allocation - .send_to(&[0x00], SocketAddr::from_str("127.0.0.1:8080")?) - .await?; - - // Shutdown - client.close().await?; - server.close().await?; - - Ok(()) -} diff --git a/turn/src/client/mod.rs b/turn/src/client/mod.rs deleted file mode 100644 index 9ae590d8f..000000000 --- a/turn/src/client/mod.rs +++ /dev/null @@ -1,652 +0,0 @@ -#[cfg(test)] -mod client_test; - -pub mod binding; -pub mod periodic_timer; -pub mod permission; -pub mod relay_conn; -pub mod transaction; - -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use async_trait::async_trait; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use binding::*; -use relay_conn::*; -use stun::agent::*; -use stun::attributes::*; -use stun::error_code::*; -use stun::fingerprint::*; -use stun::integrity::*; -use stun::message::*; -use stun::textattrs::*; -use stun::xoraddr::*; -use tokio::pin; -use tokio::select; -use tokio::sync::{mpsc, Mutex}; -use tokio_util::sync::CancellationToken; -use transaction::*; -use util::conn::*; -use util::vnet::net::*; - -use crate::error::*; -use crate::proto::chandata::*; -use crate::proto::data::*; -use crate::proto::lifetime::*; -use crate::proto::peeraddr::*; -use crate::proto::relayaddr::*; -use crate::proto::reqtrans::*; -use crate::proto::PROTO_UDP; - -const DEFAULT_RTO_IN_MS: u16 = 200; -const MAX_DATA_BUFFER_SIZE: usize = u16::MAX as usize; // message size limit for Chromium -const MAX_READ_QUEUE_SIZE: usize = 1024; - -// interval [msec] -// 0: 0 ms +500 -// 1: 500 ms +1000 -// 2: 1500 ms +2000 -// 3: 3500 ms +4000 -// 4: 7500 ms +8000 -// 5: 15500 ms +16000 -// 6: 31500 ms +32000 -// -: 63500 ms failed - -/// ClientConfig is a bag of config parameters for Client. -pub struct ClientConfig { - pub stun_serv_addr: String, // STUN server address (e.g. "stun.abc.com:3478") - pub turn_serv_addr: String, // TURN server address (e.g. "turn.abc.com:3478") - pub username: String, - pub password: String, - pub realm: String, - pub software: String, - pub rto_in_ms: u16, - pub conn: Arc, - pub vnet: Option>, -} - -struct ClientInternal { - conn: Arc, - stun_serv_addr: String, - turn_serv_addr: String, - username: Username, - password: String, - realm: Realm, - integrity: MessageIntegrity, - software: Software, - tr_map: Arc>, - binding_mgr: Arc>, - rto_in_ms: u16, - read_ch_tx: Arc>>>, - close_notify: CancellationToken, -} - -#[async_trait] -impl RelayConnObserver for ClientInternal { - /// Returns the TURN server address. - fn turn_server_addr(&self) -> String { - self.turn_serv_addr.clone() - } - - /// Returns the `username`. - fn username(&self) -> Username { - self.username.clone() - } - - /// Return the `realm`. - fn realm(&self) -> Realm { - self.realm.clone() - } - - /// Sends data to the specified destination using the base socket. - async fn write_to(&self, data: &[u8], to: &str) -> std::result::Result { - let n = self.conn.send_to(data, SocketAddr::from_str(to)?).await?; - Ok(n) - } - - /// Performs STUN transaction. - async fn perform_transaction( - &mut self, - msg: &Message, - to: &str, - ignore_result: bool, - ) -> Result { - let tr_key = BASE64_STANDARD.encode(msg.transaction_id.0); - - let mut tr = Transaction::new(TransactionConfig { - key: tr_key.clone(), - raw: msg.raw.clone(), - to: to.to_string(), - interval: self.rto_in_ms, - ignore_result, - }); - let result_ch_rx = tr.get_result_channel(); - - log::trace!("start {} transaction {} to {}", msg.typ, tr_key, tr.to); - { - let mut tm = self.tr_map.lock().await; - tm.insert(tr_key.clone(), tr); - } - - self.conn - .send_to(&msg.raw, SocketAddr::from_str(to)?) - .await?; - - let conn2 = Arc::clone(&self.conn); - let tr_map2 = Arc::clone(&self.tr_map); - { - let mut tm = self.tr_map.lock().await; - if let Some(tr) = tm.get(&tr_key) { - tr.start_rtx_timer(conn2, tr_map2).await; - } - } - - // If dontWait is true, get the transaction going and return immediately - if ignore_result { - return Ok(TransactionResult::default()); - } - - // wait_for_result waits for the transaction result - if let Some(mut result_ch_rx) = result_ch_rx { - match result_ch_rx.recv().await { - Some(tr) => Ok(tr), - None => Err(Error::ErrTransactionClosed), - } - } else { - Err(Error::ErrWaitForResultOnNonResultTransaction) - } - } -} - -impl ClientInternal { - /// Creates a new [`ClientInternal`]. - async fn new(config: ClientConfig) -> Result { - let net = if let Some(vnet) = config.vnet { - if vnet.is_virtual() { - log::warn!("vnet is enabled"); - } - vnet - } else { - Arc::new(Net::new(None)) - }; - - let stun_serv_addr = if config.stun_serv_addr.is_empty() { - String::new() - } else { - log::debug!("resolving {}", config.stun_serv_addr); - let local_addr = config.conn.local_addr()?; - let stun_serv = net - .resolve_addr(local_addr.is_ipv4(), &config.stun_serv_addr) - .await?; - log::debug!("stunServ: {}", stun_serv); - stun_serv.to_string() - }; - - let turn_serv_addr = if config.turn_serv_addr.is_empty() { - String::new() - } else { - log::debug!("resolving {}", config.turn_serv_addr); - let local_addr = config.conn.local_addr()?; - let turn_serv = net - .resolve_addr(local_addr.is_ipv4(), &config.turn_serv_addr) - .await?; - log::debug!("turnServ: {}", turn_serv); - turn_serv.to_string() - }; - - Ok(ClientInternal { - conn: Arc::clone(&config.conn), - stun_serv_addr, - turn_serv_addr, - username: Username::new(ATTR_USERNAME, config.username), - password: config.password, - realm: Realm::new(ATTR_REALM, config.realm), - software: Software::new(ATTR_SOFTWARE, config.software), - tr_map: Arc::new(Mutex::new(TransactionMap::new())), - binding_mgr: Arc::new(Mutex::new(BindingManager::new())), - rto_in_ms: if config.rto_in_ms != 0 { - config.rto_in_ms - } else { - DEFAULT_RTO_IN_MS - }, - integrity: MessageIntegrity::new_short_term_integrity(String::new()), - read_ch_tx: Arc::new(Mutex::new(None)), - close_notify: CancellationToken::new(), - }) - } - - /// Returns the STUN server address. - fn stun_server_addr(&self) -> String { - self.stun_serv_addr.clone() - } - - /// `listen()` will have this client start listening on the `relay_conn` provided via the config. - /// This is optional. If not used, you will need to call `handle_inbound` method - /// to supply incoming data, instead. - async fn listen(&self) -> Result<()> { - let conn = Arc::clone(&self.conn); - let stun_serv_str = self.stun_serv_addr.clone(); - let tr_map = Arc::clone(&self.tr_map); - let read_ch_tx = Arc::clone(&self.read_ch_tx); - let binding_mgr = Arc::clone(&self.binding_mgr); - let close_notify = self.close_notify.clone(); - - tokio::spawn(async move { - let mut buf = vec![0u8; MAX_DATA_BUFFER_SIZE]; - let wait_cancel = close_notify.cancelled(); - pin!(wait_cancel); - - loop { - let (n, from) = select! { - biased; - - _ = &mut wait_cancel => { - log::debug!("exiting read loop"); - break; - }, - result = conn.recv_from(&mut buf) => match result { - Ok((n, from)) => (n, from), - Err(err) => { - log::debug!("exiting read loop: {}", err); - break; - } - } - }; - log::debug!("received {} bytes of udp from {}", n, from); - - select! { - biased; - - _ = &mut wait_cancel => { - log::debug!("exiting read loop"); - break; - }, - result = ClientInternal::handle_inbound( - &read_ch_tx, - &buf[..n], - from, - &stun_serv_str, - &tr_map, - &binding_mgr, - ) => { - if let Err(err) = result { - log::debug!("exiting read loop: {}", err); - break; - } - } - } - } - }); - - Ok(()) - } - - /// Handles data received. - /// - /// This method handles incoming packet demultiplex it by the source address - /// and the types of the message. - /// Caller should check if the packet was handled by this client or not. - /// If not handled, it is assumed that the packet is application data. - /// If an error is returned, the caller should discard the packet regardless. - async fn handle_inbound( - read_ch_tx: &Arc>>>, - data: &[u8], - from: SocketAddr, - stun_serv_str: &str, - tr_map: &Arc>, - binding_mgr: &Arc>, - ) -> Result<()> { - // +-------------------+-------------------------------+ - // | Return Values | | - // +-------------------+ Meaning / Action | - // | handled | error | | - // |=========+=========+===============================+ - // | false | nil | Handle the packet as app data | - // |---------+---------+-------------------------------+ - // | true | nil | Nothing to do | - // |---------+---------+-------------------------------+ - // | false | error | (shouldn't happen) | - // |---------+---------+-------------------------------+ - // | true | error | Error occurred while handling | - // +---------+---------+-------------------------------+ - // Possible causes of the error: - // - Malformed packet (parse error) - // - STUN message was a request - // - Non-STUN message from the STUN server - - if is_message(data) { - ClientInternal::handle_stun_message(tr_map, read_ch_tx, data, from).await - } else if ChannelData::is_channel_data(data) { - ClientInternal::handle_channel_data(binding_mgr, read_ch_tx, data).await - } else if !stun_serv_str.is_empty() && from.to_string() == *stun_serv_str { - // received from STUN server but it is not a STUN message - Err(Error::ErrNonStunmessage) - } else { - // assume, this is an application data - log::trace!("non-STUN/TURN packect, unhandled"); - Ok(()) - } - } - - async fn handle_stun_message( - tr_map: &Arc>, - read_ch_tx: &Arc>>>, - data: &[u8], - mut from: SocketAddr, - ) -> Result<()> { - let mut msg = Message::new(); - msg.raw = data.to_vec(); - msg.decode()?; - - if msg.typ.class == CLASS_REQUEST { - return Err(Error::Other(format!( - "{:?} : {}", - Error::ErrUnexpectedStunrequestMessage, - msg - ))); - } - - if msg.typ.class == CLASS_INDICATION { - if msg.typ.method == METHOD_DATA { - let mut peer_addr = PeerAddress::default(); - peer_addr.get_from(&msg)?; - from = SocketAddr::new(peer_addr.ip, peer_addr.port); - - let mut data = Data::default(); - data.get_from(&msg)?; - - log::debug!("data indication received from {}", from); - - let _ = ClientInternal::handle_inbound_relay_conn(read_ch_tx, &data.0, from).await; - } - - return Ok(()); - } - - // This is a STUN response message (transactional) - // The type is either: - // - stun.ClassSuccessResponse - // - stun.ClassErrorResponse - - let tr_key = BASE64_STANDARD.encode(msg.transaction_id.0); - - let mut tm = tr_map.lock().await; - if tm.find(&tr_key).is_none() { - // silently discard - log::debug!("no transaction for {}", msg); - return Ok(()); - } - - if let Some(mut tr) = tm.delete(&tr_key) { - // End the transaction - tr.stop_rtx_timer(); - - if !tr - .write_result(TransactionResult { - msg, - from, - retries: tr.retries(), - ..Default::default() - }) - .await - { - log::debug!("no listener for msg.raw {:?}", data); - } - } - - Ok(()) - } - - async fn handle_channel_data( - binding_mgr: &Arc>, - read_ch_tx: &Arc>>>, - data: &[u8], - ) -> Result<()> { - let mut ch_data = ChannelData { - raw: data.to_vec(), - ..Default::default() - }; - ch_data.decode()?; - - let addr = ClientInternal::find_addr_by_channel_number(binding_mgr, ch_data.number.0) - .await - .ok_or(Error::ErrChannelBindNotFound)?; - - log::trace!( - "channel data received from {} (ch={})", - addr, - ch_data.number.0 - ); - - let _ = ClientInternal::handle_inbound_relay_conn(read_ch_tx, &ch_data.data, addr).await; - - Ok(()) - } - - /// Passes inbound data in RelayConn. - async fn handle_inbound_relay_conn( - read_ch_tx: &Arc>>>, - data: &[u8], - from: SocketAddr, - ) -> Result<()> { - let read_ch_tx_opt = read_ch_tx.lock().await; - log::debug!("read_ch_tx_opt = {}", read_ch_tx_opt.is_some()); - if let Some(tx) = &*read_ch_tx_opt { - log::debug!("try_send data = {:?}, from = {}", data, from); - if tx - .try_send(InboundData { - data: data.to_vec(), - from, - }) - .is_err() - { - log::warn!("receive buffer full"); - } - Ok(()) - } else { - Err(Error::ErrAlreadyClosed) - } - } - - /// Closes this client. - async fn close(&mut self) { - self.close_notify.cancel(); - { - let mut read_ch_tx = self.read_ch_tx.lock().await; - read_ch_tx.take(); - } - { - let mut tm = self.tr_map.lock().await; - tm.close_and_delete_all(); - } - } - - /// Sends a new STUN request to the given transport address. - async fn send_binding_request_to(&mut self, to: &str) -> Result { - let msg = { - let attrs: Vec> = if !self.software.text.is_empty() { - vec![ - Box::new(TransactionId::new()), - Box::new(BINDING_REQUEST), - Box::new(self.software.clone()), - ] - } else { - vec![Box::new(TransactionId::new()), Box::new(BINDING_REQUEST)] - }; - - let mut msg = Message::new(); - msg.build(&attrs)?; - msg - }; - - log::debug!("client.SendBindingRequestTo call PerformTransaction 1"); - let tr_res = self.perform_transaction(&msg, to, false).await?; - - let mut refl_addr = XorMappedAddress::default(); - refl_addr.get_from(&tr_res.msg)?; - - Ok(SocketAddr::new(refl_addr.ip, refl_addr.port)) - } - - /// Sends a new STUN request to the STUN server. - async fn send_binding_request(&mut self) -> Result { - if self.stun_serv_addr.is_empty() { - Err(Error::ErrStunserverAddressNotSet) - } else { - self.send_binding_request_to(&self.stun_serv_addr.clone()) - .await - } - } - - /// Returns a peer address associated with the - // channel number on this UDPConn - async fn find_addr_by_channel_number( - binding_mgr: &Arc>, - ch_num: u16, - ) -> Option { - let bm = binding_mgr.lock().await; - bm.find_by_number(ch_num).map(|b| b.addr) - } - - /// Sends a TURN allocation request to the given transport address. - async fn allocate(&mut self) -> Result { - { - let read_ch_tx = self.read_ch_tx.lock().await; - log::debug!("allocate check: read_ch_tx_opt = {}", read_ch_tx.is_some()); - if read_ch_tx.is_some() { - return Err(Error::ErrOneAllocateOnly); - } - } - - let mut msg = Message::new(); - msg.build(&[ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_ALLOCATE, CLASS_REQUEST)), - Box::new(RequestedTransport { - protocol: PROTO_UDP, - }), - Box::new(FINGERPRINT), - ])?; - - log::debug!("client.Allocate call PerformTransaction 1"); - let tr_res = self - .perform_transaction(&msg, &self.turn_serv_addr.clone(), false) - .await?; - let res = tr_res.msg; - - // Anonymous allocate failed, trying to authenticate. - let nonce = Nonce::get_from_as(&res, ATTR_NONCE)?; - self.realm = Realm::get_from_as(&res, ATTR_REALM)?; - - self.integrity = MessageIntegrity::new_long_term_integrity( - self.username.text.clone(), - self.realm.text.clone(), - self.password.clone(), - ); - - // Trying to authorize. - msg.build(&[ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_ALLOCATE, CLASS_REQUEST)), - Box::new(RequestedTransport { - protocol: PROTO_UDP, - }), - Box::new(self.username.clone()), - Box::new(self.realm.clone()), - Box::new(nonce.clone()), - Box::new(self.integrity.clone()), - Box::new(FINGERPRINT), - ])?; - - log::debug!("client.Allocate call PerformTransaction 2"); - let tr_res = self - .perform_transaction(&msg, &self.turn_serv_addr.clone(), false) - .await?; - let res = tr_res.msg; - - if res.typ.class == CLASS_ERROR_RESPONSE { - let mut code = ErrorCodeAttribute::default(); - let result = code.get_from(&res); - if result.is_err() { - return Err(Error::Other(format!("{}", res.typ))); - } else { - return Err(Error::Other(format!("{} (error {})", res.typ, code))); - } - } - - // Getting relayed addresses from response. - let mut relayed = RelayedAddress::default(); - relayed.get_from(&res)?; - let relayed_addr = SocketAddr::new(relayed.ip, relayed.port); - - // Getting lifetime from response - let mut lifetime = Lifetime::default(); - lifetime.get_from(&res)?; - - let (read_ch_tx, read_ch_rx) = mpsc::channel(MAX_READ_QUEUE_SIZE); - { - let mut read_ch_tx_opt = self.read_ch_tx.lock().await; - *read_ch_tx_opt = Some(read_ch_tx); - log::debug!("allocate: read_ch_tx_opt = {}", read_ch_tx_opt.is_some()); - } - - Ok(RelayConnConfig { - relayed_addr, - integrity: self.integrity.clone(), - nonce, - lifetime: lifetime.0, - binding_mgr: Arc::clone(&self.binding_mgr), - read_ch_rx: Arc::new(Mutex::new(read_ch_rx)), - }) - } -} - -/// Client is a STUN server client. -#[derive(Clone)] -pub struct Client { - client_internal: Arc>, -} - -impl Client { - pub async fn new(config: ClientConfig) -> Result { - let ci = ClientInternal::new(config).await?; - Ok(Client { - client_internal: Arc::new(Mutex::new(ci)), - }) - } - - pub async fn listen(&self) -> Result<()> { - let ci = self.client_internal.lock().await; - ci.listen().await - } - - pub async fn allocate(&self) -> Result { - let config = { - let mut ci = self.client_internal.lock().await; - ci.allocate().await? - }; - - Ok(RelayConn::new(Arc::clone(&self.client_internal), config).await) - } - - pub async fn close(&self) -> Result<()> { - let mut ci = self.client_internal.lock().await; - ci.close().await; - Ok(()) - } - - /// Sends a new STUN request to the given transport address. - pub async fn send_binding_request_to(&self, to: &str) -> Result { - let mut ci = self.client_internal.lock().await; - ci.send_binding_request_to(to).await - } - - /// Sends a new STUN request to the STUN server. - pub async fn send_binding_request(&self) -> Result { - let mut ci = self.client_internal.lock().await; - ci.send_binding_request().await - } -} diff --git a/turn/src/client/periodic_timer.rs b/turn/src/client/periodic_timer.rs deleted file mode 100644 index d0e5cdf44..000000000 --- a/turn/src/client/periodic_timer.rs +++ /dev/null @@ -1,93 +0,0 @@ -#[cfg(test)] -mod periodic_timer_test; - -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum TimerIdRefresh { - #[default] - Alloc, - Perms, -} - -/// `PeriodicTimerTimeoutHandler` is a handler called on timeout. -#[async_trait] -pub trait PeriodicTimerTimeoutHandler { - async fn on_timeout(&mut self, id: TimerIdRefresh); -} - -/// `PeriodicTimer` is a periodic timer. -#[derive(Default)] -pub struct PeriodicTimer { - id: TimerIdRefresh, - interval: Duration, - close_tx: Mutex>>, -} - -impl PeriodicTimer { - /// create a new [`PeriodicTimer`]. - pub fn new(id: TimerIdRefresh, interval: Duration) -> Self { - PeriodicTimer { - id, - interval, - close_tx: Mutex::new(None), - } - } - - /// Starts the timer. - pub async fn start( - &self, - timeout_handler: Arc>, - ) -> bool { - // this is a noop if the timer is always running - { - let close_tx = self.close_tx.lock().await; - if close_tx.is_some() { - return false; - } - } - - let (close_tx, mut close_rx) = mpsc::channel(1); - let interval = self.interval; - let id = self.id; - - tokio::spawn(async move { - loop { - let timer = tokio::time::sleep(interval); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() => { - let mut handler = timeout_handler.lock().await; - handler.on_timeout(id).await; - } - _ = close_rx.recv() => break, - } - } - }); - - { - let mut close = self.close_tx.lock().await; - *close = Some(close_tx); - } - - true - } - - /// Stops the timer. - pub async fn stop(&self) { - let mut close_tx = self.close_tx.lock().await; - close_tx.take(); - } - - /// Tests if the timer is running. - /// Debug purpose only. - pub async fn is_running(&self) -> bool { - let close_tx = self.close_tx.lock().await; - close_tx.is_some() - } -} diff --git a/turn/src/client/periodic_timer/periodic_timer_test.rs b/turn/src/client/periodic_timer/periodic_timer_test.rs deleted file mode 100644 index b67b31dd3..000000000 --- a/turn/src/client/periodic_timer/periodic_timer_test.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::*; -use crate::error::Result; - -struct DummyPeriodicTimerTimeoutHandler; - -#[async_trait] -impl PeriodicTimerTimeoutHandler for DummyPeriodicTimerTimeoutHandler { - async fn on_timeout(&mut self, id: TimerIdRefresh) { - assert_eq!(id, TimerIdRefresh::Perms); - } -} - -#[tokio::test] -async fn test_periodic_timer() -> Result<()> { - let timer_id = TimerIdRefresh::Perms; - let rt = PeriodicTimer::new(timer_id, Duration::from_millis(50)); - let dummy1 = Arc::new(Mutex::new(DummyPeriodicTimerTimeoutHandler {})); - let dummy2 = Arc::clone(&dummy1); - - assert!(!rt.is_running().await, "should not be running yet"); - - let ok = rt.start(dummy1).await; - assert!(ok, "should be true"); - assert!(rt.is_running().await, "should be running"); - - tokio::time::sleep(Duration::from_millis(100)).await; - - let ok = rt.start(dummy2).await; - assert!(!ok, "start again is noop"); - - tokio::time::sleep(Duration::from_millis(120)).await; - rt.stop().await; - - assert!(!rt.is_running().await, "should not be running"); - - Ok(()) -} diff --git a/turn/src/client/permission.rs b/turn/src/client/permission.rs deleted file mode 100644 index d34b1fdba..000000000 --- a/turn/src/client/permission.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicU8; - -#[derive(Default, Copy, Clone, PartialEq, Debug)] -pub(crate) enum PermState { - #[default] - Idle = 0, - Permitted = 1, -} - -impl From for PermState { - fn from(v: u8) -> Self { - match v { - 0 => PermState::Idle, - _ => PermState::Permitted, - } - } -} - -#[derive(Default)] -pub(crate) struct Permission { - st: AtomicU8, //PermState, -} - -impl Permission { - pub(crate) fn set_state(&self, state: PermState) { - self.st.store(state as u8, Ordering::SeqCst); - } - - pub(crate) fn state(&self) -> PermState { - self.st.load(Ordering::SeqCst).into() - } -} - -/// Thread-safe Permission map. -#[derive(Default)] -pub(crate) struct PermissionMap { - perm_map: HashMap>, -} - -impl PermissionMap { - pub(crate) fn new() -> PermissionMap { - PermissionMap { - perm_map: HashMap::new(), - } - } - - pub(crate) fn insert(&mut self, addr: &SocketAddr, p: Arc) { - self.perm_map.insert(addr.ip().to_string(), p); - } - - pub(crate) fn find(&self, addr: &SocketAddr) -> Option<&Arc> { - self.perm_map.get(&addr.ip().to_string()) - } - - pub(crate) fn delete(&mut self, addr: &SocketAddr) { - self.perm_map.remove(&addr.ip().to_string()); - } - - pub(crate) fn addrs(&self) -> Vec { - let mut a = vec![]; - for k in self.perm_map.keys() { - if let Ok(ip) = k.parse() { - a.push(SocketAddr::new(ip, 0)); - } - } - a - } -} diff --git a/turn/src/client/relay_conn.rs b/turn/src/client/relay_conn.rs deleted file mode 100644 index dd990a879..000000000 --- a/turn/src/client/relay_conn.rs +++ /dev/null @@ -1,630 +0,0 @@ -#[cfg(test)] -mod relay_conn_test; - -// client implements the API for a TURN client -use std::io; -use std::net::SocketAddr; -use std::sync::Arc; - -use async_trait::async_trait; -use stun::agent::*; -use stun::attributes::*; -use stun::error_code::*; -use stun::fingerprint::*; -use stun::integrity::*; -use stun::message::*; -use stun::textattrs::*; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::{Duration, Instant}; -use util::Conn; - -use super::binding::*; -use super::periodic_timer::*; -use super::permission::*; -use super::transaction::*; -use crate::{proto, Error}; - -const PERM_REFRESH_INTERVAL: Duration = Duration::from_secs(120); -const MAX_RETRY_ATTEMPTS: u16 = 3; - -pub(crate) struct InboundData { - pub(crate) data: Vec, - pub(crate) from: SocketAddr, -} - -/// `RelayConnObserver` is an interface to [`RelayConn`] observer. -#[async_trait] -pub trait RelayConnObserver { - fn turn_server_addr(&self) -> String; - fn username(&self) -> Username; - fn realm(&self) -> Realm; - async fn write_to(&self, data: &[u8], to: &str) -> Result; - async fn perform_transaction( - &mut self, - msg: &Message, - to: &str, - ignore_result: bool, - ) -> Result; -} - -/// `RelayConnConfig` is a set of configuration params used by [`RelayConn::new()`]. -pub(crate) struct RelayConnConfig { - pub(crate) relayed_addr: SocketAddr, - pub(crate) integrity: MessageIntegrity, - pub(crate) nonce: Nonce, - pub(crate) lifetime: Duration, - pub(crate) binding_mgr: Arc>, - pub(crate) read_ch_rx: Arc>>, -} - -pub struct RelayConnInternal { - obs: Arc>, - relayed_addr: SocketAddr, - perm_map: PermissionMap, - binding_mgr: Arc>, - integrity: MessageIntegrity, - nonce: Nonce, - lifetime: Duration, -} - -/// `RelayConn` is the implementation of the Conn interfaces for UDP Relayed network connections. -pub struct RelayConn { - relayed_addr: SocketAddr, - read_ch_rx: Arc>>, - relay_conn: Arc>>, - refresh_alloc_timer: PeriodicTimer, - refresh_perms_timer: PeriodicTimer, -} - -impl RelayConn { - /// Creates a new [`RelayConn`]. - pub(crate) async fn new(obs: Arc>, config: RelayConnConfig) -> Self { - log::debug!("initial lifetime: {} seconds", config.lifetime.as_secs()); - - let c = RelayConn { - refresh_alloc_timer: PeriodicTimer::new(TimerIdRefresh::Alloc, config.lifetime / 2), - refresh_perms_timer: PeriodicTimer::new(TimerIdRefresh::Perms, PERM_REFRESH_INTERVAL), - relayed_addr: config.relayed_addr, - read_ch_rx: Arc::clone(&config.read_ch_rx), - relay_conn: Arc::new(Mutex::new(RelayConnInternal::new(obs, config))), - }; - - let rci1 = Arc::clone(&c.relay_conn); - let rci2 = Arc::clone(&c.relay_conn); - - if c.refresh_alloc_timer.start(rci1).await { - log::debug!("refresh_alloc_timer started"); - } - if c.refresh_perms_timer.start(rci2).await { - log::debug!("refresh_perms_timer started"); - } - - c - } -} - -#[async_trait] -impl Conn for RelayConn { - async fn connect(&self, _addr: SocketAddr) -> Result<(), util::Error> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, _buf: &mut [u8]) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - /// Reads a packet from the connection, - /// copying the payload into `p`. It returns the number of - /// bytes copied into `p` and the return address that - /// was on the packet. - /// It returns the number of bytes read `(0 <= n <= len(p))` - /// and any error encountered. Callers should always process - /// the `n > 0` bytes returned before considering the error. - /// It can be made to time out and return - /// an Error with Timeout() == true after a fixed time limit; - /// see SetDeadline and SetReadDeadline. - async fn recv_from(&self, p: &mut [u8]) -> Result<(usize, SocketAddr), util::Error> { - let mut read_ch_rx = self.read_ch_rx.lock().await; - - if let Some(ib_data) = read_ch_rx.recv().await { - let n = ib_data.data.len(); - if p.len() < n { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - Error::ErrShortBuffer.to_string(), - ) - .into()); - } - p[..n].copy_from_slice(&ib_data.data); - Ok((n, ib_data.from)) - } else { - Err(io::Error::new( - io::ErrorKind::ConnectionAborted, - Error::ErrAlreadyClosed.to_string(), - ) - .into()) - } - } - - async fn send(&self, _buf: &[u8]) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - /// Writes a packet with payload `p` to `addr`. - /// It can be made to time out and return - /// an Error with Timeout() == true after a fixed time limit; - /// see SetDeadline and SetWriteDeadline. - /// On packet-oriented connections, write timeouts are rare. - async fn send_to(&self, p: &[u8], addr: SocketAddr) -> Result { - let mut relay_conn = self.relay_conn.lock().await; - match relay_conn.send_to(p, addr).await { - Ok(n) => Ok(n), - Err(err) => Err(io::Error::new(io::ErrorKind::Other, err.to_string()).into()), - } - } - - /// Returns the local network address. - fn local_addr(&self) -> Result { - Ok(self.relayed_addr) - } - - fn remote_addr(&self) -> Option { - None - } - - /// Closes the connection. - /// Any blocked [`Self::recv_from()`] or [`Self::send_to()`] operations - /// will be unblocked and return errors. - async fn close(&self) -> Result<(), util::Error> { - self.refresh_alloc_timer.stop().await; - self.refresh_perms_timer.stop().await; - - let mut relay_conn = self.relay_conn.lock().await; - let _ = relay_conn - .close() - .await - .map_err(|err| util::Error::Other(format!("{err}"))); - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -impl RelayConnInternal { - /// Creates a new [`RelayConnInternal`]. - fn new(obs: Arc>, config: RelayConnConfig) -> Self { - RelayConnInternal { - obs, - relayed_addr: config.relayed_addr, - perm_map: PermissionMap::new(), - binding_mgr: config.binding_mgr, - integrity: config.integrity, - nonce: config.nonce, - lifetime: config.lifetime, - } - } - - /// Writes a packet with payload `p` to `addr`. - /// It can be made to time out and return - /// an Error with Timeout() == true after a fixed time limit; - /// see SetDeadline and SetWriteDeadline. - /// On packet-oriented connections, write timeouts are rare. - async fn send_to(&mut self, p: &[u8], addr: SocketAddr) -> Result { - // check if we have a permission for the destination IP addr - let perm = if let Some(perm) = self.perm_map.find(&addr) { - Arc::clone(perm) - } else { - let perm = Arc::new(Permission::default()); - self.perm_map.insert(&addr, Arc::clone(&perm)); - perm - }; - - let mut result = Ok(()); - for _ in 0..MAX_RETRY_ATTEMPTS { - result = self.create_perm(&perm, addr).await; - if let Err(err) = &result { - if Error::ErrTryAgain != *err { - break; - } - } - } - result?; - - let number = { - let (bind_st, bind_at, bind_number, bind_addr) = { - let mut binding_mgr = self.binding_mgr.lock().await; - let b = if let Some(b) = binding_mgr.find_by_addr(&addr) { - b - } else { - binding_mgr - .create(addr) - .ok_or_else(|| Error::Other("Addr not found".to_owned()))? - }; - (b.state(), b.refreshed_at(), b.number, b.addr) - }; - - if bind_st == BindingState::Idle - || bind_st == BindingState::Request - || bind_st == BindingState::Failed - { - // block only callers with the same binding until - // the binding transaction has been complete - // binding state may have been changed while waiting. check again. - if bind_st == BindingState::Idle { - let binding_mgr = Arc::clone(&self.binding_mgr); - let rc_obs = Arc::clone(&self.obs); - let nonce = self.nonce.clone(); - let integrity = self.integrity.clone(); - { - let mut bm = binding_mgr.lock().await; - if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_state(BindingState::Request); - } - } - tokio::spawn(async move { - let result = RelayConnInternal::bind( - rc_obs, - bind_addr, - bind_number, - nonce, - integrity, - ) - .await; - - { - let mut bm = binding_mgr.lock().await; - if let Err(err) = result { - if Error::ErrUnexpectedResponse != err { - bm.delete_by_addr(&bind_addr); - } else if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_state(BindingState::Failed); - } - - // keep going... - log::warn!("bind() failed: {}", err); - } else if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_state(BindingState::Ready); - } - } - }); - } - - // send data using SendIndication - let peer_addr = socket_addr2peer_address(&addr); - let mut msg = Message::new(); - msg.build(&[ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_SEND, CLASS_INDICATION)), - Box::new(proto::data::Data(p.to_vec())), - Box::new(peer_addr), - Box::new(FINGERPRINT), - ])?; - - // indication has no transaction (fire-and-forget) - let obs = self.obs.lock().await; - let turn_server_addr = obs.turn_server_addr(); - return Ok(obs.write_to(&msg.raw, &turn_server_addr).await?); - } - - // binding is either ready - - // check if the binding needs a refresh - if bind_st == BindingState::Ready - && Instant::now() - .checked_duration_since(bind_at) - .unwrap_or_else(|| Duration::from_secs(0)) - > Duration::from_secs(5 * 60) - { - let binding_mgr = Arc::clone(&self.binding_mgr); - let rc_obs = Arc::clone(&self.obs); - let nonce = self.nonce.clone(); - let integrity = self.integrity.clone(); - { - let mut bm = binding_mgr.lock().await; - if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_state(BindingState::Refresh); - } - } - tokio::spawn(async move { - let result = - RelayConnInternal::bind(rc_obs, bind_addr, bind_number, nonce, integrity) - .await; - - { - let mut bm = binding_mgr.lock().await; - if let Err(err) = result { - if Error::ErrUnexpectedResponse != err { - bm.delete_by_addr(&bind_addr); - } else if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_state(BindingState::Failed); - } - - // keep going... - log::warn!("bind() for refresh failed: {}", err); - } else if let Some(b) = bm.get_by_addr(&bind_addr) { - b.set_refreshed_at(Instant::now()); - b.set_state(BindingState::Ready); - } - } - }); - } - - bind_number - }; - - // send via ChannelData - self.send_channel_data(p, number).await - } - - /// This func-block would block, per destination IP (, or perm), until - /// the perm state becomes "requested". Purpose of this is to guarantee - /// the order of packets (within the same perm). - /// Note that CreatePermission transaction may not be complete before - /// all the data transmission. This is done assuming that the request - /// will be mostly likely successful and we can tolerate some loss of - /// UDP packet (or reorder), inorder to minimize the latency in most cases. - async fn create_perm(&mut self, perm: &Arc, addr: SocketAddr) -> Result<(), Error> { - if perm.state() == PermState::Idle { - // punch a hole! (this would block a bit..) - if let Err(err) = self.create_permissions(&[addr]).await { - self.perm_map.delete(&addr); - return Err(err); - } - perm.set_state(PermState::Permitted); - } - Ok(()) - } - - async fn send_channel_data(&self, data: &[u8], ch_num: u16) -> Result { - let mut ch_data = proto::chandata::ChannelData { - data: data.to_vec(), - number: proto::channum::ChannelNumber(ch_num), - ..Default::default() - }; - ch_data.encode(); - - let obs = self.obs.lock().await; - Ok(obs.write_to(&ch_data.raw, &obs.turn_server_addr()).await?) - } - - async fn create_permissions(&mut self, addrs: &[SocketAddr]) -> Result<(), Error> { - let res = { - let msg = { - let obs = self.obs.lock().await; - let mut setters: Vec> = vec![ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_CREATE_PERMISSION, CLASS_REQUEST)), - ]; - - for addr in addrs { - setters.push(Box::new(socket_addr2peer_address(addr))); - } - - setters.push(Box::new(obs.username())); - setters.push(Box::new(obs.realm())); - setters.push(Box::new(self.nonce.clone())); - setters.push(Box::new(self.integrity.clone())); - setters.push(Box::new(FINGERPRINT)); - - let mut msg = Message::new(); - msg.build(&setters)?; - msg - }; - - let mut obs = self.obs.lock().await; - let turn_server_addr = obs.turn_server_addr(); - - log::debug!("UDPConn.createPermissions call PerformTransaction 1"); - let tr_res = obs - .perform_transaction(&msg, &turn_server_addr, false) - .await?; - - tr_res.msg - }; - - if res.typ.class == CLASS_ERROR_RESPONSE { - let mut code = ErrorCodeAttribute::default(); - let result = code.get_from(&res); - if result.is_err() { - return Err(Error::Other(format!("{}", res.typ))); - } else if code.code == CODE_STALE_NONCE { - self.set_nonce_from_msg(&res); - return Err(Error::ErrTryAgain); - } else { - return Err(Error::Other(format!("{} (error {})", res.typ, code))); - } - } - - Ok(()) - } - - pub fn set_nonce_from_msg(&mut self, msg: &Message) { - // Update nonce - match Nonce::get_from_as(msg, ATTR_NONCE) { - Ok(nonce) => { - self.nonce = nonce; - log::debug!("refresh allocation: 438, got new nonce."); - } - Err(_) => log::warn!("refresh allocation: 438 but no nonce."), - } - } - - /// Closes the connection. - /// Any blocked `recv_from` or `send_to` operations will be unblocked and return errors. - pub async fn close(&mut self) -> Result<(), Error> { - self.refresh_allocation(Duration::from_secs(0), true /* dontWait=true */) - .await - } - - async fn refresh_allocation( - &mut self, - lifetime: Duration, - dont_wait: bool, - ) -> Result<(), Error> { - let res = { - let mut obs = self.obs.lock().await; - - let mut msg = Message::new(); - msg.build(&[ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_REFRESH, CLASS_REQUEST)), - Box::new(proto::lifetime::Lifetime(lifetime)), - Box::new(obs.username()), - Box::new(obs.realm()), - Box::new(self.nonce.clone()), - Box::new(self.integrity.clone()), - Box::new(FINGERPRINT), - ])?; - - log::debug!("send refresh request (dont_wait={})", dont_wait); - let turn_server_addr = obs.turn_server_addr(); - let tr_res = obs - .perform_transaction(&msg, &turn_server_addr, dont_wait) - .await?; - - if dont_wait { - log::debug!("refresh request sent"); - return Ok(()); - } - - log::debug!("refresh request sent, and waiting response"); - - tr_res.msg - }; - - if res.typ.class == CLASS_ERROR_RESPONSE { - let mut code = ErrorCodeAttribute::default(); - let result = code.get_from(&res); - if result.is_err() { - return Err(Error::Other(format!("{}", res.typ))); - } else if code.code == CODE_STALE_NONCE { - self.set_nonce_from_msg(&res); - return Err(Error::ErrTryAgain); - } else { - return Ok(()); - } - } - - // Getting lifetime from response - let mut updated_lifetime = proto::lifetime::Lifetime::default(); - updated_lifetime.get_from(&res)?; - - self.lifetime = updated_lifetime.0; - log::debug!("updated lifetime: {} seconds", self.lifetime.as_secs()); - Ok(()) - } - - async fn refresh_permissions(&mut self) -> Result<(), Error> { - let addrs = self.perm_map.addrs(); - if addrs.is_empty() { - log::debug!("no permission to refresh"); - return Ok(()); - } - - if let Err(err) = self.create_permissions(&addrs).await { - if Error::ErrTryAgain != err { - log::error!("fail to refresh permissions: {}", err); - } - return Err(err); - } - - log::debug!("refresh permissions successful"); - Ok(()) - } - - async fn bind( - rc_obs: Arc>, - bind_addr: SocketAddr, - bind_number: u16, - nonce: Nonce, - integrity: MessageIntegrity, - ) -> Result<(), Error> { - let (msg, turn_server_addr) = { - let obs = rc_obs.lock().await; - - let setters: Vec> = vec![ - Box::new(TransactionId::new()), - Box::new(MessageType::new(METHOD_CHANNEL_BIND, CLASS_REQUEST)), - Box::new(socket_addr2peer_address(&bind_addr)), - Box::new(proto::channum::ChannelNumber(bind_number)), - Box::new(obs.username()), - Box::new(obs.realm()), - Box::new(nonce), - Box::new(integrity), - Box::new(FINGERPRINT), - ]; - - let mut msg = Message::new(); - msg.build(&setters)?; - - (msg, obs.turn_server_addr()) - }; - - log::debug!("UDPConn.bind call PerformTransaction 1"); - let tr_res = { - let mut obs = rc_obs.lock().await; - obs.perform_transaction(&msg, &turn_server_addr, false) - .await? - }; - - let res = tr_res.msg; - - if res.typ != MessageType::new(METHOD_CHANNEL_BIND, CLASS_SUCCESS_RESPONSE) { - return Err(Error::ErrUnexpectedResponse); - } - - log::debug!("channel binding successful: {} {}", bind_addr, bind_number); - - // Success. - Ok(()) - } -} - -#[async_trait] -impl PeriodicTimerTimeoutHandler for RelayConnInternal { - async fn on_timeout(&mut self, id: TimerIdRefresh) { - log::debug!("refresh timer {:?} expired", id); - match id { - TimerIdRefresh::Alloc => { - let lifetime = self.lifetime; - // limit the max retries on errTryAgain to 3 - // when stale nonce returns, second retry should succeed - let mut result = Ok(()); - for _ in 0..MAX_RETRY_ATTEMPTS { - result = self.refresh_allocation(lifetime, false).await; - if let Err(err) = &result { - if Error::ErrTryAgain != *err { - break; - } - } - } - if result.is_err() { - log::warn!("refresh allocation failed"); - } - } - TimerIdRefresh::Perms => { - let mut result = Ok(()); - for _ in 0..MAX_RETRY_ATTEMPTS { - result = self.refresh_permissions().await; - if let Err(err) = &result { - if Error::ErrTryAgain != *err { - break; - } - } - } - if result.is_err() { - log::warn!("refresh permissions failed"); - } - } - } - } -} - -fn socket_addr2peer_address(addr: &SocketAddr) -> proto::peeraddr::PeerAddress { - proto::peeraddr::PeerAddress { - ip: addr.ip(), - port: addr.port(), - } -} diff --git a/turn/src/client/relay_conn/relay_conn_test.rs b/turn/src/client/relay_conn/relay_conn_test.rs deleted file mode 100644 index cc0e6f59a..000000000 --- a/turn/src/client/relay_conn/relay_conn_test.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::net::Ipv4Addr; - -use super::*; -use crate::error::Result; - -struct DummyRelayConnObserver { - turn_server_addr: String, - username: Username, - realm: Realm, -} - -#[async_trait] -impl RelayConnObserver for DummyRelayConnObserver { - fn turn_server_addr(&self) -> String { - self.turn_server_addr.clone() - } - - fn username(&self) -> Username { - self.username.clone() - } - - fn realm(&self) -> Realm { - self.realm.clone() - } - - async fn write_to(&self, _data: &[u8], _to: &str) -> std::result::Result { - Ok(0) - } - - async fn perform_transaction( - &mut self, - _msg: &Message, - _to: &str, - _dont_wait: bool, - ) -> Result { - Err(Error::ErrFakeErr) - } -} - -#[tokio::test] -async fn test_relay_conn() -> Result<()> { - let obs = DummyRelayConnObserver { - turn_server_addr: String::new(), - username: Username::new(ATTR_USERNAME, "username".to_owned()), - realm: Realm::new(ATTR_REALM, "realm".to_owned()), - }; - - let (_read_ch_tx, read_ch_rx) = mpsc::channel(100); - - let config = RelayConnConfig { - relayed_addr: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), - integrity: MessageIntegrity::default(), - nonce: Nonce::new(ATTR_NONCE, "nonce".to_owned()), - lifetime: Duration::from_secs(0), - binding_mgr: Arc::new(Mutex::new(BindingManager::new())), - read_ch_rx: Arc::new(Mutex::new(read_ch_rx)), - }; - - let rc = RelayConn::new(Arc::new(Mutex::new(obs)), config).await; - - let rci = rc.relay_conn.lock().await; - let (bind_addr, bind_number) = { - let mut bm = rci.binding_mgr.lock().await; - let b = bm - .create(SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 1234)) - .unwrap(); - (b.addr, b.number) - }; - - //let binding_mgr = Arc::clone(&rci.binding_mgr); - let rc_obs = Arc::clone(&rci.obs); - let nonce = rci.nonce.clone(); - let integrity = rci.integrity.clone(); - - if let Err(err) = - RelayConnInternal::bind(rc_obs, bind_addr, bind_number, nonce, integrity).await - { - assert!(Error::ErrUnexpectedResponse != err); - } else { - panic!("should fail"); - } - - Ok(()) -} diff --git a/turn/src/client/transaction.rs b/turn/src/client/transaction.rs deleted file mode 100644 index 557269d7e..000000000 --- a/turn/src/client/transaction.rs +++ /dev/null @@ -1,282 +0,0 @@ -use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicU16; -use stun::message::*; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; -use util::Conn; - -use crate::error::*; - -const MAX_RTX_INTERVAL_IN_MS: u16 = 1600; -const MAX_RTX_COUNT: u16 = 7; // total 7 requests (Rc) - -async fn on_rtx_timeout( - conn: &Arc, - tr_map: &Arc>, - tr_key: &str, - n_rtx: u16, -) -> bool { - let mut tm = tr_map.lock().await; - let (tr_raw, tr_to) = match tm.find(tr_key) { - Some(tr) => (tr.raw.clone(), tr.to.clone()), - None => return true, // already gone - }; - - if n_rtx == MAX_RTX_COUNT { - // all retransmisstions failed - if let Some(tr) = tm.delete(tr_key) { - if !tr - .write_result(TransactionResult { - err: Some(Error::Other(format!( - "{:?} {}", - Error::ErrAllRetransmissionsFailed, - tr_key - ))), - ..Default::default() - }) - .await - { - log::debug!("no listener for transaction"); - } - } - return true; - } - - log::trace!( - "retransmitting transaction {} to {} (n_rtx={})", - tr_key, - tr_to, - n_rtx - ); - - let dst = match SocketAddr::from_str(&tr_to) { - Ok(dst) => dst, - Err(_) => return false, - }; - - if conn.send_to(&tr_raw, dst).await.is_err() { - if let Some(tr) = tm.delete(tr_key) { - if !tr - .write_result(TransactionResult { - err: Some(Error::Other(format!( - "{:?} {}", - Error::ErrAllRetransmissionsFailed, - tr_key - ))), - ..Default::default() - }) - .await - { - log::debug!("no listener for transaction"); - } - } - return true; - } - - false -} - -/// `TransactionResult` is a bag of result values of a transaction. -#[derive(Debug)] //Clone -pub struct TransactionResult { - pub msg: Message, - pub from: SocketAddr, - pub retries: u16, - pub err: Option, -} - -impl Default for TransactionResult { - fn default() -> Self { - TransactionResult { - msg: Message::default(), - from: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), - retries: 0, - err: None, - } - } -} - -/// `TransactionConfig` is a set of config params used by [`Transaction::new()`]. -#[derive(Default)] -pub struct TransactionConfig { - pub key: String, - pub raw: Vec, - pub to: String, - pub interval: u16, - pub ignore_result: bool, // true to throw away the result of this transaction (it will not be readable using wait_for_result) -} - -/// `Transaction` represents a transaction. -#[derive(Debug)] -pub struct Transaction { - pub key: String, - pub raw: Vec, - pub to: String, - pub n_rtx: Arc, - pub interval: Arc, - timer_ch_tx: Option>, - result_ch_tx: Option>, - result_ch_rx: Option>, -} - -impl Default for Transaction { - fn default() -> Self { - Transaction { - key: String::new(), - raw: vec![], - to: String::new(), - n_rtx: Arc::new(AtomicU16::new(0)), - interval: Arc::new(AtomicU16::new(0)), - //timer: None, - timer_ch_tx: None, - result_ch_tx: None, - result_ch_rx: None, - } - } -} - -impl Transaction { - /// Creates a new [`Transaction`] using the given `config`. - pub fn new(config: TransactionConfig) -> Self { - let (result_ch_tx, result_ch_rx) = if !config.ignore_result { - let (tx, rx) = mpsc::channel(1); - (Some(tx), Some(rx)) - } else { - (None, None) - }; - - Transaction { - key: config.key, - raw: config.raw, - to: config.to, - interval: Arc::new(AtomicU16::new(config.interval)), - result_ch_tx, - result_ch_rx, - ..Default::default() - } - } - - /// Starts the transaction timer. - pub async fn start_rtx_timer( - &mut self, - conn: Arc, - tr_map: Arc>, - ) { - let (timer_ch_tx, mut timer_ch_rx) = mpsc::channel(1); - self.timer_ch_tx = Some(timer_ch_tx); - let (n_rtx, interval, key) = (self.n_rtx.clone(), self.interval.clone(), self.key.clone()); - - tokio::spawn(async move { - let mut done = false; - while !done { - let timer = tokio::time::sleep(Duration::from_millis( - interval.load(Ordering::SeqCst) as u64, - )); - tokio::pin!(timer); - - tokio::select! { - _ = timer.as_mut() => { - let rtx = n_rtx.fetch_add(1, Ordering::SeqCst); - - let mut val = interval.load(Ordering::SeqCst); - val *= 2; - if val > MAX_RTX_INTERVAL_IN_MS { - val = MAX_RTX_INTERVAL_IN_MS; - } - interval.store(val, Ordering::SeqCst); - - done = on_rtx_timeout(&conn, &tr_map, &key, rtx + 1).await; - } - _ = timer_ch_rx.recv() => done = true, - } - } - }); - } - - /// Stops the transaction timer. - pub fn stop_rtx_timer(&mut self) { - if self.timer_ch_tx.is_some() { - self.timer_ch_tx.take(); - } - } - - /// Writes the result to the result channel. - pub async fn write_result(&self, res: TransactionResult) -> bool { - if let Some(result_ch) = &self.result_ch_tx { - result_ch.send(res).await.is_ok() - } else { - false - } - } - - /// Returns the result channel. - pub fn get_result_channel(&mut self) -> Option> { - self.result_ch_rx.take() - } - - /// Closes the transaction. - pub fn close(&mut self) { - if self.result_ch_tx.is_some() { - self.result_ch_tx.take(); - } - } - - /// Returns the number of retransmission it has made. - pub fn retries(&self) -> u16 { - self.n_rtx.load(Ordering::SeqCst) - } -} - -/// `TransactionMap` is a thread-safe transaction map. -#[derive(Default, Debug)] -pub struct TransactionMap { - tr_map: HashMap, -} - -impl TransactionMap { - /// Create a new [`TransactionMap`]. - pub fn new() -> TransactionMap { - TransactionMap { - tr_map: HashMap::new(), - } - } - - /// Inserts a [`Transaction`] to the map. - pub fn insert(&mut self, key: String, tr: Transaction) -> bool { - self.tr_map.insert(key, tr); - true - } - - /// Looks up a [`Transaction`] by its key. - pub fn find(&self, key: &str) -> Option<&Transaction> { - self.tr_map.get(key) - } - - /// Gets the [`Transaction`] associated with the given `key`. - pub fn get(&mut self, key: &str) -> Option<&mut Transaction> { - self.tr_map.get_mut(key) - } - - /// Deletes a [`Transaction`] by its key. - pub fn delete(&mut self, key: &str) -> Option { - self.tr_map.remove(key) - } - - /// Closes and deletes all [`Transaction`]s. - pub fn close_and_delete_all(&mut self) { - for tr in self.tr_map.values_mut() { - tr.close(); - } - self.tr_map.clear(); - } - - /// Returns its length. - pub fn size(&self) -> usize { - self.tr_map.len() - } -} diff --git a/turn/src/error.rs b/turn/src/error.rs deleted file mode 100644 index 415bd51ff..000000000 --- a/turn/src/error.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::num::ParseIntError; -use std::time::SystemTimeError; -use std::{io, net}; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic")] - ErrRelayAddressInvalid, - #[error("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed")] - ErrNoAvailableConns, - #[error("turn: PacketConnConfig must have a non-nil Conn")] - ErrConnUnset, - #[error("turn: ListenerConfig must have a non-nil Listener")] - ErrListenerUnset, - #[error("turn: RelayAddressGenerator has invalid ListeningAddress")] - ErrListeningAddressInvalid, - #[error("turn: RelayAddressGenerator in RelayConfig is unset")] - ErrRelayAddressGeneratorUnset, - #[error("turn: max retries exceeded")] - ErrMaxRetriesExceeded, - #[error("turn: MaxPort must be not 0")] - ErrMaxPortNotZero, - #[error("turn: MaxPort must be not 0")] - ErrMinPortNotZero, - #[error("turn: MaxPort less than MinPort")] - ErrMaxPortLessThanMinPort, - #[error("turn: relay_conn cannot not be nil")] - ErrNilConn, - #[error("turn: TODO")] - ErrTodo, - #[error("turn: already listening")] - ErrAlreadyListening, - #[error("turn: Server failed to close")] - ErrFailedToClose, - #[error("turn: failed to retransmit transaction")] - ErrFailedToRetransmitTransaction, - #[error("all retransmissions failed")] - ErrAllRetransmissionsFailed, - #[error("no binding found for channel")] - ErrChannelBindNotFound, - #[error("STUN server address is not set for the client")] - ErrStunserverAddressNotSet, - #[error("only one Allocate() caller is allowed")] - ErrOneAllocateOnly, - #[error("already allocated")] - ErrAlreadyAllocated, - #[error("non-STUN message from STUN server")] - ErrNonStunmessage, - #[error("failed to decode STUN message")] - ErrFailedToDecodeStun, - #[error("unexpected STUN request message")] - ErrUnexpectedStunrequestMessage, - #[error("channel number not in [0x4000, 0x7FFF]")] - ErrInvalidChannelNumber, - #[error("channelData length != len(Data)")] - ErrBadChannelDataLength, - #[error("unexpected EOF")] - ErrUnexpectedEof, - #[error("invalid value for requested family attribute")] - ErrInvalidRequestedFamilyValue, - #[error("error code 443: peer address family mismatch")] - ErrPeerAddressFamilyMismatch, - #[error("fake error")] - ErrFakeErr, - #[error("try again")] - ErrTryAgain, - #[error("use of closed network connection")] - ErrClosed, - #[error("addr is not a net.UDPAddr")] - ErrUdpaddrCast, - #[error("already closed")] - ErrAlreadyClosed, - #[error("try-lock is already locked")] - ErrDoubleLock, - #[error("transaction closed")] - ErrTransactionClosed, - #[error("wait_for_result called on non-result transaction")] - ErrWaitForResultOnNonResultTransaction, - #[error("failed to build refresh request")] - ErrFailedToBuildRefreshRequest, - #[error("failed to refresh allocation")] - ErrFailedToRefreshAllocation, - #[error("failed to get lifetime from refresh response")] - ErrFailedToGetLifetime, - #[error("too short buffer")] - ErrShortBuffer, - #[error("unexpected response type")] - ErrUnexpectedResponse, - #[error("AllocatePacketConn must be set")] - ErrAllocatePacketConnMustBeSet, - #[error("AllocateConn must be set")] - ErrAllocateConnMustBeSet, - #[error("LeveledLogger must be set")] - ErrLeveledLoggerMustBeSet, - #[error("you cannot use the same channel number with different peer")] - ErrSameChannelDifferentPeer, - #[error("allocations must not be created with nil FivTuple")] - ErrNilFiveTuple, - #[error("allocations must not be created with nil FiveTuple.src_addr")] - ErrNilFiveTupleSrcAddr, - #[error("allocations must not be created with nil FiveTuple.dst_addr")] - ErrNilFiveTupleDstAddr, - #[error("allocations must not be created with nil turnSocket")] - ErrNilTurnSocket, - #[error("allocations must not be created with a lifetime of 0")] - ErrLifetimeZero, - #[error("allocation attempt created with duplicate FiveTuple")] - ErrDupeFiveTuple, - #[error("failed to cast net.Addr to *net.UDPAddr")] - ErrFailedToCastUdpaddr, - #[error("failed to generate nonce")] - ErrFailedToGenerateNonce, - #[error("failed to send error message")] - ErrFailedToSendError, - #[error("duplicated Nonce generated, discarding request")] - ErrDuplicatedNonce, - #[error("no such user exists")] - ErrNoSuchUser, - #[error("unexpected class")] - ErrUnexpectedClass, - #[error("unexpected method")] - ErrUnexpectedMethod, - #[error("failed to handle")] - ErrFailedToHandle, - #[error("unhandled STUN packet")] - ErrUnhandledStunpacket, - #[error("unable to handle ChannelData")] - ErrUnableToHandleChannelData, - #[error("failed to create stun message from packet")] - ErrFailedToCreateStunpacket, - #[error("failed to create channel data from packet")] - ErrFailedToCreateChannelData, - #[error("relay already allocated for 5-TUPLE")] - ErrRelayAlreadyAllocatedForFiveTuple, - #[error("RequestedTransport must be UDP")] - ErrRequestedTransportMustBeUdp, - #[error("no support for DONT-FRAGMENT")] - ErrNoDontFragmentSupport, - #[error("Request must not contain RESERVATION-TOKEN and EVEN-PORT")] - ErrRequestWithReservationTokenAndEvenPort, - #[error("Request must not contain RESERVATION-TOKEN and REQUESTED-ADDRESS-FAMILY")] - ErrRequestWithReservationTokenAndReqAddressFamily, - #[error("no allocation found")] - ErrNoAllocationFound, - #[error("unable to handle send-indication, no permission added")] - ErrNoPermission, - #[error("packet write smaller than packet")] - ErrShortWrite, - #[error("no such channel bind")] - ErrNoSuchChannelBind, - #[error("failed writing to socket")] - ErrFailedWriteSocket, - #[error("parse int: {0}")] - ParseInt(#[from] ParseIntError), - #[error("parse addr: {0}")] - ParseIp(#[from] net::AddrParseError), - #[error("{0}")] - Io(#[source] IoError), - #[error("{0}")] - Util(#[from] util::Error), - #[error("{0}")] - Stun(#[from] stun::Error), - #[error("{0}")] - Other(String), -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -impl From for Error { - fn from(e: SystemTimeError) -> Self { - Error::Other(e.to_string()) - } -} diff --git a/turn/src/lib.rs b/turn/src/lib.rs deleted file mode 100644 index 2436eeee0..000000000 --- a/turn/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] -#![recursion_limit = "256"] - -pub mod allocation; -pub mod auth; -pub mod client; -mod error; -pub mod proto; -pub mod relay; -pub mod server; - -pub use error::Error; diff --git a/turn/src/proto/addr.rs b/turn/src/proto/addr.rs deleted file mode 100644 index 70cc4f45f..000000000 --- a/turn/src/proto/addr.rs +++ /dev/null @@ -1,62 +0,0 @@ -#[cfg(test)] -mod addr_test; - -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - -use super::*; - -/// `Addr` is `ip:port`. -#[derive(PartialEq, Eq, Debug)] -pub struct Addr { - ip: IpAddr, - port: u16, -} - -impl Default for Addr { - fn default() -> Self { - Addr { - ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - port: 0, - } - } -} - -impl fmt::Display for Addr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.ip, self.port) - } -} - -impl Addr { - /// Returns this network. - pub fn network(&self) -> String { - "turn".to_owned() - } - - /// Creates a new [`Addr`] from `n`. - pub fn from_socket_addr(n: &SocketAddr) -> Self { - let ip = n.ip(); - let port = n.port(); - - Addr { ip, port } - } - - /// Returns `true` if the `other` has the same IP address. - pub fn equal_ip(&self, other: &Addr) -> bool { - self.ip == other.ip - } -} - -// FiveTuple represents 5-TUPLE value. -#[derive(PartialEq, Eq, Default)] -pub struct FiveTuple { - pub client: Addr, - pub server: Addr, - pub proto: Protocol, -} - -impl fmt::Display for FiveTuple { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}->{} ({})", self.client, self.server, self.proto) - } -} diff --git a/turn/src/proto/addr/addr_test.rs b/turn/src/proto/addr/addr_test.rs deleted file mode 100644 index ba05a9b12..000000000 --- a/turn/src/proto/addr/addr_test.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::net::Ipv4Addr; - -use super::*; -use crate::error::Result; - -#[test] -fn test_addr_from_socket_addr() -> Result<()> { - let u = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234); - - let a = Addr::from_socket_addr(&u); - assert!( - u.ip() == a.ip || u.port() != a.port || u.to_string() != a.to_string(), - "not equal" - ); - assert_eq!(a.network(), "turn", "unexpected network"); - - Ok(()) -} - -#[test] -fn test_addr_equal_ip() -> Result<()> { - let a = Addr { - ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port: 1337, - }; - let b = Addr { - ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port: 1338, - }; - assert_ne!(a, b, "a != b"); - assert!(a.equal_ip(&b), "a.IP should equal to b.IP"); - - Ok(()) -} - -#[test] -fn test_five_tuple_equal() -> Result<()> { - let tests = vec![ - ("blank", FiveTuple::default(), FiveTuple::default(), true), - ( - "proto", - FiveTuple { - proto: PROTO_UDP, - ..Default::default() - }, - FiveTuple::default(), - false, - ), - ( - "server", - FiveTuple { - server: Addr { - port: 100, - ..Default::default() - }, - ..Default::default() - }, - FiveTuple::default(), - false, - ), - ( - "client", - FiveTuple { - client: Addr { - port: 100, - ..Default::default() - }, - ..Default::default() - }, - FiveTuple::default(), - false, - ), - ]; - - for (name, a, b, r) in tests { - let v = a == b; - assert_eq!(v, r, "({name}) {a} [{v}!={r}] {b}"); - } - - Ok(()) -} - -#[test] -fn test_five_tuple_string() -> Result<()> { - let s = FiveTuple { - proto: PROTO_UDP, - server: Addr { - port: 100, - ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - }, - client: Addr { - port: 200, - ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - }, - } - .to_string(); - - assert_eq!( - s, "127.0.0.1:200->127.0.0.1:100 (UDP)", - "unexpected stringer output" - ); - - Ok(()) -} diff --git a/turn/src/proto/chandata.rs b/turn/src/proto/chandata.rs deleted file mode 100644 index 58b11549b..000000000 --- a/turn/src/proto/chandata.rs +++ /dev/null @@ -1,110 +0,0 @@ -#[cfg(test)] -mod chandata_test; - -use super::channum::*; -use crate::error::*; - -const PADDING: usize = 4; - -fn nearest_padded_value_length(l: usize) -> usize { - let mut n = PADDING * (l / PADDING); - if n < l { - n += PADDING; - } - n -} - -const CHANNEL_DATA_LENGTH_SIZE: usize = 2; -const CHANNEL_DATA_NUMBER_SIZE: usize = CHANNEL_DATA_LENGTH_SIZE; -const CHANNEL_DATA_HEADER_SIZE: usize = CHANNEL_DATA_LENGTH_SIZE + CHANNEL_DATA_NUMBER_SIZE; - -/// `ChannelData` represents the `ChannelData` Message defined in -/// [RFC 5766 Section 11.4](https://www.rfc-editor.org/rfc/rfc5766#section-11.4). -#[derive(Default, Debug)] -pub struct ChannelData { - pub data: Vec, // can be subslice of Raw - pub number: ChannelNumber, - pub raw: Vec, -} - -impl PartialEq for ChannelData { - fn eq(&self, other: &Self) -> bool { - self.data == other.data && self.number == other.number - } -} - -impl ChannelData { - /// Resets length, [`Self::data`] and [`Self::raw`] length. - #[inline] - pub fn reset(&mut self) { - self.raw.clear(); - self.data.clear(); - } - - /// Encodes this to [`Self::raw`]. - pub fn encode(&mut self) { - self.raw.clear(); - self.write_header(); - self.raw.extend_from_slice(&self.data); - let padded = nearest_padded_value_length(self.raw.len()); - let bytes_to_add = padded - self.raw.len(); - if bytes_to_add > 0 { - self.raw.extend_from_slice(&vec![0; bytes_to_add]); - } - } - - /// Decodes this from [`Self::raw`]. - pub fn decode(&mut self) -> Result<()> { - let buf = &self.raw; - if buf.len() < CHANNEL_DATA_HEADER_SIZE { - return Err(Error::ErrUnexpectedEof); - } - let num = u16::from_be_bytes([buf[0], buf[1]]); - self.number = ChannelNumber(num); - if !self.number.valid() { - return Err(Error::ErrInvalidChannelNumber); - } - let l = u16::from_be_bytes([ - buf[CHANNEL_DATA_NUMBER_SIZE], - buf[CHANNEL_DATA_NUMBER_SIZE + 1], - ]) as usize; - if l > buf[CHANNEL_DATA_HEADER_SIZE..].len() { - return Err(Error::ErrBadChannelDataLength); - } - self.data = buf[CHANNEL_DATA_HEADER_SIZE..CHANNEL_DATA_HEADER_SIZE + l].to_vec(); - - Ok(()) - } - - /// Writes channel number and length. - pub fn write_header(&mut self) { - if self.raw.len() < CHANNEL_DATA_HEADER_SIZE { - // Making WriteHeader call valid even when c.Raw - // is nil or len(c.Raw) is less than needed for header. - self.raw - .resize(self.raw.len() + CHANNEL_DATA_HEADER_SIZE, 0); - } - self.raw[..CHANNEL_DATA_NUMBER_SIZE].copy_from_slice(&self.number.0.to_be_bytes()); - self.raw[CHANNEL_DATA_NUMBER_SIZE..CHANNEL_DATA_HEADER_SIZE] - .copy_from_slice(&(self.data.len() as u16).to_be_bytes()); - } - - /// Returns `true` if `buf` looks like the `ChannelData` Message. - pub fn is_channel_data(buf: &[u8]) -> bool { - if buf.len() < CHANNEL_DATA_HEADER_SIZE { - return false; - } - - if u16::from_be_bytes([ - buf[CHANNEL_DATA_NUMBER_SIZE], - buf[CHANNEL_DATA_NUMBER_SIZE + 1], - ]) > buf[CHANNEL_DATA_HEADER_SIZE..].len() as u16 - { - return false; - } - - // Quick check for channel number. - let num = ChannelNumber(u16::from_be_bytes([buf[0], buf[1]])); - num.valid() - } -} diff --git a/turn/src/proto/chandata/chandata_test.rs b/turn/src/proto/chandata/chandata_test.rs deleted file mode 100644 index 9376f6f4d..000000000 --- a/turn/src/proto/chandata/chandata_test.rs +++ /dev/null @@ -1,211 +0,0 @@ -use super::*; - -#[test] -fn test_channel_data_encode() -> Result<()> { - let mut d = ChannelData { - data: vec![1, 2, 3, 4], - number: ChannelNumber(MIN_CHANNEL_NUMBER + 1), - ..Default::default() - }; - d.encode(); - - let mut b = ChannelData::default(); - b.raw.extend_from_slice(&d.raw); - b.decode()?; - - assert_eq!(b, d, "not equal"); - - assert!( - ChannelData::is_channel_data(&b.raw) && ChannelData::is_channel_data(&d.raw), - "unexpected IsChannelData" - ); - - Ok(()) -} - -#[test] -fn test_channel_data_equal() -> Result<()> { - let tests = vec![ - ( - "equal", - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3], - ..Default::default() - }, - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3], - ..Default::default() - }, - true, - ), - ( - "number", - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER + 1), - data: vec![1, 2, 3], - ..Default::default() - }, - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3], - ..Default::default() - }, - false, - ), - ( - "length", - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3, 4], - ..Default::default() - }, - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3], - ..Default::default() - }, - false, - ), - ( - "data", - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 2], - ..Default::default() - }, - ChannelData { - number: ChannelNumber(MIN_CHANNEL_NUMBER), - data: vec![1, 2, 3], - ..Default::default() - }, - false, - ), - ]; - - for (name, a, b, r) in tests { - let v = a == b; - assert_eq!(v, r, "unexpected: ({name}) {r} != {r}"); - } - - Ok(()) -} - -#[test] -fn test_channel_data_decode() -> Result<()> { - let tests = vec![ - ("small", vec![1, 2, 3], Error::ErrUnexpectedEof), - ( - "zeroes", - vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - Error::ErrInvalidChannelNumber, - ), - ( - "bad chan number", - vec![63, 255, 0, 0, 0, 4, 0, 0, 1, 2, 3, 4], - Error::ErrInvalidChannelNumber, - ), - ( - "bad length", - vec![0x40, 0x40, 0x02, 0x23, 0x16, 0, 0, 0, 0, 0, 0, 0], - Error::ErrBadChannelDataLength, - ), - ]; - - for (name, buf, want_err) in tests { - let mut m = ChannelData { - raw: buf, - ..Default::default() - }; - if let Err(err) = m.decode() { - assert_eq!(want_err, err, "unexpected: ({name}) {want_err} != {err}"); - } else { - panic!("expected error, but got ok"); - } - } - - Ok(()) -} - -#[test] -fn test_channel_data_reset() -> Result<()> { - let mut d = ChannelData { - data: vec![1, 2, 3, 4], - number: ChannelNumber(MIN_CHANNEL_NUMBER + 1), - ..Default::default() - }; - d.encode(); - let mut buf = vec![0; d.raw.len()]; - buf.copy_from_slice(&d.raw); - d.reset(); - d.raw = buf; - d.decode()?; - - Ok(()) -} - -#[test] -fn test_is_channel_data() -> Result<()> { - let tests = vec![ - ("small", vec![1, 2, 3, 4], false), - ("zeroes", vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], false), - ]; - - for (name, buf, r) in tests { - let v = ChannelData::is_channel_data(&buf); - assert_eq!(v, r, "unexpected: ({name}) {r} != {v}"); - } - - Ok(()) -} - -const CHANDATA_TEST_HEX: [&str; 2] = [ - "40000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a33776c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e001eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442", - "4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d0403023011310f300d06035504030c06576562525443301e170d3138303831313033353230305a170d3138303931313033353230305a3011310f300d06035504030c065765625254433059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469cfb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd74b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4bc9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a96e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd000000000000001300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e9a19240b3a68b95699546c044f863cf8349bc8046214fefd000000000000001400010116fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed06898c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b", -]; - -#[test] -fn test_chrome_channel_data() -> Result<()> { - let mut data = vec![]; - let mut messages = vec![]; - - // Decoding hex data into binary. - for h in &CHANDATA_TEST_HEX { - let b = match hex::decode(h) { - Ok(b) => b, - Err(_) => return Err(Error::Other("hex decode error".to_owned())), - }; - data.push(b); - } - - // All hex streams decoded to raw binary format and stored in data slice. - // Decoding packets to messages. - for packet in data { - let mut m = ChannelData { - raw: packet, - ..Default::default() - }; - - m.decode()?; - let mut encoded = ChannelData { - data: m.data.clone(), - number: m.number, - ..Default::default() - }; - encoded.encode(); - - let mut decoded = ChannelData { - raw: encoded.raw.clone(), - ..Default::default() - }; - - decoded.decode()?; - assert_eq!(decoded, m, "should be equal"); - - messages.push(m); - } - assert_eq!(messages.len(), 2, "unexpected message slice list"); - - Ok(()) -} diff --git a/turn/src/proto/channum.rs b/turn/src/proto/channum.rs deleted file mode 100644 index 406bbcfb2..000000000 --- a/turn/src/proto/channum.rs +++ /dev/null @@ -1,70 +0,0 @@ -#[cfg(test)] -mod channnum_test; - -use std::fmt; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -// 16 bits of uint + 16 bits of RFFU = 0. -const CHANNEL_NUMBER_SIZE: usize = 4; - -// See https://tools.ietf.org/html/rfc5766#section-11: -// -// 0x4000 through 0x7FFF: These values are the allowed channel -// numbers (16,383 possible values). -pub const MIN_CHANNEL_NUMBER: u16 = 0x4000; -pub const MAX_CHANNEL_NUMBER: u16 = 0x7FFF; - -/// `ChannelNumber` represents `CHANNEL-NUMBER` attribute. Encoded as `u16`. -/// -/// The `CHANNEL-NUMBER` attribute contains the number of the channel. -/// -/// [RFC 5766 Section 14.1](https://www.rfc-editor.org/rfc/rfc5766#section-14.1). -#[derive(Default, Eq, PartialEq, Debug, Copy, Clone, Hash)] -pub struct ChannelNumber(pub u16); - -impl fmt::Display for ChannelNumber { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Setter for ChannelNumber { - /// Adds `CHANNEL-NUMBER` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0; CHANNEL_NUMBER_SIZE]; - v[..2].copy_from_slice(&self.0.to_be_bytes()); - // v[2:4] are zeroes (RFFU = 0) - m.add(ATTR_CHANNEL_NUMBER, &v); - Ok(()) - } -} - -impl Getter for ChannelNumber { - /// Decodes `CHANNEL-NUMBER` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_CHANNEL_NUMBER)?; - - check_size(ATTR_CHANNEL_NUMBER, v.len(), CHANNEL_NUMBER_SIZE)?; - - //_ = v[CHANNEL_NUMBER_SIZE-1] // asserting length - self.0 = u16::from_be_bytes([v[0], v[1]]); - // v[2:4] is RFFU and equals to 0. - Ok(()) - } -} - -impl ChannelNumber { - /// Returns true if c in `[0x4000, 0x7FFF]`. - fn is_channel_number_valid(&self) -> bool { - self.0 >= MIN_CHANNEL_NUMBER && self.0 <= MAX_CHANNEL_NUMBER - } - - /// returns `true` if channel number has correct value that complies - /// [RFC 5766 Section 11](https://www.rfc-editor.org/rfc/rfc5766#section-11) range. - pub fn valid(&self) -> bool { - self.is_channel_number_valid() - } -} diff --git a/turn/src/proto/channum/channnum_test.rs b/turn/src/proto/channum/channnum_test.rs deleted file mode 100644 index f05756802..000000000 --- a/turn/src/proto/channum/channnum_test.rs +++ /dev/null @@ -1,81 +0,0 @@ -use super::*; - -#[test] -fn test_channel_number_string() -> Result<(), stun::Error> { - let n = ChannelNumber(112); - assert_eq!(n.to_string(), "112", "bad string {n}, expected 112"); - Ok(()) -} - -/* -#[test] -fn test_channel_number_NoAlloc() -> Result<(), stun::Error> { - let mut m = Message::default(); - - if wasAllocs(func() { - // Case with ChannelNumber on stack. - n: = ChannelNumber(6) - n.AddTo(m) //nolint - m.Reset() - }) { - t.Error("Unexpected allocations") - } - - n: = ChannelNumber(12) - nP: = &n - if wasAllocs(func() { - // On heap. - nP.AddTo(m) //nolint - m.Reset() - }) { - t.Error("Unexpected allocations") - } - Ok(()) -} -*/ - -#[test] -fn test_channel_number_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let n = ChannelNumber(6); - n.add_to(&mut m)?; - m.write_header(); - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - - let mut num_decoded = ChannelNumber::default(); - num_decoded.get_from(&decoded)?; - assert_eq!(num_decoded, n, "Decoded {num_decoded}, expected {n}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut n_handle = ChannelNumber::default(); - if let Err(err) = n_handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } else { - panic!("expected error, but got ok"); - } - - m.add(ATTR_CHANNEL_NUMBER, &[1, 2, 3]); - - if let Err(err) = n_handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, but got ok"); - } - } - } - - Ok(()) -} diff --git a/turn/src/proto/data.rs b/turn/src/proto/data.rs deleted file mode 100644 index 03fdf9b4d..000000000 --- a/turn/src/proto/data.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[cfg(test)] -mod data_test; - -use stun::attributes::*; -use stun::message::*; - -/// `Data` represents `DATA` attribute. -/// -/// The `DATA` attribute is present in all Send and Data indications. The -/// value portion of this attribute is variable length and consists of -/// the application data (that is, the data that would immediately follow -/// the UDP header if the data was been sent directly between the client -/// and the peer). -/// -/// [RFC 5766 Section 14.4](https://www.rfc-editor.org/rfc/rfc5766#section-14.4). -#[derive(Default, Debug, PartialEq, Eq)] -pub struct Data(pub Vec); - -impl Setter for Data { - /// Adds `DATA` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - m.add(ATTR_DATA, &self.0); - Ok(()) - } -} - -impl Getter for Data { - /// Decodes `DATA` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - self.0 = m.get(ATTR_DATA)?; - Ok(()) - } -} diff --git a/turn/src/proto/data/data_test.rs b/turn/src/proto/data/data_test.rs deleted file mode 100644 index 813047af2..000000000 --- a/turn/src/proto/data/data_test.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::*; - -#[test] -fn test_data_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let d = Data(vec![1, 2, 33, 44, 0x13, 0xaf]); - d.add_to(&mut m)?; - m.write_header(); - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - - let mut data_decoded = Data::default(); - data_decoded.get_from(&decoded)?; - assert_eq!(data_decoded, d); - - //"HandleErr" - { - let m = Message::new(); - let mut handle = Data::default(); - if let Err(err) = handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } - } - } - Ok(()) -} diff --git a/turn/src/proto/dontfrag.rs b/turn/src/proto/dontfrag.rs deleted file mode 100644 index 621586ff2..000000000 --- a/turn/src/proto/dontfrag.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[cfg(test)] -mod dontfrag_test; - -use stun::attributes::*; -use stun::message::*; - -/// `DontFragmentAttr` represents `DONT-FRAGMENT` attribute. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct DontFragmentAttr; - -impl Setter for DontFragmentAttr { - /// Adds `DONT-FRAGMENT` attribute to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - m.add(ATTR_DONT_FRAGMENT, &[]); - Ok(()) - } -} - -impl Getter for DontFragmentAttr { - /// Returns true if `DONT-FRAGMENT` attribute is set. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let _ = m.get(ATTR_DONT_FRAGMENT)?; - Ok(()) - } -} diff --git a/turn/src/proto/dontfrag/dontfrag_test.rs b/turn/src/proto/dontfrag/dontfrag_test.rs deleted file mode 100644 index a981cd778..000000000 --- a/turn/src/proto/dontfrag/dontfrag_test.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::*; - -#[test] -fn test_dont_fragment_false() -> Result<(), stun::Error> { - let mut dont_fragment = DontFragmentAttr; - - let mut m = Message::new(); - m.write_header(); - assert!(dont_fragment.get_from(&m).is_err(), "should not be set"); - - Ok(()) -} - -#[test] -fn test_dont_fragment_add_to() -> Result<(), stun::Error> { - let mut dont_fragment = DontFragmentAttr; - - let mut m = Message::new(); - dont_fragment.add_to(&mut m)?; - m.write_header(); - - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - assert!(dont_fragment.get_from(&m).is_ok(), "should be set"); - - Ok(()) -} diff --git a/turn/src/proto/evenport.rs b/turn/src/proto/evenport.rs deleted file mode 100644 index fe364d18a..000000000 --- a/turn/src/proto/evenport.rs +++ /dev/null @@ -1,63 +0,0 @@ -#[cfg(test)] -mod evenport_test; - -use std::fmt; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -/// `EvenPort` represents `EVEN-PORT` attribute. -/// -/// This attribute allows the client to request that the port in the -/// relayed transport address be even, and (optionally) that the server -/// reserve the next-higher port number. -/// -/// [RFC 5766 Section 14.6](https://www.rfc-editor.org/rfc/rfc5766#section-14.6). -#[derive(Default, Debug, PartialEq, Eq)] -pub struct EvenPort { - /// `reserve_port` means that the server is requested to reserve - /// the next-higher port number (on the same IP address) - /// for a subsequent allocation. - reserve_port: bool, -} - -impl fmt::Display for EvenPort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.reserve_port { - write!(f, "reserve: true") - } else { - write!(f, "reserve: false") - } - } -} - -const EVEN_PORT_SIZE: usize = 1; -const FIRST_BIT_SET: u8 = 0b10000000; //FIXME? (1 << 8) - 1; - -impl Setter for EvenPort { - /// Adds `EVEN-PORT` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0; EVEN_PORT_SIZE]; - if self.reserve_port { - // Set first bit to 1. - v[0] = FIRST_BIT_SET; - } - m.add(ATTR_EVEN_PORT, &v); - Ok(()) - } -} - -impl Getter for EvenPort { - /// Decodes `EVEN-PORT` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_EVEN_PORT)?; - - check_size(ATTR_EVEN_PORT, v.len(), EVEN_PORT_SIZE)?; - - if v[0] & FIRST_BIT_SET > 0 { - self.reserve_port = true; - } - Ok(()) - } -} diff --git a/turn/src/proto/evenport/evenport_test.rs b/turn/src/proto/evenport/evenport_test.rs deleted file mode 100644 index 57cc8bd9f..000000000 --- a/turn/src/proto/evenport/evenport_test.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::*; - -#[test] -fn test_even_port_string() -> Result<(), stun::Error> { - let mut p = EvenPort::default(); - assert_eq!( - p.to_string(), - "reserve: false", - "bad value {p} for reselve: false" - ); - - p.reserve_port = true; - assert_eq!( - p.to_string(), - "reserve: true", - "bad value {p} for reselve: true" - ); - - Ok(()) -} - -#[test] -fn test_even_port_false() -> Result<(), stun::Error> { - let mut m = Message::new(); - let p = EvenPort { - reserve_port: false, - }; - p.add_to(&mut m)?; - m.write_header(); - - let mut decoded = Message::new(); - let mut port = EvenPort::default(); - decoded.write(&m.raw)?; - port.get_from(&m)?; - assert_eq!(port, p); - - Ok(()) -} - -#[test] -fn test_even_port_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let p = EvenPort { reserve_port: true }; - p.add_to(&mut m)?; - m.write_header(); - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - let mut port = EvenPort::default(); - port.get_from(&decoded)?; - assert_eq!(port, p, "Decoded {port}, expected {p}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut handle = EvenPort::default(); - if let Err(err) = handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } - m.add(ATTR_EVEN_PORT, &[1, 2, 3]); - if let Err(err) = handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, but got ok"); - } - } - } - - Ok(()) -} diff --git a/turn/src/proto/lifetime.rs b/turn/src/proto/lifetime.rs deleted file mode 100644 index 449bc277e..000000000 --- a/turn/src/proto/lifetime.rs +++ /dev/null @@ -1,59 +0,0 @@ -#[cfg(test)] -mod lifetime_test; - -use std::fmt; -use std::time::Duration; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -/// `DEFAULT_LIFETIME` in RFC 5766 is 10 minutes. -/// -/// [RFC 5766 Section 2.2](https://www.rfc-editor.org/rfc/rfc5766#section-2.2). -pub const DEFAULT_LIFETIME: Duration = Duration::from_secs(10 * 60); - -/// `Lifetime` represents `LIFETIME` attribute. -/// -/// The `LIFETIME` attribute represents the duration for which the server -/// will maintain an allocation in the absence of a refresh. The value -/// portion of this attribute is 4-bytes long and consists of a 32-bit -/// unsigned integral value representing the number of seconds remaining -/// until expiration. -/// -/// [RFC 5766 Section 14.2](https://www.rfc-editor.org/rfc/rfc5766#section-14.2). -#[derive(Default, Debug, PartialEq, Eq)] -pub struct Lifetime(pub Duration); - -impl fmt::Display for Lifetime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}s", self.0.as_secs()) - } -} - -// uint32 seconds -const LIFETIME_SIZE: usize = 4; // 4 bytes, 32 bits - -impl Setter for Lifetime { - /// Adds `LIFETIME` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0; LIFETIME_SIZE]; - v.copy_from_slice(&(self.0.as_secs() as u32).to_be_bytes()); - m.add(ATTR_LIFETIME, &v); - Ok(()) - } -} - -impl Getter for Lifetime { - /// Decodes `LIFETIME` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_LIFETIME)?; - - check_size(ATTR_LIFETIME, v.len(), LIFETIME_SIZE)?; - - let seconds = u32::from_be_bytes([v[0], v[1], v[2], v[3]]); - self.0 = Duration::from_secs(seconds as u64); - - Ok(()) - } -} diff --git a/turn/src/proto/lifetime/lifetime_test.rs b/turn/src/proto/lifetime/lifetime_test.rs deleted file mode 100644 index 2ec54ac31..000000000 --- a/turn/src/proto/lifetime/lifetime_test.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::*; - -#[test] -fn test_lifetime_string() -> Result<(), stun::Error> { - let l = Lifetime(Duration::from_secs(10)); - assert_eq!(l.to_string(), "10s", "bad string {l}, expected 10s"); - - Ok(()) -} - -#[test] -fn test_lifetime_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let l = Lifetime(Duration::from_secs(10)); - l.add_to(&mut m)?; - m.write_header(); - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - - let mut life = Lifetime::default(); - life.get_from(&decoded)?; - assert_eq!(life, l, "Decoded {life}, expected {l}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut n_handle = Lifetime::default(); - if let Err(err) = n_handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } else { - panic!("expected error, but got ok"); - } - m.add(ATTR_LIFETIME, &[1, 2, 3]); - - if let Err(err) = n_handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, but got ok"); - } - } - } - - Ok(()) -} diff --git a/turn/src/proto/mod.rs b/turn/src/proto/mod.rs deleted file mode 100644 index ada03ffd8..000000000 --- a/turn/src/proto/mod.rs +++ /dev/null @@ -1,70 +0,0 @@ -#[cfg(test)] -mod proto_test; - -pub mod addr; -pub mod chandata; -pub mod channum; -pub mod data; -pub mod dontfrag; -pub mod evenport; -pub mod lifetime; -pub mod peeraddr; -pub mod relayaddr; -pub mod reqfamily; -pub mod reqtrans; -pub mod rsrvtoken; - -use std::fmt; - -use stun::message::*; - -// proto implements RFC 5766 Traversal Using Relays around NAT. - -/// `Protocol` is IANA assigned protocol number. -#[derive(PartialEq, Eq, Default, Debug, Clone, Copy, Hash)] -pub struct Protocol(pub u8); - -/// `PROTO_TCP` is IANA assigned protocol number for TCP. -pub const PROTO_TCP: Protocol = Protocol(6); -/// `PROTO_UDP` is IANA assigned protocol number for UDP. -pub const PROTO_UDP: Protocol = Protocol(17); - -impl fmt::Display for Protocol { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let others = format!("{}", self.0); - let s = match *self { - PROTO_UDP => "UDP", - PROTO_TCP => "TCP", - _ => others.as_str(), - }; - - write!(f, "{s}") - } -} - -// Default ports for TURN from RFC 5766 Section 4. - -/// `DEFAULT_PORT` for TURN is same as STUN. -pub const DEFAULT_PORT: u16 = stun::DEFAULT_PORT; -/// `DEFAULT_TLSPORT` is for TURN over TLS and is same as STUN. -pub const DEFAULT_TLS_PORT: u16 = stun::DEFAULT_TLS_PORT; - -/// Shorthand for create permission request type. -pub fn create_permission_request() -> MessageType { - MessageType::new(METHOD_CREATE_PERMISSION, CLASS_REQUEST) -} - -/// Shorthand for allocation request message type. -pub fn allocate_request() -> MessageType { - MessageType::new(METHOD_ALLOCATE, CLASS_REQUEST) -} - -/// Shorthand for send indication message type. -pub fn send_indication() -> MessageType { - MessageType::new(METHOD_SEND, CLASS_INDICATION) -} - -/// Shorthand for refresh request message type. -pub fn refresh_request() -> MessageType { - MessageType::new(METHOD_REFRESH, CLASS_REQUEST) -} diff --git a/turn/src/proto/peeraddr.rs b/turn/src/proto/peeraddr.rs deleted file mode 100644 index 58c1f20a3..000000000 --- a/turn/src/proto/peeraddr.rs +++ /dev/null @@ -1,71 +0,0 @@ -#[cfg(test)] -mod peeraddr_test; - -use std::fmt; -use std::net::{IpAddr, Ipv4Addr}; - -use stun::attributes::*; -use stun::message::*; -use stun::xoraddr::*; - -/// `PeerAddress` implements `XOR-PEER-ADDRESS` attribute. -/// -/// The `XOR-PEER-ADDRESS` specifies the address and port of the peer as -/// seen from the TURN server. (For example, the peer's server-reflexive -/// transport address if the peer is behind a NAT.) -/// -/// [RFC 5766 Section 14.3](https://www.rfc-editor.org/rfc/rfc5766#section-14.3). -#[derive(PartialEq, Eq, Debug)] -pub struct PeerAddress { - pub ip: IpAddr, - pub port: u16, -} - -impl Default for PeerAddress { - fn default() -> Self { - PeerAddress { - ip: IpAddr::V4(Ipv4Addr::from(0)), - port: 0, - } - } -} - -impl fmt::Display for PeerAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.ip { - IpAddr::V4(_) => write!(f, "{}:{}", self.ip, self.port), - IpAddr::V6(_) => write!(f, "[{}]:{}", self.ip, self.port), - } - } -} - -impl Setter for PeerAddress { - /// Adds `XOR-PEER-ADDRESS` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let a = XorMappedAddress { - ip: self.ip, - port: self.port, - }; - a.add_to_as(m, ATTR_XOR_PEER_ADDRESS) - } -} - -impl Getter for PeerAddress { - /// Decodes `XOR-PEER-ADDRESS` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let mut a = XorMappedAddress::default(); - a.get_from_as(m, ATTR_XOR_PEER_ADDRESS)?; - self.ip = a.ip; - self.port = a.port; - Ok(()) - } -} - -/// `PeerAddress` implements `XOR-PEER-ADDRESS` attribute. -/// -/// The `XOR-PEER-ADDRESS` specifies the address and port of the peer as -/// seen from the TURN server. (For example, the peer's server-reflexive -/// transport address if the peer is behind a NAT.) -/// -/// [RFC 5766 Section 14.3](https://www.rfc-editor.org/rfc/rfc5766#section-14.3). -pub type XorPeerAddress = PeerAddress; diff --git a/turn/src/proto/peeraddr/peeraddr_test.rs b/turn/src/proto/peeraddr/peeraddr_test.rs deleted file mode 100644 index 353788c99..000000000 --- a/turn/src/proto/peeraddr/peeraddr_test.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::net::Ipv4Addr; - -use super::*; - -#[test] -fn test_peer_address() -> Result<(), stun::Error> { - // Simple tests because already tested in stun. - let a = PeerAddress { - ip: IpAddr::V4(Ipv4Addr::new(111, 11, 1, 2)), - port: 333, - }; - - assert_eq!(a.to_string(), "111.11.1.2:333", "invalid string"); - - let mut m = Message::new(); - a.add_to(&mut m)?; - m.write_header(); - - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - - let mut a_got = PeerAddress::default(); - a_got.get_from(&decoded)?; - - Ok(()) -} diff --git a/turn/src/proto/proto_test.rs b/turn/src/proto/proto_test.rs deleted file mode 100644 index c40d1a755..000000000 --- a/turn/src/proto/proto_test.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; -use crate::error::*; - -const CHROME_ALLOC_REQ_TEST_HEX: [&str; 4] = [ - "000300242112a442626b4a6849664c3630526863802f0016687474703a2f2f6c6f63616c686f73743a333030302f00000019000411000000", - "011300582112a442626b4a6849664c36305268630009001000000401556e617574686f72697a656400150010356130323039623563623830363130360014000b61312e63796465762e7275758022001a436f7475726e2d342e352e302e33202764616e204569646572272300", - "0003006c2112a442324e50695a437a4634535034802f0016687474703a2f2f6c6f63616c686f73743a333030302f000000190004110000000006000665726e61646f00000014000b61312e63796465762e7275000015001035613032303962356362383036313036000800145c8743f3b64bec0880cdd8d476d37b801a6c3d33", - "010300582112a442324e50695a437a4634535034001600080001fb922b1ab211002000080001adb2f49f38ae000d0004000002588022001a436f7475726e2d342e352e302e33202764616e204569646572277475000800145d7e85b767a519ffce91dbf0a96775e370db92e3", -]; - -#[test] -fn test_chrome_alloc_request() -> Result<()> { - let mut data = vec![]; - let mut messages = vec![]; - - // Decoding hex data into binary. - for h in &CHROME_ALLOC_REQ_TEST_HEX { - let b = match hex::decode(h) { - Ok(b) => b, - Err(_) => return Err(Error::Other("hex decode error".to_owned())), - }; - data.push(b); - } - - // All hex streams decoded to raw binary format and stored in data slice. - // Decoding packets to messages. - for packet in data { - let mut m = Message::new(); - m.write(&packet)?; - messages.push(m); - } - assert_eq!(messages.len(), 4, "unexpected message slice list"); - - Ok(()) -} diff --git a/turn/src/proto/relayaddr.rs b/turn/src/proto/relayaddr.rs deleted file mode 100644 index a5675e16f..000000000 --- a/turn/src/proto/relayaddr.rs +++ /dev/null @@ -1,69 +0,0 @@ -#[cfg(test)] -mod relayaddr_test; - -use std::fmt; -use std::net::{IpAddr, Ipv4Addr}; - -use stun::attributes::*; -use stun::message::*; -use stun::xoraddr::*; - -/// `RelayedAddress` implements `XOR-RELAYED-ADDRESS` attribute. -/// -/// It specifies the address and port that the server allocated to the -/// client. It is encoded in the same way as `XOR-MAPPED-ADDRESS`. -/// -/// [RFC 5766 Section 14.5](https://www.rfc-editor.org/rfc/rfc5766#section-14.5). -#[derive(PartialEq, Eq, Debug)] -pub struct RelayedAddress { - pub ip: IpAddr, - pub port: u16, -} - -impl Default for RelayedAddress { - fn default() -> Self { - RelayedAddress { - ip: IpAddr::V4(Ipv4Addr::from(0)), - port: 0, - } - } -} - -impl fmt::Display for RelayedAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.ip { - IpAddr::V4(_) => write!(f, "{}:{}", self.ip, self.port), - IpAddr::V6(_) => write!(f, "[{}]:{}", self.ip, self.port), - } - } -} - -impl Setter for RelayedAddress { - /// Adds `XOR-PEER-ADDRESS` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let a = XorMappedAddress { - ip: self.ip, - port: self.port, - }; - a.add_to_as(m, ATTR_XOR_RELAYED_ADDRESS) - } -} - -impl Getter for RelayedAddress { - /// Decodes `XOR-PEER-ADDRESS` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let mut a = XorMappedAddress::default(); - a.get_from_as(m, ATTR_XOR_RELAYED_ADDRESS)?; - self.ip = a.ip; - self.port = a.port; - Ok(()) - } -} - -/// `XorRelayedAddress` implements `XOR-RELAYED-ADDRESS` attribute. -/// -/// It specifies the address and port that the server allocated to the -/// client. It is encoded in the same way as `XOR-MAPPED-ADDRESS`. -/// -/// [RFC 5766 Section 14.5](https://www.rfc-editor.org/rfc/rfc5766#section-14.5). -pub type XorRelayedAddress = RelayedAddress; diff --git a/turn/src/proto/relayaddr/relayaddr_test.rs b/turn/src/proto/relayaddr/relayaddr_test.rs deleted file mode 100644 index 8ea3e04bb..000000000 --- a/turn/src/proto/relayaddr/relayaddr_test.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::net::Ipv4Addr; - -use super::*; - -#[test] -fn test_relayed_address() -> Result<(), stun::Error> { - // Simple tests because already tested in stun. - let a = RelayedAddress { - ip: IpAddr::V4(Ipv4Addr::new(111, 11, 1, 2)), - port: 333, - }; - - assert_eq!(a.to_string(), "111.11.1.2:333", "invalid string"); - - let mut m = Message::new(); - a.add_to(&mut m)?; - m.write_header(); - - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - - let mut a_got = RelayedAddress::default(); - a_got.get_from(&decoded)?; - - Ok(()) -} diff --git a/turn/src/proto/reqfamily.rs b/turn/src/proto/reqfamily.rs deleted file mode 100644 index 409ddc516..000000000 --- a/turn/src/proto/reqfamily.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[cfg(test)] -mod reqfamily_test; - -use std::fmt; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -// Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1. -pub const REQUESTED_FAMILY_IPV4: RequestedAddressFamily = RequestedAddressFamily(0x01); -pub const REQUESTED_FAMILY_IPV6: RequestedAddressFamily = RequestedAddressFamily(0x02); - -/// `RequestedAddressFamily` represents the `REQUESTED-ADDRESS-FAMILY` Attribute as -/// defined in [RFC 6156 Section 4.1.1](https://www.rfc-editor.org/rfc/rfc6156#section-4.1.1). -#[derive(Debug, Default, PartialEq, Eq)] -pub struct RequestedAddressFamily(pub u8); - -impl fmt::Display for RequestedAddressFamily { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - REQUESTED_FAMILY_IPV4 => "IPv4", - REQUESTED_FAMILY_IPV6 => "IPv6", - _ => "unknown", - }; - write!(f, "{s}") - } -} - -const REQUESTED_FAMILY_SIZE: usize = 4; - -impl Setter for RequestedAddressFamily { - /// Adds `REQUESTED-ADDRESS-FAMILY` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0; REQUESTED_FAMILY_SIZE]; - v[0] = self.0; - // b[1:4] is RFFU = 0. - // The RFFU field MUST be set to zero on transmission and MUST be - // ignored on reception. It is reserved for future uses. - m.add(ATTR_REQUESTED_ADDRESS_FAMILY, &v); - Ok(()) - } -} - -impl Getter for RequestedAddressFamily { - /// Decodes `REQUESTED-ADDRESS-FAMILY` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_REQUESTED_ADDRESS_FAMILY)?; - check_size( - ATTR_REQUESTED_ADDRESS_FAMILY, - v.len(), - REQUESTED_FAMILY_SIZE, - )?; - - if v[0] != REQUESTED_FAMILY_IPV4.0 && v[0] != REQUESTED_FAMILY_IPV6.0 { - return Err(stun::Error::Other("ErrInvalidRequestedFamilyValue".into())); - } - self.0 = v[0]; - Ok(()) - } -} diff --git a/turn/src/proto/reqfamily/reqfamily_test.rs b/turn/src/proto/reqfamily/reqfamily_test.rs deleted file mode 100644 index aa56bef35..000000000 --- a/turn/src/proto/reqfamily/reqfamily_test.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::*; - -#[test] -fn test_requested_address_family_string() -> Result<(), stun::Error> { - assert_eq!( - REQUESTED_FAMILY_IPV4.to_string(), - "IPv4", - "bad string {}, expected {}", - REQUESTED_FAMILY_IPV4, - "IPv4" - ); - - assert_eq!( - REQUESTED_FAMILY_IPV6.to_string(), - "IPv6", - "bad string {}, expected {}", - REQUESTED_FAMILY_IPV6, - "IPv6" - ); - - assert_eq!( - RequestedAddressFamily(0x04).to_string(), - "unknown", - "should be unknown" - ); - - Ok(()) -} - -#[test] -fn test_requested_address_family_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let r = REQUESTED_FAMILY_IPV4; - r.add_to(&mut m)?; - m.write_header(); - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - let mut req = RequestedAddressFamily::default(); - req.get_from(&decoded)?; - assert_eq!(req, r, "Decoded {req}, expected {r}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut handle = RequestedAddressFamily::default(); - if let Err(err) = handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } else { - panic!("expected error, but got ok"); - } - m.add(ATTR_REQUESTED_ADDRESS_FAMILY, &[1, 2, 3]); - if let Err(err) = handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, but got ok"); - } - m.reset(); - m.add(ATTR_REQUESTED_ADDRESS_FAMILY, &[5, 0, 0, 0]); - assert!( - handle.get_from(&m).is_err(), - "should error on invalid value" - ); - } - } - - Ok(()) -} diff --git a/turn/src/proto/reqtrans.rs b/turn/src/proto/reqtrans.rs deleted file mode 100644 index 0d2a64349..000000000 --- a/turn/src/proto/reqtrans.rs +++ /dev/null @@ -1,54 +0,0 @@ -#[cfg(test)] -mod reqtrans_test; - -use std::fmt; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -use super::*; - -/// `RequestedTransport` represents `REQUESTED-TRANSPORT` attribute. -/// -/// This attribute is used by the client to request a specific transport -/// protocol for the allocated transport address. RFC 5766 only allows the use of -/// codepoint 17 (User Datagram protocol). -/// -/// [RFC 5766 Section 14.7](https://www.rfc-editor.org/rfc/rfc5766#section-14.7). -#[derive(Default, Debug, PartialEq, Eq)] -pub struct RequestedTransport { - pub protocol: Protocol, -} - -impl fmt::Display for RequestedTransport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "protocol: {}", self.protocol) - } -} - -const REQUESTED_TRANSPORT_SIZE: usize = 4; - -impl Setter for RequestedTransport { - /// Adds `REQUESTED-TRANSPORT` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - let mut v = vec![0; REQUESTED_TRANSPORT_SIZE]; - v[0] = self.protocol.0; - // b[1:4] is RFFU = 0. - // The RFFU field MUST be set to zero on transmission and MUST be - // ignored on reception. It is reserved for future uses. - m.add(ATTR_REQUESTED_TRANSPORT, &v); - Ok(()) - } -} - -impl Getter for RequestedTransport { - /// Decodes `REQUESTED-TRANSPORT` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_REQUESTED_TRANSPORT)?; - - check_size(ATTR_REQUESTED_TRANSPORT, v.len(), REQUESTED_TRANSPORT_SIZE)?; - self.protocol = Protocol(v[0]); - Ok(()) - } -} diff --git a/turn/src/proto/reqtrans/reqtrans_test.rs b/turn/src/proto/reqtrans/reqtrans_test.rs deleted file mode 100644 index 30a3e9463..000000000 --- a/turn/src/proto/reqtrans/reqtrans_test.rs +++ /dev/null @@ -1,75 +0,0 @@ -use super::*; - -#[test] -fn test_requested_transport_string() -> Result<(), stun::Error> { - let mut r = RequestedTransport { - protocol: PROTO_UDP, - }; - assert_eq!( - r.to_string(), - "protocol: UDP", - "bad string {}, expected {}", - r, - "protocol: UDP", - ); - r.protocol = Protocol(254); - if r.to_string() != "protocol: 254" { - assert_eq!( - r.to_string(), - "protocol: UDP", - "bad string {}, expected {}", - r, - "protocol: 254", - ); - } - - Ok(()) -} - -#[test] -fn test_requested_transport_add_to() -> Result<(), stun::Error> { - let mut m = Message::new(); - let r = RequestedTransport { - protocol: PROTO_UDP, - }; - r.add_to(&mut m)?; - m.write_header(); - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - let mut req = RequestedTransport { - protocol: PROTO_UDP, - }; - req.get_from(&decoded)?; - assert_eq!(req, r, "Decoded {req}, expected {r}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut handle = RequestedTransport::default(); - if let Err(err) = handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } else { - panic!("expected error, got ok"); - } - - m.add(ATTR_REQUESTED_TRANSPORT, &[1, 2, 3]); - if let Err(err) = handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, got ok"); - } - } - } - - Ok(()) -} diff --git a/turn/src/proto/rsrvtoken.rs b/turn/src/proto/rsrvtoken.rs deleted file mode 100644 index 6e4ac9663..000000000 --- a/turn/src/proto/rsrvtoken.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[cfg(test)] -mod rsrvtoken_test; - -use stun::attributes::*; -use stun::checks::*; -use stun::message::*; - -/// `ReservationToken` represents `RESERVATION-TOKEN` attribute. -/// -/// The `RESERVATION-TOKEN` attribute contains a token that uniquely -/// identifies a relayed transport address being held in reserve by the -/// server. The server includes this attribute in a success response to -/// tell the client about the token, and the client includes this -/// attribute in a subsequent Allocate request to request the server use -/// that relayed transport address for the allocation. -/// -/// [RFC 5766 Section 14.9](https://www.rfc-editor.org/rfc/rfc5766#section-14.9). -#[derive(Debug, Default, PartialEq, Eq)] -pub struct ReservationToken(pub Vec); - -const RESERVATION_TOKEN_SIZE: usize = 8; // 8 bytes - -impl Setter for ReservationToken { - /// Adds `RESERVATION-TOKEN` to message. - fn add_to(&self, m: &mut Message) -> Result<(), stun::Error> { - check_size(ATTR_RESERVATION_TOKEN, self.0.len(), RESERVATION_TOKEN_SIZE)?; - m.add(ATTR_RESERVATION_TOKEN, &self.0); - Ok(()) - } -} - -impl Getter for ReservationToken { - /// Decodes `RESERVATION-TOKEN` from message. - fn get_from(&mut self, m: &Message) -> Result<(), stun::Error> { - let v = m.get(ATTR_RESERVATION_TOKEN)?; - check_size(ATTR_RESERVATION_TOKEN, v.len(), RESERVATION_TOKEN_SIZE)?; - self.0 = v; - Ok(()) - } -} diff --git a/turn/src/proto/rsrvtoken/rsrvtoken_test.rs b/turn/src/proto/rsrvtoken/rsrvtoken_test.rs deleted file mode 100644 index 4990d8982..000000000 --- a/turn/src/proto/rsrvtoken/rsrvtoken_test.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::*; - -#[test] -fn test_reservation_token() -> Result<(), stun::Error> { - let mut m = Message::new(); - let mut v = vec![0; 8]; - v[2] = 33; - v[7] = 1; - let tk = ReservationToken(v); - tk.add_to(&mut m)?; - m.write_header(); - - //"HandleErr" - { - let bad_tk = ReservationToken(vec![34, 45]); - if let Err(err) = bad_tk.add_to(&mut m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, but got ok"); - } - } - - //"GetFrom" - { - let mut decoded = Message::new(); - decoded.write(&m.raw)?; - let mut tok = ReservationToken::default(); - tok.get_from(&decoded)?; - assert_eq!(tok, tk, "Decoded {tok:?}, expected {tk:?}"); - - //"HandleErr" - { - let mut m = Message::new(); - let mut handle = ReservationToken::default(); - if let Err(err) = handle.get_from(&m) { - assert_eq!( - stun::Error::ErrAttributeNotFound, - err, - "{err} should be not found" - ); - } else { - panic!("expected error, but got ok"); - } - m.add(ATTR_RESERVATION_TOKEN, &[1, 2, 3]); - if let Err(err) = handle.get_from(&m) { - assert!( - is_attr_size_invalid(&err), - "IsAttrSizeInvalid should be true" - ); - } else { - panic!("expected error, got ok"); - } - } - } - - Ok(()) -} diff --git a/turn/src/relay/mod.rs b/turn/src/relay/mod.rs deleted file mode 100644 index 928db108a..000000000 --- a/turn/src/relay/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -pub mod relay_none; -pub mod relay_range; -pub mod relay_static; - -use std::net::SocketAddr; -use std::sync::Arc; - -use async_trait::async_trait; -use util::Conn; - -use crate::error::Result; - -/// `RelayAddressGenerator` is used to generate a Relay Address when creating an allocation. -/// You can use one of the provided ones or provide your own. -#[async_trait] -pub trait RelayAddressGenerator { - /// Confirms that this is properly initialized - fn validate(&self) -> Result<()>; - - /// Allocates a Relay Address - async fn allocate_conn( - &self, - use_ipv4: bool, - requested_port: u16, - ) -> Result<(Arc, SocketAddr)>; -} diff --git a/turn/src/relay/relay_none.rs b/turn/src/relay/relay_none.rs deleted file mode 100644 index 1cf0a54a6..000000000 --- a/turn/src/relay/relay_none.rs +++ /dev/null @@ -1,37 +0,0 @@ -use async_trait::async_trait; -use util::vnet::net::*; - -use super::*; -use crate::error::*; - -/// `RelayAddressGeneratorNone` returns the listener with no modifications. -pub struct RelayAddressGeneratorNone { - /// `address` is passed to Listen/ListenPacket when creating the Relay. - pub address: String, - pub net: Arc, -} - -#[async_trait] -impl RelayAddressGenerator for RelayAddressGeneratorNone { - fn validate(&self) -> Result<()> { - if self.address.is_empty() { - Err(Error::ErrListeningAddressInvalid) - } else { - Ok(()) - } - } - - async fn allocate_conn( - &self, - use_ipv4: bool, - requested_port: u16, - ) -> Result<(Arc, SocketAddr)> { - let addr = self - .net - .resolve_addr(use_ipv4, &format!("{}:{}", self.address, requested_port)) - .await?; - let conn = self.net.bind(addr).await?; - let relay_addr = conn.local_addr()?; - Ok((conn, relay_addr)) - } -} diff --git a/turn/src/relay/relay_range.rs b/turn/src/relay/relay_range.rs deleted file mode 100644 index c612100bd..000000000 --- a/turn/src/relay/relay_range.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::net::IpAddr; - -use async_trait::async_trait; -use util::vnet::net::*; - -use super::*; -use crate::error::*; - -/// `RelayAddressGeneratorRanges` can be used to only allocate connections inside a defined port range. -pub struct RelayAddressGeneratorRanges { - /// `relay_address` is the IP returned to the user when the relay is created. - pub relay_address: IpAddr, - - /// `min_port` the minimum port to allocate. - pub min_port: u16, - - /// `max_port` the maximum (inclusive) port to allocate. - pub max_port: u16, - - /// `max_retries` the amount of tries to allocate a random port in the defined range. - pub max_retries: u16, - - /// `address` is passed to Listen/ListenPacket when creating the Relay. - pub address: String, - - pub net: Arc, -} - -#[async_trait] -impl RelayAddressGenerator for RelayAddressGeneratorRanges { - fn validate(&self) -> Result<()> { - if self.min_port == 0 { - Err(Error::ErrMinPortNotZero) - } else if self.max_port == 0 { - Err(Error::ErrMaxPortNotZero) - } else if self.max_port < self.min_port { - Err(Error::ErrMaxPortLessThanMinPort) - } else if self.address.is_empty() { - Err(Error::ErrListeningAddressInvalid) - } else { - Ok(()) - } - } - - async fn allocate_conn( - &self, - use_ipv4: bool, - requested_port: u16, - ) -> Result<(Arc, SocketAddr)> { - let max_retries = if self.max_retries == 0 { - 10 - } else { - self.max_retries - }; - - if requested_port != 0 { - let addr = self - .net - .resolve_addr(use_ipv4, &format!("{}:{}", self.address, requested_port)) - .await?; - let conn = self.net.bind(addr).await?; - let mut relay_addr = conn.local_addr()?; - relay_addr.set_ip(self.relay_address); - return Ok((conn, relay_addr)); - } - - for _ in 0..max_retries { - let port = self.min_port + rand::random::() % (self.max_port - self.min_port + 1); - let addr = self - .net - .resolve_addr(use_ipv4, &format!("{}:{}", self.address, port)) - .await?; - let conn = match self.net.bind(addr).await { - Ok(conn) => conn, - Err(_) => continue, - }; - - let mut relay_addr = conn.local_addr()?; - relay_addr.set_ip(self.relay_address); - return Ok((conn, relay_addr)); - } - - Err(Error::ErrMaxRetriesExceeded) - } -} diff --git a/turn/src/relay/relay_static.rs b/turn/src/relay/relay_static.rs deleted file mode 100644 index 72056691c..000000000 --- a/turn/src/relay/relay_static.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::net::IpAddr; - -use async_trait::async_trait; -use util::vnet::net::*; - -use super::*; -use crate::error::*; - -/// `RelayAddressGeneratorStatic` can be used to return static IP address each time a relay is created. -/// This can be used when you have a single static IP address that you want to use. -pub struct RelayAddressGeneratorStatic { - /// `relay_address` is the IP returned to the user when the relay is created. - pub relay_address: IpAddr, - - /// `address` is passed to Listen/ListenPacket when creating the Relay. - pub address: String, - - pub net: Arc, -} - -#[async_trait] -impl RelayAddressGenerator for RelayAddressGeneratorStatic { - fn validate(&self) -> Result<()> { - if self.address.is_empty() { - Err(Error::ErrListeningAddressInvalid) - } else { - Ok(()) - } - } - - async fn allocate_conn( - &self, - use_ipv4: bool, - requested_port: u16, - ) -> Result<(Arc, SocketAddr)> { - let addr = self - .net - .resolve_addr(use_ipv4, &format!("{}:{}", self.address, requested_port)) - .await?; - let conn = self.net.bind(addr).await?; - let mut relay_addr = conn.local_addr()?; - relay_addr.set_ip(self.relay_address); - return Ok((conn, relay_addr)); - } -} diff --git a/turn/src/server/config.rs b/turn/src/server/config.rs deleted file mode 100644 index bf1a36c93..000000000 --- a/turn/src/server/config.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::sync::Arc; - -use tokio::sync::mpsc; -use tokio::time::Duration; -use util::Conn; - -use crate::allocation::*; -use crate::auth::*; -use crate::error::*; -use crate::relay::*; - -/// ConnConfig is used for UDP listeners -pub struct ConnConfig { - pub conn: Arc, - - // When an allocation is generated the RelayAddressGenerator - // creates the net.PacketConn and returns the IP/Port it is available at - pub relay_addr_generator: Box, -} - -impl ConnConfig { - pub fn validate(&self) -> Result<()> { - self.relay_addr_generator.validate() - } -} - -/// ServerConfig configures the TURN Server -pub struct ServerConfig { - /// `conn_configs` are a list of all the turn listeners. - /// Each listener can have custom behavior around the creation of Relays. - pub conn_configs: Vec, - - /// `realm` sets the realm for this server - pub realm: String, - - /// `auth_handler` is a callback used to handle incoming auth requests, - /// allowing users to customize Pion TURN with custom behavior. - pub auth_handler: Arc, - - /// `channel_bind_timeout` sets the lifetime of channel binding. Defaults to 10 minutes. - pub channel_bind_timeout: Duration, - - /// To receive notify on allocation close event, with metrics data. - pub alloc_close_notify: Option>, -} - -impl ServerConfig { - pub fn validate(&self) -> Result<()> { - if self.conn_configs.is_empty() { - return Err(Error::ErrNoAvailableConns); - } - - for cc in &self.conn_configs { - cc.validate()?; - } - Ok(()) - } -} diff --git a/turn/src/server/mod.rs b/turn/src/server/mod.rs deleted file mode 100644 index c8604fc01..000000000 --- a/turn/src/server/mod.rs +++ /dev/null @@ -1,261 +0,0 @@ -#[cfg(test)] -mod server_test; - -pub mod config; -pub mod request; - -use std::collections::HashMap; -use std::sync::Arc; - -use config::*; -use request::*; -use tokio::sync::broadcast::error::RecvError; -use tokio::sync::broadcast::{self}; -use tokio::sync::{mpsc, oneshot, Mutex}; -use tokio::time::{Duration, Instant}; -use util::Conn; - -use crate::allocation::allocation_manager::*; -use crate::allocation::five_tuple::FiveTuple; -use crate::allocation::AllocationInfo; -use crate::auth::AuthHandler; -use crate::error::*; -use crate::proto::lifetime::DEFAULT_LIFETIME; - -const INBOUND_MTU: usize = 1500; - -/// Server is an instance of the TURN Server -pub struct Server { - auth_handler: Arc, - realm: String, - channel_bind_timeout: Duration, - pub(crate) nonces: Arc>>, - command_tx: Mutex>>, -} - -impl Server { - /// creates a new TURN server - pub async fn new(config: ServerConfig) -> Result { - config.validate()?; - - let (command_tx, _) = broadcast::channel(16); - let mut s = Server { - auth_handler: config.auth_handler, - realm: config.realm, - channel_bind_timeout: config.channel_bind_timeout, - nonces: Arc::new(Mutex::new(HashMap::new())), - command_tx: Mutex::new(Some(command_tx.clone())), - }; - - if s.channel_bind_timeout == Duration::from_secs(0) { - s.channel_bind_timeout = DEFAULT_LIFETIME; - } - - for p in config.conn_configs.into_iter() { - let nonces = Arc::clone(&s.nonces); - let auth_handler = Arc::clone(&s.auth_handler); - let realm = s.realm.clone(); - let channel_bind_timeout = s.channel_bind_timeout; - let handle_rx = command_tx.subscribe(); - let conn = p.conn; - let allocation_manager = Arc::new(Manager::new(ManagerConfig { - relay_addr_generator: p.relay_addr_generator, - alloc_close_notify: config.alloc_close_notify.clone(), - })); - - tokio::spawn(Server::read_loop( - conn, - allocation_manager, - nonces, - auth_handler, - realm, - channel_bind_timeout, - handle_rx, - )); - } - - Ok(s) - } - - /// Deletes all existing [`Allocation`][`Allocation`]s by the provided `username`. - /// - /// [`Allocation`]: crate::allocation::Allocation - pub async fn delete_allocations_by_username(&self, username: String) -> Result<()> { - let tx = { - let command_tx = self.command_tx.lock().await; - command_tx.clone() - }; - if let Some(tx) = tx { - let (closed_tx, closed_rx) = mpsc::channel(1); - tx.send(Command::DeleteAllocations(username, Arc::new(closed_rx))) - .map_err(|_| Error::ErrClosed)?; - - closed_tx.closed().await; - - Ok(()) - } else { - Err(Error::ErrClosed) - } - } - - /// Get information of [`Allocation`][`Allocation`]s by specified [`FiveTuple`]s. - /// - /// If `five_tuples` is: - /// - [`None`]: It returns information about the all - /// [`Allocation`][`Allocation`]s. - /// - [`Some`] and not empty: It returns information about - /// the [`Allocation`][`Allocation`]s associated with - /// the specified [`FiveTuples`]. - /// - [`Some`], but empty: It returns an empty [`HashMap`]. - /// - /// [`Allocation`]: crate::allocation::Allocation - pub async fn get_allocations_info( - &self, - five_tuples: Option>, - ) -> Result> { - if let Some(five_tuples) = &five_tuples { - if five_tuples.is_empty() { - return Ok(HashMap::new()); - } - } - - let tx = { - let command_tx = self.command_tx.lock().await; - command_tx.clone() - }; - if let Some(tx) = tx { - let (infos_tx, mut infos_rx) = mpsc::channel(1); - tx.send(Command::GetAllocationsInfo(five_tuples, infos_tx)) - .map_err(|_| Error::ErrClosed)?; - - let mut info: HashMap = HashMap::new(); - - for _ in 0..tx.receiver_count() { - info.extend(infos_rx.recv().await.ok_or(Error::ErrClosed)?); - } - - Ok(info) - } else { - Err(Error::ErrClosed) - } - } - - async fn read_loop( - conn: Arc, - allocation_manager: Arc, - nonces: Arc>>, - auth_handler: Arc, - realm: String, - channel_bind_timeout: Duration, - mut handle_rx: broadcast::Receiver, - ) { - let mut buf = vec![0u8; INBOUND_MTU]; - - let (mut close_tx, mut close_rx) = oneshot::channel::<()>(); - - tokio::spawn({ - let allocation_manager = Arc::clone(&allocation_manager); - - async move { - loop { - match handle_rx.recv().await { - Ok(Command::DeleteAllocations(name, _)) => { - allocation_manager - .delete_allocations_by_username(name.as_str()) - .await; - continue; - } - Ok(Command::GetAllocationsInfo(five_tuples, tx)) => { - let infos = allocation_manager.get_allocations_info(five_tuples).await; - let _ = tx.send(infos).await; - - continue; - } - Err(RecvError::Closed) | Ok(Command::Close(_)) => { - close_rx.close(); - break; - } - Err(RecvError::Lagged(n)) => { - log::warn!("Turn server has lagged by {} messages", n); - continue; - } - } - } - } - }); - - loop { - let (n, addr) = tokio::select! { - v = conn.recv_from(&mut buf) => { - match v { - Ok(v) => v, - Err(err) => { - log::debug!("exit read loop on error: {}", err); - break; - } - } - }, - _ = close_tx.closed() => break - }; - - let mut r = Request { - conn: Arc::clone(&conn), - src_addr: addr, - buff: buf[..n].to_vec(), - allocation_manager: Arc::clone(&allocation_manager), - nonces: Arc::clone(&nonces), - auth_handler: Arc::clone(&auth_handler), - realm: realm.clone(), - channel_bind_timeout, - }; - - if let Err(err) = r.handle_request().await { - log::error!("error when handling datagram: {}", err); - } - } - - let _ = allocation_manager.close().await; - let _ = conn.close().await; - } - - /// Close stops the TURN Server. It cleans up any associated state and closes all connections it is managing. - pub async fn close(&self) -> Result<()> { - let tx = { - let mut command_tx = self.command_tx.lock().await; - command_tx.take() - }; - - if let Some(tx) = tx { - if tx.receiver_count() == 0 { - return Ok(()); - } - - let (closed_tx, closed_rx) = mpsc::channel(1); - let _ = tx.send(Command::Close(Arc::new(closed_rx))); - closed_tx.closed().await - } - - Ok(()) - } -} - -/// The protocol to communicate between the [`Server`]'s public methods -/// and the tasks spawned in the [`Server::read_loop`] method. -#[derive(Clone)] -enum Command { - /// Command to delete [`Allocation`][`Allocation`] by provided `username`. - /// - /// [`Allocation`]: `crate::allocation::Allocation` - DeleteAllocations(String, Arc>), - - /// Command to get information of [`Allocation`][`Allocation`]s by provided [`FiveTuple`]s. - /// - /// [`Allocation`]: `crate::allocation::Allocation` - GetAllocationsInfo( - Option>, - mpsc::Sender>, - ), - - /// Command to close the [`Server`]. - Close(Arc>), -} diff --git a/turn/src/server/request.rs b/turn/src/server/request.rs deleted file mode 100644 index 9d1a3104f..000000000 --- a/turn/src/server/request.rs +++ /dev/null @@ -1,1031 +0,0 @@ -#[cfg(test)] -mod request_test; - -use std::collections::HashMap; -use std::marker::{Send, Sync}; -use std::net::SocketAddr; -#[cfg(feature = "metrics")] -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use md5::{Digest, Md5}; -use stun::agent::*; -use stun::attributes::*; -use stun::error_code::*; -use stun::fingerprint::*; -use stun::integrity::*; -use stun::message::*; -use stun::textattrs::*; -use stun::uattrs::*; -use stun::xoraddr::*; -use tokio::sync::Mutex; -use tokio::time::{Duration, Instant}; -use util::Conn; - -use crate::allocation::allocation_manager::*; -use crate::allocation::channel_bind::ChannelBind; -use crate::allocation::five_tuple::*; -use crate::allocation::permission::Permission; -use crate::auth::*; -use crate::error::*; -use crate::proto::chandata::ChannelData; -use crate::proto::channum::ChannelNumber; -use crate::proto::data::Data; -use crate::proto::evenport::EvenPort; -use crate::proto::lifetime::*; -use crate::proto::peeraddr::PeerAddress; -use crate::proto::relayaddr::RelayedAddress; -use crate::proto::reqfamily::{ - RequestedAddressFamily, REQUESTED_FAMILY_IPV4, REQUESTED_FAMILY_IPV6, -}; -use crate::proto::reqtrans::RequestedTransport; -use crate::proto::rsrvtoken::ReservationToken; -use crate::proto::*; - -pub(crate) const MAXIMUM_ALLOCATION_LIFETIME: Duration = Duration::from_secs(3600); // https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation -pub(crate) const NONCE_LIFETIME: Duration = Duration::from_secs(3600); // https://tools.ietf.org/html/rfc5766#section-4 - -/// Request contains all the state needed to process a single incoming datagram -pub struct Request { - // Current Request State - pub conn: Arc, - pub src_addr: SocketAddr, - pub buff: Vec, - - // Server State - pub allocation_manager: Arc, - pub nonces: Arc>>, - - // User Configuration - pub auth_handler: Arc, - pub realm: String, - pub channel_bind_timeout: Duration, -} - -impl Request { - pub fn new( - conn: Arc, - src_addr: SocketAddr, - allocation_manager: Arc, - auth_handler: Arc, - ) -> Self { - Request { - conn, - src_addr, - buff: vec![], - allocation_manager, - nonces: Arc::new(Mutex::new(HashMap::new())), - auth_handler, - realm: String::new(), - channel_bind_timeout: Duration::from_secs(0), - } - } - - /// Processes the give [`Request`] - pub async fn handle_request(&mut self) -> Result<()> { - /*log::debug!( - "received {} bytes of udp from {} on {}", - self.buff.len(), - self.src_addr, - self.conn.local_addr().await? - );*/ - - if ChannelData::is_channel_data(&self.buff) { - self.handle_data_packet().await - } else { - self.handle_turn_packet().await - } - } - - async fn handle_data_packet(&mut self) -> Result<()> { - log::debug!("received DataPacket from {}", self.src_addr); - let mut c = ChannelData { - raw: self.buff.clone(), - ..Default::default() - }; - c.decode()?; - self.handle_channel_data(&c).await - } - - async fn handle_turn_packet(&mut self) -> Result<()> { - log::debug!("handle_turn_packet"); - let mut m = Message { - raw: self.buff.clone(), - ..Default::default() - }; - m.decode()?; - - self.process_message_handler(&m).await - } - - async fn process_message_handler(&mut self, m: &Message) -> Result<()> { - if m.typ.class == CLASS_INDICATION { - match m.typ.method { - METHOD_SEND => self.handle_send_indication(m).await, - _ => Err(Error::ErrUnexpectedClass), - } - } else if m.typ.class == CLASS_REQUEST { - match m.typ.method { - METHOD_ALLOCATE => self.handle_allocate_request(m).await, - METHOD_REFRESH => self.handle_refresh_request(m).await, - METHOD_CREATE_PERMISSION => self.handle_create_permission_request(m).await, - METHOD_CHANNEL_BIND => self.handle_channel_bind_request(m).await, - METHOD_BINDING => self.handle_binding_request(m).await, - _ => Err(Error::ErrUnexpectedClass), - } - } else { - Err(Error::ErrUnexpectedClass) - } - } - - pub(crate) async fn authenticate_request( - &mut self, - m: &Message, - calling_method: Method, - ) -> Result> { - if !m.contains(ATTR_MESSAGE_INTEGRITY) { - self.respond_with_nonce(m, calling_method, CODE_UNAUTHORIZED) - .await?; - return Ok(None); - } - - let mut nonce_attr = Nonce::new(ATTR_NONCE, String::new()); - let mut username_attr = Username::new(ATTR_USERNAME, String::new()); - let mut realm_attr = Realm::new(ATTR_REALM, String::new()); - let bad_request_msg = build_msg( - m.transaction_id, - MessageType::new(calling_method, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_BAD_REQUEST, - reason: vec![], - })], - )?; - - if let Err(err) = nonce_attr.get_from(m) { - build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()).await?; - return Ok(None); - } - - let to_be_deleted = { - // Assert Nonce exists and is not expired - let mut nonces = self.nonces.lock().await; - - let to_be_deleted = if let Some(nonce_creation_time) = nonces.get(&nonce_attr.text) { - Instant::now() - .checked_duration_since(*nonce_creation_time) - .unwrap_or_else(|| Duration::from_secs(0)) - >= NONCE_LIFETIME - } else { - true - }; - - if to_be_deleted { - nonces.remove(&nonce_attr.text); - } - to_be_deleted - }; - - if to_be_deleted { - self.respond_with_nonce(m, calling_method, CODE_STALE_NONCE) - .await?; - return Ok(None); - } - - if let Err(err) = realm_attr.get_from(m) { - build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()).await?; - return Ok(None); - } - if let Err(err) = username_attr.get_from(m) { - build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()).await?; - return Ok(None); - } - - let our_key = match self.auth_handler.auth_handle( - &username_attr.to_string(), - &realm_attr.to_string(), - self.src_addr, - ) { - Ok(key) => key, - Err(_) => { - build_and_send_err( - &self.conn, - self.src_addr, - bad_request_msg, - Error::ErrNoSuchUser, - ) - .await?; - return Ok(None); - } - }; - - let mi = MessageIntegrity(our_key); - if let Err(err) = mi.check(&mut m.clone()) { - build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()).await?; - Ok(None) - } else { - Ok(Some((username_attr, mi))) - } - } - - async fn respond_with_nonce( - &mut self, - m: &Message, - calling_method: Method, - response_code: ErrorCode, - ) -> Result<()> { - let nonce = build_nonce()?; - - { - // Nonce has already been taken - let mut nonces = self.nonces.lock().await; - if nonces.contains_key(&nonce) { - return Err(Error::ErrDuplicatedNonce); - } - nonces.insert(nonce.clone(), Instant::now()); - } - - let msg = build_msg( - m.transaction_id, - MessageType::new(calling_method, CLASS_ERROR_RESPONSE), - vec![ - Box::new(ErrorCodeAttribute { - code: response_code, - reason: vec![], - }), - Box::new(Nonce::new(ATTR_NONCE, nonce)), - Box::new(Realm::new(ATTR_REALM, self.realm.clone())), - ], - )?; - - build_and_send(&self.conn, self.src_addr, msg).await - } - - pub(crate) async fn handle_binding_request(&mut self, m: &Message) -> Result<()> { - log::debug!("received BindingRequest from {}", self.src_addr); - - let (ip, port) = (self.src_addr.ip(), self.src_addr.port()); - - let msg = build_msg( - m.transaction_id, - BINDING_SUCCESS, - vec![ - Box::new(XorMappedAddress { ip, port }), - Box::new(FINGERPRINT), - ], - )?; - - build_and_send(&self.conn, self.src_addr, msg).await - } - - /// https://tools.ietf.org/html/rfc5766#section-6.2 - pub(crate) async fn handle_allocate_request(&mut self, m: &Message) -> Result<()> { - log::debug!("received AllocateRequest from {}", self.src_addr); - - // 1. The server MUST require that the request be authenticated. This - // authentication MUST be done using the long-term credential - // mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2] - // unless the client and server agree to use another mechanism through - // some procedure outside the scope of this document. - let (username, message_integrity) = - if let Some(mi) = self.authenticate_request(m, METHOD_ALLOCATE).await? { - mi - } else { - log::debug!("no MessageIntegrity"); - return Ok(()); - }; - - let five_tuple = FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }; - let mut requested_port = 0; - let mut reservation_token = "".to_owned(); - let mut use_ipv4 = true; - - // 2. The server checks if the 5-tuple is currently in use by an - // existing allocation. If yes, the server rejects the request with - // a 437 (Allocation Mismatch) error. - if self - .allocation_manager - .get_allocation(&five_tuple) - .await - .is_some() - { - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_ALLOC_MISMATCH, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - msg, - Error::ErrRelayAlreadyAllocatedForFiveTuple, - ) - .await; - } - - // 3. The server checks if the request contains a REQUESTED-TRANSPORT - // attribute. If the REQUESTED-TRANSPORT attribute is not included - // or is malformed, the server rejects the request with a 400 (Bad - // Request) error. Otherwise, if the attribute is included but - // specifies a protocol other that UDP, the server rejects the - // request with a 442 (Unsupported Transport Protocol) error. - let mut requested_transport = RequestedTransport::default(); - if let Err(err) = requested_transport.get_from(m) { - let bad_request_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_BAD_REQUEST, - reason: vec![], - })], - )?; - return build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()) - .await; - } else if requested_transport.protocol != PROTO_UDP { - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_UNSUPPORTED_TRANS_PROTO, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - msg, - Error::ErrRequestedTransportMustBeUdp, - ) - .await; - } - - // 4. The request may contain a DONT-FRAGMENT attribute. If it does, - // but the server does not support sending UDP datagrams with the DF - // bit set to 1 (see Section 12), then the server treats the DONT- - // FRAGMENT attribute in the Allocate request as an unknown - // comprehension-required attribute. - if m.contains(ATTR_DONT_FRAGMENT) { - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![ - Box::new(ErrorCodeAttribute { - code: CODE_UNKNOWN_ATTRIBUTE, - reason: vec![], - }), - Box::new(UnknownAttributes(vec![ATTR_DONT_FRAGMENT])), - ], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - msg, - Error::ErrNoDontFragmentSupport, - ) - .await; - } - - // 5. The server checks if the request contains a RESERVATION-TOKEN - // attribute. If yes, and the request also contains an EVEN-PORT - // attribute, then the server rejects the request with a 400 (Bad - // Request) error. Otherwise, it checks to see if the token is - // valid (i.e., the token is in range and has not expired and the - // corresponding relayed transport address is still available). If - // the token is not valid for some reason, the server rejects the - // request with a 508 (Insufficient Capacity) error. - let mut reservation_token_attr = ReservationToken::default(); - let reservation_token_attr_result = reservation_token_attr.get_from(m); - if reservation_token_attr_result.is_ok() { - let mut even_port = EvenPort::default(); - if even_port.get_from(m).is_ok() { - let bad_request_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_BAD_REQUEST, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - bad_request_msg, - Error::ErrRequestWithReservationTokenAndEvenPort, - ) - .await; - } - } - - // RFC 6156, Section 4.2: - // - // If it contains both a RESERVATION-TOKEN and a - // REQUESTED-ADDRESS-FAMILY, the server replies with a 400 - // (Bad Request) Allocate error response. - // - // 4.2.1. Unsupported Address Family - // This document defines the following new error response code: - // 440 (Address Family not Supported): The server does not support the - // address family requested by the client. - let mut req_family = RequestedAddressFamily::default(); - match req_family.get_from(m) { - Err(err) => { - // Currently, the RequestedAddressFamily::get_from() function returns - // Err::Other only when it is an unsupported address family. - if let stun::Error::Other(_) = err { - let addr_family_not_supported_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_ADDR_FAMILY_NOT_SUPPORTED, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - addr_family_not_supported_msg, - Error::ErrInvalidRequestedFamilyValue, - ) - .await; - } - } - Ok(()) => { - if reservation_token_attr_result.is_ok() { - let bad_request_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_BAD_REQUEST, - reason: vec![], - })], - )?; - - return build_and_send_err( - &self.conn, - self.src_addr, - bad_request_msg, - Error::ErrRequestWithReservationTokenAndReqAddressFamily, - ) - .await; - } - - if req_family == REQUESTED_FAMILY_IPV6 { - use_ipv4 = false; - } - } - } - - // 6. The server checks if the request contains an EVEN-PORT attribute. - // If yes, then the server checks that it can satisfy the request - // (i.e., can allocate a relayed transport address as described - // below). If the server cannot satisfy the request, then the - // server rejects the request with a 508 (Insufficient Capacity) - // error. - let mut even_port = EvenPort::default(); - if even_port.get_from(m).is_ok() { - let mut random_port = 1; - - while random_port % 2 != 0 { - random_port = match self.allocation_manager.get_random_even_port().await { - Ok(port) => port, - Err(err) => { - let insufficient_capacity_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_INSUFFICIENT_CAPACITY, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - insufficient_capacity_msg, - err, - ) - .await; - } - }; - } - - requested_port = random_port; - reservation_token = rand_seq(8); - } - - // 7. At any point, the server MAY choose to reject the request with a - // 486 (Allocation Quota Reached) error if it feels the client is - // trying to exceed some locally defined allocation quota. The - // server is free to define this allocation quota any way it wishes, - // but SHOULD define it based on the username used to authenticate - // the request, and not on the client's transport address. - - // 8. Also at any point, the server MAY choose to reject the request - // with a 300 (Try Alternate) error if it wishes to redirect the - // client to a different server. The use of this error code and - // attribute follow the specification in [RFC5389]. - let lifetime_duration = allocation_lifetime(m); - let a = match self - .allocation_manager - .create_allocation( - five_tuple, - Arc::clone(&self.conn), - requested_port, - lifetime_duration, - username, - use_ipv4, - ) - .await - { - Ok(a) => a, - Err(err) => { - let insufficient_capacity_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_INSUFFICIENT_CAPACITY, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - insufficient_capacity_msg, - err, - ) - .await; - } - }; - - // Once the allocation is created, the server replies with a success - // response. The success response contains: - // * An XOR-RELAYED-ADDRESS attribute containing the relayed transport - // address. - // * A LIFETIME attribute containing the current value of the time-to- - // expiry timer. - // * A RESERVATION-TOKEN attribute (if a second relayed transport - // address was reserved). - // * An XOR-MAPPED-ADDRESS attribute containing the client's IP address - // and port (from the 5-tuple). - - let (src_ip, src_port) = (self.src_addr.ip(), self.src_addr.port()); - let relay_ip = a.relay_addr.ip(); - let relay_port = a.relay_addr.port(); - - let msg = { - if !reservation_token.is_empty() { - self.allocation_manager - .create_reservation(reservation_token.clone(), relay_port) - .await; - } - - let mut response_attrs: Vec> = vec![ - Box::new(RelayedAddress { - ip: relay_ip, - port: relay_port, - }), - Box::new(Lifetime(lifetime_duration)), - Box::new(XorMappedAddress { - ip: src_ip, - port: src_port, - }), - ]; - - if !reservation_token.is_empty() { - response_attrs.push(Box::new(ReservationToken( - reservation_token.as_bytes().to_vec(), - ))); - } - - response_attrs.push(Box::new(message_integrity)); - build_msg( - m.transaction_id, - MessageType::new(METHOD_ALLOCATE, CLASS_SUCCESS_RESPONSE), - response_attrs, - )? - }; - - build_and_send(&self.conn, self.src_addr, msg).await - } - - pub(crate) async fn handle_refresh_request(&mut self, m: &Message) -> Result<()> { - log::debug!("received RefreshRequest from {}", self.src_addr); - - let (_, message_integrity) = - if let Some(mi) = self.authenticate_request(m, METHOD_REFRESH).await? { - mi - } else { - log::debug!("no MessageIntegrity"); - return Ok(()); - }; - - let lifetime_duration = allocation_lifetime(m); - let five_tuple = FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }; - - if lifetime_duration != Duration::from_secs(0) { - let a = self.allocation_manager.get_allocation(&five_tuple).await; - if let Some(a) = a { - // If a server receives a Refresh Request with a REQUESTED-ADDRESS-FAMILY - // attribute, and the attribute's value doesn't match the address - // family of the allocation, the server MUST reply with a 443 (Peer - // Address Family Mismatch) Refresh error response. [RFC 6156, Section 5.2] - let mut req_family = RequestedAddressFamily::default(); - if req_family.get_from(m).is_ok() - && ((req_family == REQUESTED_FAMILY_IPV6 && !a.relay_addr.is_ipv6()) - || (req_family == REQUESTED_FAMILY_IPV4 && !a.relay_addr.is_ipv4())) - { - let peer_address_family_mismatch_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_REFRESH, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_PEER_ADDR_FAMILY_MISMATCH, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - peer_address_family_mismatch_msg, - Error::ErrPeerAddressFamilyMismatch, - ) - .await; - } - a.refresh(lifetime_duration).await; - } else { - return Err(Error::ErrNoAllocationFound); - } - } else { - self.allocation_manager.delete_allocation(&five_tuple).await; - } - - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_REFRESH, CLASS_SUCCESS_RESPONSE), - vec![ - Box::new(Lifetime(lifetime_duration)), - Box::new(message_integrity), - ], - )?; - - build_and_send(&self.conn, self.src_addr, msg).await - } - - pub(crate) async fn handle_create_permission_request(&mut self, m: &Message) -> Result<()> { - log::debug!("received CreatePermission from {}", self.src_addr); - - let a = self - .allocation_manager - .get_allocation(&FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }) - .await; - - if let Some(a) = a { - let (_, message_integrity) = if let Some(mi) = self - .authenticate_request(m, METHOD_CREATE_PERMISSION) - .await? - { - mi - } else { - log::debug!("no MessageIntegrity"); - return Ok(()); - }; - let mut add_count = 0; - - { - for attr in &m.attributes.0 { - if attr.typ != ATTR_XOR_PEER_ADDRESS { - continue; - } - - let mut peer_address = PeerAddress::default(); - if peer_address.get_from(m).is_err() { - add_count = 0; - break; - } - - // If an XOR-PEER-ADDRESS attribute contains an address of an address - // family different than that of the relayed transport address for the - // allocation, the server MUST generate an error response with the 443 - // (Peer Address Family Mismatch) response code. [RFC 6156, Section 6.2] - if (peer_address.ip.is_ipv4() && !a.relay_addr.is_ipv4()) - || (peer_address.ip.is_ipv6() && !a.relay_addr.is_ipv6()) - { - let peer_address_family_mismatch_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_CREATE_PERMISSION, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_PEER_ADDR_FAMILY_MISMATCH, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - peer_address_family_mismatch_msg, - Error::ErrPeerAddressFamilyMismatch, - ) - .await; - } - - log::debug!( - "adding permission for {}", - format!("{}:{}", peer_address.ip, peer_address.port) - ); - - a.add_permission(Permission::new(SocketAddr::new( - peer_address.ip, - peer_address.port, - ))) - .await; - add_count += 1; - } - } - - let mut resp_class = CLASS_SUCCESS_RESPONSE; - if add_count == 0 { - resp_class = CLASS_ERROR_RESPONSE; - } - - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_CREATE_PERMISSION, resp_class), - vec![Box::new(message_integrity)], - )?; - - build_and_send(&self.conn, self.src_addr, msg).await - } else { - Err(Error::ErrNoAllocationFound) - } - } - - pub(crate) async fn handle_send_indication(&mut self, m: &Message) -> Result<()> { - log::debug!("received SendIndication from {}", self.src_addr); - - let a = self - .allocation_manager - .get_allocation(&FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }) - .await; - - if let Some(a) = a { - let mut data_attr = Data::default(); - data_attr.get_from(m)?; - - let mut peer_address = PeerAddress::default(); - peer_address.get_from(m)?; - - let msg_dst = SocketAddr::new(peer_address.ip, peer_address.port); - - let has_perm = a.has_permission(&msg_dst).await; - if !has_perm { - return Err(Error::ErrNoPermission); - } - - let l = a.relay_socket.send_to(&data_attr.0, msg_dst).await?; - if l != data_attr.0.len() { - Err(Error::ErrShortWrite) - } else { - #[cfg(feature = "metrics")] - a.relayed_bytes - .fetch_add(data_attr.0.len(), Ordering::AcqRel); - - Ok(()) - } - } else { - Err(Error::ErrNoAllocationFound) - } - } - - pub(crate) async fn handle_channel_bind_request(&mut self, m: &Message) -> Result<()> { - log::debug!("received ChannelBindRequest from {}", self.src_addr); - - let a = self - .allocation_manager - .get_allocation(&FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }) - .await; - - if let Some(a) = a { - let bad_request_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_CHANNEL_BIND, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_BAD_REQUEST, - reason: vec![], - })], - )?; - - let (_, message_integrity) = - if let Some(mi) = self.authenticate_request(m, METHOD_CHANNEL_BIND).await? { - mi - } else { - log::debug!("no MessageIntegrity"); - return Ok(()); - }; - let mut channel = ChannelNumber::default(); - if let Err(err) = channel.get_from(m) { - return build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err.into()) - .await; - } - - let mut peer_addr = PeerAddress::default(); - match peer_addr.get_from(m) { - Err(err) => { - return build_and_send_err( - &self.conn, - self.src_addr, - bad_request_msg, - err.into(), - ) - .await; - } - _ => { - // If the XOR-PEER-ADDRESS attribute contains an address of an address - // family different than that of the relayed transport address for the - // allocation, the server MUST generate an error response with the 443 - // (Peer Address Family Mismatch) response code. [RFC 6156, Section 7.2] - if (peer_addr.ip.is_ipv4() && !a.relay_addr.is_ipv4()) - || (peer_addr.ip.is_ipv6() && !a.relay_addr.is_ipv6()) - { - let peer_address_family_mismatch_msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_CHANNEL_BIND, CLASS_ERROR_RESPONSE), - vec![Box::new(ErrorCodeAttribute { - code: CODE_PEER_ADDR_FAMILY_MISMATCH, - reason: vec![], - })], - )?; - return build_and_send_err( - &self.conn, - self.src_addr, - peer_address_family_mismatch_msg, - Error::ErrPeerAddressFamilyMismatch, - ) - .await; - } - } - } - - log::debug!( - "binding channel {} to {}", - channel, - format!("{}:{}", peer_addr.ip, peer_addr.port) - ); - - let result = { - a.add_channel_bind( - ChannelBind::new(channel, SocketAddr::new(peer_addr.ip, peer_addr.port)), - self.channel_bind_timeout, - ) - .await - }; - if let Err(err) = result { - return build_and_send_err(&self.conn, self.src_addr, bad_request_msg, err).await; - } - - let msg = build_msg( - m.transaction_id, - MessageType::new(METHOD_CHANNEL_BIND, CLASS_SUCCESS_RESPONSE), - vec![Box::new(message_integrity)], - )?; - build_and_send(&self.conn, self.src_addr, msg).await - } else { - Err(Error::ErrNoAllocationFound) - } - } - - pub(crate) async fn handle_channel_data(&mut self, c: &ChannelData) -> Result<()> { - log::debug!("received ChannelData from {}", self.src_addr); - - let a = self - .allocation_manager - .get_allocation(&FiveTuple { - src_addr: self.src_addr, - dst_addr: self.conn.local_addr()?, - protocol: PROTO_UDP, - }) - .await; - - if let Some(a) = a { - let channel = a.get_channel_addr(&c.number).await; - if let Some(peer) = channel { - let l = a.relay_socket.send_to(&c.data, peer).await?; - if l != c.data.len() { - Err(Error::ErrShortWrite) - } else { - #[cfg(feature = "metrics")] - a.relayed_bytes.fetch_add(c.data.len(), Ordering::AcqRel); - - Ok(()) - } - } else { - Err(Error::ErrNoSuchChannelBind) - } - } else { - Err(Error::ErrNoAllocationFound) - } - } -} - -pub(crate) fn rand_seq(n: usize) -> String { - let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".as_bytes(); - let mut buf = vec![0u8; n]; - for b in &mut buf { - *b = letters[rand::random::() % letters.len()]; - } - if let Ok(s) = String::from_utf8(buf) { - s - } else { - String::new() - } -} - -pub(crate) fn build_nonce() -> Result { - /* #nosec */ - let mut s = String::new(); - s.push_str( - format!( - "{}", - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? - .as_nanos() - ) - .as_str(), - ); - s.push_str(format!("{}", rand::random::()).as_str()); - - let mut h = Md5::new(); - h.update(s.as_bytes()); - Ok(format!("{:x}", h.finalize())) -} - -pub(crate) async fn build_and_send( - conn: &Arc, - dst: SocketAddr, - msg: Message, -) -> Result<()> { - let _ = conn.send_to(&msg.raw, dst).await?; - Ok(()) -} - -/// Send a STUN packet and return the original error to the caller -pub(crate) async fn build_and_send_err( - conn: &Arc, - dst: SocketAddr, - msg: Message, - err: Error, -) -> Result<()> { - build_and_send(conn, dst, msg).await?; - - Err(err) -} - -pub(crate) fn build_msg( - transaction_id: TransactionId, - msg_type: MessageType, - mut additional: Vec>, -) -> Result { - let mut attrs: Vec> = vec![ - Box::new(Message { - transaction_id, - ..Default::default() - }), - Box::new(msg_type), - ]; - - attrs.append(&mut additional); - - let mut msg = Message::new(); - msg.build(&attrs)?; - Ok(msg) -} - -pub(crate) fn allocation_lifetime(m: &Message) -> Duration { - let mut lifetime_duration = DEFAULT_LIFETIME; - - let mut lifetime = Lifetime::default(); - if lifetime.get_from(m).is_ok() && lifetime.0 < MAXIMUM_ALLOCATION_LIFETIME { - lifetime_duration = lifetime.0; - } - - lifetime_duration -} diff --git a/turn/src/server/request/request_test.rs b/turn/src/server/request/request_test.rs deleted file mode 100644 index cfe012055..000000000 --- a/turn/src/server/request/request_test.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::net::IpAddr; -use std::str::FromStr; - -use tokio::net::UdpSocket; -use tokio::time::{Duration, Instant}; -use util::vnet::net::*; - -use super::*; -use crate::relay::relay_none::*; - -const STATIC_KEY: &str = "ABC"; - -#[tokio::test] -async fn test_allocation_lifetime_parsing() -> Result<()> { - let lifetime = Lifetime(Duration::from_secs(5)); - - let mut m = Message::new(); - let lifetime_duration = allocation_lifetime(&m); - - assert_eq!( - lifetime_duration, DEFAULT_LIFETIME, - "Allocation lifetime should be default time duration" - ); - - lifetime.add_to(&mut m)?; - - let lifetime_duration = allocation_lifetime(&m); - assert_eq!( - lifetime_duration, lifetime.0, - "Expect lifetime_duration is {lifetime}, but {lifetime_duration:?}" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_allocation_lifetime_overflow() -> Result<()> { - let lifetime = Lifetime(MAXIMUM_ALLOCATION_LIFETIME * 2); - - let mut m2 = Message::new(); - lifetime.add_to(&mut m2)?; - - let lifetime_duration = allocation_lifetime(&m2); - assert_eq!( - lifetime_duration, DEFAULT_LIFETIME, - "Expect lifetime_duration is {DEFAULT_LIFETIME:?}, but {lifetime_duration:?}" - ); - - Ok(()) -} - -struct TestAuthHandler; -impl AuthHandler for TestAuthHandler { - fn auth_handle(&self, _username: &str, _realm: &str, _src_addr: SocketAddr) -> Result> { - Ok(STATIC_KEY.as_bytes().to_vec()) - } -} - -#[tokio::test] -async fn test_allocation_lifetime_deletion_zero_lifetime() -> Result<()> { - //env_logger::init(); - - let l = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let allocation_manager = Arc::new(Manager::new(ManagerConfig { - relay_addr_generator: Box::new(RelayAddressGeneratorNone { - address: "0.0.0.0".to_owned(), - net: Arc::new(Net::new(None)), - }), - alloc_close_notify: None, - })); - - let socket = SocketAddr::new(IpAddr::from_str("127.0.0.1")?, 5000); - - let mut r = Request::new(l, socket, allocation_manager, Arc::new(TestAuthHandler {})); - - { - let mut nonces = r.nonces.lock().await; - nonces.insert(STATIC_KEY.to_owned(), Instant::now()); - } - - let five_tuple = FiveTuple { - src_addr: r.src_addr, - dst_addr: r.conn.local_addr()?, - protocol: PROTO_UDP, - }; - - r.allocation_manager - .create_allocation( - five_tuple, - Arc::clone(&r.conn), - 0, - Duration::from_secs(3600), - TextAttribute::new(ATTR_USERNAME, "user".into()), - true, - ) - .await?; - assert!(r - .allocation_manager - .get_allocation(&five_tuple) - .await - .is_some()); - - let mut m = Message::new(); - Lifetime::default().add_to(&mut m)?; - MessageIntegrity(STATIC_KEY.as_bytes().to_vec()).add_to(&mut m)?; - Nonce::new(ATTR_NONCE, STATIC_KEY.to_owned()).add_to(&mut m)?; - Realm::new(ATTR_REALM, STATIC_KEY.to_owned()).add_to(&mut m)?; - Username::new(ATTR_USERNAME, STATIC_KEY.to_owned()).add_to(&mut m)?; - - r.handle_refresh_request(&m).await?; - assert!(r - .allocation_manager - .get_allocation(&five_tuple) - .await - .is_none()); - - Ok(()) -} diff --git a/turn/src/server/server_test.rs b/turn/src/server/server_test.rs deleted file mode 100644 index 1505a12da..000000000 --- a/turn/src/server/server_test.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; - -use tokio::net::UdpSocket; -use tokio::sync::mpsc; -use util::vnet::router::Nic; -use util::vnet::*; - -use super::config::*; -use super::*; -use crate::auth::generate_auth_key; -use crate::client::*; -use crate::error::*; -use crate::relay::relay_none::RelayAddressGeneratorNone; -use crate::relay::relay_static::*; - -struct TestAuthHandler { - cred_map: HashMap>, -} - -impl TestAuthHandler { - fn new() -> Self { - let mut cred_map = HashMap::new(); - cred_map.insert( - "user".to_owned(), - generate_auth_key("user", "webrtc.rs", "pass"), - ); - - TestAuthHandler { cred_map } - } -} - -impl AuthHandler for TestAuthHandler { - fn auth_handle(&self, username: &str, _realm: &str, _src_addr: SocketAddr) -> Result> { - if let Some(pw) = self.cred_map.get(username) { - Ok(pw.to_vec()) - } else { - Err(Error::ErrFakeErr) - } - } -} - -#[tokio::test] -async fn test_server_simple() -> Result<()> { - // here, it should use static port, like "0.0.0.0:3478", - // but, due to different test environment, let's fake it by using "0.0.0.0:0" - // to auto assign a "static" port - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - let server_port = conn.local_addr()?.port(); - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorStatic { - relay_address: IpAddr::from_str("127.0.0.1")?, - address: "0.0.0.0".to_owned(), - net: Arc::new(net::Net::new(None)), - }), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(TestAuthHandler::new()), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - assert_eq!( - DEFAULT_LIFETIME, server.channel_bind_timeout, - "should match" - ); - - let conn = Arc::new(UdpSocket::bind("0.0.0.0:0").await?); - - let client = Client::new(ClientConfig { - stun_serv_addr: String::new(), - turn_serv_addr: String::new(), - username: String::new(), - password: String::new(), - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn, - vnet: None, - }) - .await?; - - client.listen().await?; - - client - .send_binding_request_to(format!("127.0.0.1:{server_port}").as_str()) - .await?; - - client.close().await?; - server.close().await?; - - Ok(()) -} - -struct VNet { - wan: Arc>, - net0: Arc, - net1: Arc, - netl0: Arc, - server: Server, -} - -async fn build_vnet() -> Result { - // WAN - let wan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?)); - - let net0 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ip: "1.2.3.4".to_owned(), // will be assigned to eth0 - ..Default::default() - }))); - - let net1 = Arc::new(net::Net::new(Some(net::NetConfig { - static_ip: "1.2.3.5".to_owned(), // will be assigned to eth0 - ..Default::default() - }))); - - { - let nic0 = net0.get_nic()?; - let nic1 = net1.get_nic()?; - - { - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic0)).await?; - w.add_net(Arc::clone(&nic1)).await?; - } - - let n0 = nic0.lock().await; - n0.set_router(Arc::clone(&wan)).await?; - - let n1 = nic1.lock().await; - n1.set_router(Arc::clone(&wan)).await?; - } - - // LAN - let lan = Arc::new(Mutex::new(router::Router::new(router::RouterConfig { - static_ip: "5.6.7.8".to_owned(), // this router's external IP on eth0 - cidr: "192.168.0.0/24".to_owned(), - nat_type: Some(nat::NatType { - mapping_behavior: nat::EndpointDependencyType::EndpointIndependent, - filtering_behavior: nat::EndpointDependencyType::EndpointIndependent, - ..Default::default() - }), - ..Default::default() - })?)); - - let netl0 = Arc::new(net::Net::new(Some(net::NetConfig::default()))); - - { - let nic = netl0.get_nic()?; - - { - let mut l = lan.lock().await; - l.add_net(Arc::clone(&nic)).await?; - } - - let n = nic.lock().await; - n.set_router(Arc::clone(&lan)).await?; - } - - { - { - let mut w = wan.lock().await; - w.add_router(Arc::clone(&lan)).await?; - } - - { - let l = lan.lock().await; - l.set_router(Arc::clone(&wan)).await?; - } - } - - { - let mut w = wan.lock().await; - w.start().await?; - } - - // start server... - let conn = net0.bind(SocketAddr::from_str("0.0.0.0:3478")?).await?; - - let server = Server::new(ServerConfig { - conn_configs: vec![ConnConfig { - conn, - relay_addr_generator: Box::new(RelayAddressGeneratorNone { - address: "1.2.3.4".to_owned(), - net: Arc::clone(&net0), - }), - }], - realm: "webrtc.rs".to_owned(), - auth_handler: Arc::new(TestAuthHandler::new()), - channel_bind_timeout: Duration::from_secs(0), - alloc_close_notify: None, - }) - .await?; - - // register host names - { - let mut w = wan.lock().await; - w.add_host("stun.webrtc.rs".to_owned(), "1.2.3.4".to_owned()) - .await?; - w.add_host("turn.webrtc.rs".to_owned(), "1.2.3.4".to_owned()) - .await?; - w.add_host("echo.webrtc.rs".to_owned(), "1.2.3.5".to_owned()) - .await?; - } - - Ok(VNet { - wan, - net0, - net1, - netl0, - server, - }) -} - -#[tokio::test] -async fn test_server_vnet_send_binding_request() -> Result<()> { - let v = build_vnet().await?; - - let lconn = v.netl0.bind(SocketAddr::from_str("0.0.0.0:0")?).await?; - log::debug!("creating a client."); - let client = Client::new(ClientConfig { - stun_serv_addr: "1.2.3.4:3478".to_owned(), - turn_serv_addr: String::new(), - username: String::new(), - password: String::new(), - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn: lconn, - vnet: Some(Arc::clone(&v.netl0)), - }) - .await?; - - client.listen().await?; - - log::debug!("sending a binding request."); - let refl_addr = client.send_binding_request().await?; - log::debug!("mapped-address: {}", refl_addr); - - // The mapped-address should have IP address that was assigned - // to the LAN router. - assert_eq!( - refl_addr.ip().to_string(), - Ipv4Addr::new(5, 6, 7, 8).to_string(), - "should match", - ); - - client.close().await?; - Ok(()) -} - -#[tokio::test] -async fn test_server_vnet_echo_via_relay() -> Result<()> { - let v = build_vnet().await?; - - let lconn = v.netl0.bind(SocketAddr::from_str("0.0.0.0:0")?).await?; - log::debug!("creating a client."); - let client = Client::new(ClientConfig { - stun_serv_addr: "stun.webrtc.rs:3478".to_owned(), - turn_serv_addr: "turn.webrtc.rs:3478".to_owned(), - username: "user".to_owned(), - password: "pass".to_owned(), - realm: String::new(), - software: String::new(), - rto_in_ms: 0, - conn: lconn, - vnet: Some(Arc::clone(&v.netl0)), - }) - .await?; - - client.listen().await?; - - log::debug!("sending a binding request."); - let conn = client.allocate().await?; - let local_addr = conn.local_addr()?; - - log::debug!("laddr: {}", conn.local_addr()?); - - let echo_conn = v.net1.bind(SocketAddr::from_str("1.2.3.5:5678")?).await?; - let echo_addr = echo_conn.local_addr()?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - let mut n; - let mut from; - loop { - tokio::select! { - _ = done_rx.recv() => break, - result = echo_conn.recv_from(&mut buf) => { - match result { - Ok((s, addr)) => { - n = s; - from = addr; - } - Err(_) => break, - } - } - } - - // verify the message was received from the relay address - assert_eq!(local_addr.to_string(), from.to_string(), "should match"); - assert_eq!(b"Hello", &buf[..n], "should match"); - - // echo the data - let _ = echo_conn.send_to(&buf[..n], from).await; - } - }); - - let mut buf = vec![0u8; 1500]; - - for _ in 0..10 { - log::debug!("sending \"Hello\".."); - conn.send_to(b"Hello", echo_addr).await?; - - let (_, from) = conn.recv_from(&mut buf).await?; - - // verify the message was received from the relay address - assert_eq!(echo_addr.to_string(), from.to_string(), "should match"); - - tokio::time::sleep(Duration::from_millis(100)).await; - } - - tokio::time::sleep(Duration::from_millis(100)).await; - - client.close().await?; - drop(done_tx); - - Ok(()) -} diff --git a/util/.gitignore b/util/.gitignore deleted file mode 100644 index 81561ed32..000000000 --- a/util/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/.idea/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/util/CHANGELOG.md b/util/CHANGELOG.md deleted file mode 100644 index c7946330b..000000000 --- a/util/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# webrtc-util changelog - -## v0.7.0 - -### Breaking changes - -* Make functions non-async [#338](https://github.com/webrtc-rs/webrtc/pull/338): - - `Bridge`: - - `drop_next_nwrites`; - - `reorder_next_nwrites`. - - `Conn`: - - `local_addr`; - - `remote_addr`. - - -## v0.6.0 - -* Increase min version of `log` dependency to `0.4.16`. [#250 Fix log at ^0.4.16 to make tests compile](https://github.com/webrtc-rs/webrtc/pull/250) by [@k0nserv](https://github.com/k0nserv). -* Increased minimum support rust version to `1.60.0`. - -## Prior to 0.6.0 - -Before 0.6.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/util/releases). - diff --git a/util/Cargo.toml b/util/Cargo.toml deleted file mode 100644 index a902e8d9c..000000000 --- a/util/Cargo.toml +++ /dev/null @@ -1,66 +0,0 @@ -[package] -name = "webrtc-util" -version = "0.9.0" -authors = ["Rain Liu "] -edition = "2021" -description = "Utilities for WebRTC.rs stack" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc-util" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc/tree/master/util" - -[features] -default = ["buffer", "conn", "ifaces", "vnet", "marshal", "sync"] -buffer = [] -conn = ["buffer", "sync"] -ifaces = [] -vnet = ["ifaces"] -marshal = [] -sync = [] - -[dependencies] -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -lazy_static = "1" -async-trait = "0.1" -ipnet = "2.6.0" -log = "0.4" -rand = "0.8" -bytes = "1" -thiserror = "1" -portable-atomic = "1.6" - -[target.'cfg(not(windows))'.dependencies] -nix = "0.26.2" -libc = "0.2.126" - -[target.'cfg(windows)'.dependencies] -bitflags = "1.3" -winapi = { version = "0.3.9", features = [ - "basetsd", - "guiddef", - "ws2def", - "winerror", - "ws2ipdef", -] } - -[dev-dependencies] -tokio-test = "0.4" -env_logger = "0.10" -chrono = "0.4.28" -criterion = { version = "0.5", features = ["async_futures"] } -async-global-executor = "2" - -[[bench]] -name = "bench" -harness = false diff --git a/util/LICENSE-APACHE b/util/LICENSE-APACHE deleted file mode 100644 index 16fe87b06..000000000 --- a/util/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/util/LICENSE-MIT b/util/LICENSE-MIT deleted file mode 100644 index e11d93bef..000000000 --- a/util/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 WebRTC.rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/util/README.md b/util/README.md deleted file mode 100644 index 03a6bd7b2..000000000 --- a/util/README.md +++ /dev/null @@ -1,30 +0,0 @@ -

- WebRTC.rs -
-

-

- - - - - - - - - - - - - - - - - License: MIT/Apache 2.0 - - - Discord - -

-

- Utilities for WebRTC.rs stack. Rewrite Pion Util/Transport in Rust -

diff --git a/util/benches/bench.rs b/util/benches/bench.rs deleted file mode 100644 index ff5eb5e71..000000000 --- a/util/benches/bench.rs +++ /dev/null @@ -1,33 +0,0 @@ -use criterion::async_executor::FuturesExecutor; -use criterion::{criterion_group, criterion_main, Criterion}; -use webrtc_util::Buffer; - -async fn buffer_write_then_read(times: u32) { - let buffer = Buffer::new(0, 0); - let mut packet: Vec = vec![0; 4]; - for _ in 0..times { - buffer.write(&[0, 1]).await.unwrap(); - buffer.read(&mut packet, None).await.unwrap(); - } -} - -fn benchmark_buffer(c: &mut Criterion) { - /////////////////////////////////////////////////////////////////////////////////////////////// - c.bench_function("Benchmark Buffer WriteThenRead 1", |b| { - b.to_async(FuturesExecutor) - .iter(|| buffer_write_then_read(1)); - }); - - c.bench_function("Benchmark Buffer WriteThenRead 10", |b| { - b.to_async(FuturesExecutor) - .iter(|| buffer_write_then_read(10)); - }); - - c.bench_function("Benchmark Buffer WriteThenRead 100", |b| { - b.to_async(FuturesExecutor) - .iter(|| buffer_write_then_read(100)); - }); -} - -criterion_group!(benches, benchmark_buffer); -criterion_main!(benches); diff --git a/util/codecov.yml b/util/codecov.yml deleted file mode 100644 index 2961b8e43..000000000 --- a/util/codecov.yml +++ /dev/null @@ -1,23 +0,0 @@ -codecov: - require_ci_to_pass: yes - max_report_age: off - token: 5dbbc458-896e-486d-af8e-96fc2fbbbcff - -coverage: - precision: 2 - round: down - range: 50..90 - status: - project: - default: - enabled: no - threshold: 0.2 - if_not_found: success - patch: - default: - enabled: no - if_not_found: success - changes: - default: - enabled: no - if_not_found: success diff --git a/util/doc/webrtc.rs.png b/util/doc/webrtc.rs.png deleted file mode 100644 index 7bf0dda2a869e19f110bc581a3d20f9609982824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33276 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 z zz4!Tc#dxiysfjf)F%c$ulwsh%v#R4j?;4+)if(Ld{QNba#pmOmHMQRD;cz&eC8Dj3 zb8~Z3tJ5BjL94zW5O9C2q}^t#^&;=|$|vbNi%-skZ-5htyqF5$&dFLBA9&!fueSyA zx}RgwYqvO_a_O|&&&V+Lj>K{Pl}w?KJAYD^oH!~7(6{`(o@Tq*k@OWPGCVI-D&!lD zh9UKRF4z$X`OlZ%)|)L7mY~e4I|0F_9g&7n)?hp-oF1+2VmCFgIGpyn#gbgoj*d(* znDoX+vw5*t%tJ+Yu*6u*#-Oj5Nq74;Fd*1wxAFCM5XfdZ9|%R@8CY3KrT#t5m7O=QUA@cpObrl}<7=|NHfD z&zt>VD6w2No6%R`aXnkDTRwn+g*Bcp6tex*5R}0GTDkee>3Te`yp=IT{5nZm&#E%a z9$o^H2FM6i$mdOx!>g3b?sT2i(`sPDLxL;tJ)mE#x0?Lva$EjkP(UmaK3}W#0``K# zZq4NX?&In!dbr1@p&tl6a>1_B3E7$JmJu8?hC1-EoTFnW)fGMFZ;cDub^`Ig`H zAXF?4JFJ1hQLQfXSCf(R6#aigzJL771Wled#`fJ89OAK9N~c>9p`la~RaCY2ZWzGj z05s3nOLf79+v-Izsj?!m7)()d@!JtJh3|6jG~f;n4&g|IvyT@WGJMx)$jF6wWxLw8 zerjmHv^>2-4O1qo>WSFd)6!@)l5=xo3knGH^YdR|WMoo6g}Ui z@^B`{>+|IZPS&MyTt$Izv{I?$`u?7bo*toQMfwY1pP!#!_~&EjW7SJrlCmF>xF?pm zf>`MXtee^#D1>#Uu9Wd^evxYWD;vB%oOSo`WwTigy}#VuJUkqH5svZZSuTg8(Q2u@ z7#xcji`6{Vxyc!H1X{p}RFT_uCjFuIshSy%Fx5UWyFD0^#p6-06S+lhy;41z%jJT_ z=k3}#s;Xqa-XTn^CkS9C4-_+G!)o%YM z8`r)=h)c6`4^y}JZ`S}DmRH7h$16;VtRgF~^AoMABE`bi7c~`F7cjAMN}0crjJ0)tpLS zv;A=n8@B90SjbKZ2odU3Cg9@Y%KfUpSS@B9cSm62@wkS*5`YrD-IsSLgoTAQ+wJi= z|G1|7F(LH3S5mLj9-YnOxgg*)n?(Q0A&|54&Ag>|hSp1l$aN{1pJd!zp7 z%gI=h6KBJ|nAG2n7lPr?WpM0Q;-QiU2k6r?Dlqw@koJ< zk#nU433-b;8QDmRKRQk8Ss7+ z!QyJwc-)U&>i}fKT1@?RAU}=I?OMqq%KQbF@b6VzAq!sy{d_6V#{zz0k5bMmlTdGj-Sl7qC{P_jd-`yTR{!lpT>i$+Y|Eg-bmtiQ- z1D8GS)*!M6Ajt#0S}YfW8T;Rrv4;=^-mgU#ekjG1O-rc?yqz~!OXXZ4dKSLD);IuV zWz^)hr;fM!`%mHMh+(Ivr|Is!Y;Ebj3Q)hQInJ-te%XD+Z}zLnXjHQNlU;jG_9A6; z`WSCOU0 zfQQX?`o7$<&{uT-UnO4|74;W(J9H^sl7iF#(%pZ$MH-|_y1P*jB&9o~JEXfsy1PMI zy8Aucb??Xf;r+x~1N>&@#D4bP&ps#Z2$VR_TTNwU9b4+Wz~Q_4NFF6MX95MpgF5t=F)Xe$T!~ZTCl__XZ?F-VIBzH=Ri2m_$U{ z`>QX^e!1C=-D;*(xnll(-Ao>yH3ulJH61f4gNe+{l@uz~X2Zy+s2h>|M|95Rpsc73 z*CihX$T{mbcI8V47dO);51Sbo8z&3-@EtDJGhaL|yRGV)pcV}7{7&a}`fWk!L@i+> za)b;BOrGEK(+i7VYJAPSzNb}LW$)2$(Cm5$_JP#T)g|}M2#Jh?Z^X81@fvzBh26x| z^d}u2Eie#a7~NuR_a^c#_NT)tD_QhB_wp#C7??;O;^$RXyp9EbRad*XWpY~0m~8ZX zSLB`D%BlT4nqj&>RdnlDZ?lLmhQ$!(?d@$29vZ#l^>{f8o`nX!yxDlR1QiVpy@k`O z#3Y^~KN#F>ZBzqENyPePS7Hpo3#IQqreJKKQ;diqQBlk%Za|`de9y8BJR;&Rqh3_RSA=Uy z)mC5_7`1CcUN$HIM#}3!pVMA~bZiSe^ohax+$GD}1g}VAKz;TW>(|UoBy*zi-)sN) z@k1#~hWO2!WMSzG0IN87cv~mzTGeI|pKRVK1%R@NI_lEg@oBtDI(&Ng>kDrLdaSSY zdgWk@{u@wdU|L#5Go{4ESsXUR!3Xir-`f#%Jl&8$C*kS%WjdI|{#mhc)@qi`Xq7QsVQH#r!Yo z7t%i}bkDfzd;cB)gk#a@YPJMg+vC-zTFM@Wy2h@ABql0&ZoG$V6W~UpyFV&WsYaB?dL7;{`{zgmP0QKPx78)8y z#Np{dMSIglTfbU|qz1Im>Y~K-RsxoHdOioR6V<0O)u= zz)%s#qfA6ZBzwL+yu3p8cz0?1{PYOwTjdZOOF#hVc-$ZMDt>88`;J3<281r1p;I{S z#C#e2*sZtTvOvF4)yW`p`&ij0?x*Ya=XiJT&EFBrD>S2m*CZwCWt3D@J$SPS89lzE#ml1y;mU6g<*cz4pH68d40z>=3G z37{g~RY)q92MFD>F)<8Y0ZM%vEJlaCjiyM5&@?qd55{z7@WkX~gvxqR-?`4*-F zB54^JdN!5|U`-BkuX?K-oyVElI0Me?+?<^ss!J2W6`;u*37>4Cmz;V&mueZ_ zXEFc?a!ks3;O$TY6i^H#&Q(jSS3*6JpetoeZFso2Jzzvf08Req+D1!7^&=%kdyhZq zO~h|y)4&(s#{*E%H{7;qDhb^_fYPI*7-%K47{dDc`qG7NF;mIuvi-4_ar%7o9)3iF z7K0yXV=S$Oep~Eei?31^OvF5ElXlKR<7@TM;*dQcg&g=rU=E~0b{CK@8pP|zxDF*zd&}HvdXKC!O z6COMrlG2I__@A4tpEqp7hg`P!wil{ysB%y=cOy3v)6#T|QVOz@g#FqEhminX&fIqZ z)>jMge1fhXD(9_xe*b3kyf81wU2X@j-GXY6K1+iB$3XT+5T+cal@0@C&Ck;YhR6rK z$b%1`xmgexzC!8;wEi9*DwvzowF7u7^5ZufB7}v7rGIa<)a1fIt2nocFg`gME97&B zo18tUWpM}Gkv~1(aXhZ(KU+-Xq5wwr0Qk4!vcDiBW2vZQpvprvAuWOi^{iR42C9Of zbLNLN94yJ2l3Dh3d`s5JDTKfH9V&JbC3`k+_x2)9OK_o3KMUCZ1MLCWl>A2gXPb%VNu4<$R62@%|rumh!epdpy6`uTBUU=i&zab41W# zn5Z#L(AC{_&pGt!PhkY%^{DIr%L3#TdzHes8&B!W$)Ond;?C{iep%_*qi<}BbI%kA zy#5aNHeHw}AvU%<{0(~=TS^i4MUsI5sPBAlJ|_MviU1q)4?u;1(ks5QH>g789NaWsY2?}cft(;bUuJ?-NOEfB8*ijSo zgQEdrx^)0a@q=%tNmj*f;G#`!U{q>yO^%#uTzD&Ad0e$1X|#Fov(ex z*cf?5E~MjT>baf#d9F=Y_}(G;k-f3MJM8_oY$|(F&|fC35h1q=-eDW7$=nbH zp)KK+CY$$(DDRs$Qe^k`_5_|E&KvlF5XseV_ibXB1#TFcLS?yfi=+G4Vu#%}Y9^n_ ze~uL)2C>a|Y1=cLmZCSZfrn;PP&@}uL)J|%#$wT4j5Sb?O;a!rEpP7c*}bl;m*W|L z8Pxv#^hrPHY``$C-XLtgKD+#FK!F6jZ7W99_EoK=Xu+`bIDx%o3a(-G#=RS3O5;Yo zjP*(jZ>IMd3J{(y_kGjly8M7r^DoC00Q(JCwNi;WQ(X+fNUg7WG}GGNsMFZjBk#O( zyXL+o5c#9a&e({>vzO+U)3bAj5+OfM?tvnbzEfmj$%{3?l zp6j~&__kkNI~}%u@6O)@r;ZO-pE$UqNZR#7kJCpKY(78^Ct5t5K#>ToujdAyrt5A! z8a`v8^T$S=9yxZuDgcWw{uB*Evi1Bo)aNx~kK$_^$kV{hMMohO6ff}J>v14Rj# zON0I}aOQKBKjj3N^!4>$%&#k@?F|V%?^B83s1Nea%de5xVc8lsMM-DwG8v4$Qs_9o zg(^K2E5>AfT*ee-qj>NMZ1}ERPBi{Y`9#P?oVz|IF0SYK=~_5ADk>~&GcQq(^8Nd= zIb%kAG~mkp{rk7!!Wc9FHh@Oo1m3&z4L9&$^X0C-(W3SHG``a+ z5N6g+LK&LbFW#?_@&}Mp5i-Qu8Xu^7` zUE4Z%*Np|Miaa2}m%Pu10bK7NRUOV%eFdsIwWPc$6a|N-()-pCn1VcBSB%O7-dKKs z$a?^VChPMAcy7bVASpilc4hZz^g0^y5iXWIo=>H!)Ei7XgM=bDuI+$->Pr+Wv>T`L zd>{8!W>3y399GyBnH_-+X!X3LcS^aAekbMlaO1#bJ*V`K^~G8PoPMfQONAh6?@#nQ zS0o?Li``Bz_>66%AB(ixyT8}tJAp5C=u7_Q zO+Tn1gN(kA&m(Duz$9fl9GQQ56Fxwv-mw{dfwlgs^k}01{>1ScdM%htI5r<*iY?@~ zZi%E8y$5kmbrLP^r`al_+>U>xZQEN<)@}e}|C#E=MDOKdZ(liCy{Xzeldb5dHTCE1 zfR2H$0Zq&-3(ooo1HWuoFk2^{{A*S?eHNEZRBt%)B8RJ!W_73h`jyam;Q9f-QUQoN zV65HT-5IeEo}8Yp18#1%F4|Wo>j9uDiNiwq>8S-6wBe-uTzOVl)M>(YI%gb(ML@7KcKhwsn=XJ-{R0DCX4TQr(E-zz zy#_4ygYWAb+Q^AZ!|QIVga4UO9mjT)j7Ojqe<2XRgjx76G6#ptwe)N^yY-!|nV%Wv zgmTmPc<4S{O&>73NK*DEa%FI?{>`h<4W0swKpP^ES5Ra6#5D}8=5R)CX0g;-KcCpM&s2@_v>9-+1i4n=YdRCs ztw)o8vyNH_5V`;0d&-dh>ihMxRQ)&!n~8KZbfu#Cs0r4O--)2+m*c-VzhyOm6-Kq42;JPF3e%%p~=%+4~f1)=+ z@L>8V%;c_Ml<_=zfzBiMBMJ%m@n-I)SmdP^J~#Qvz@rNeHg@qt#?|3HGqH*TF`Y%q z=22-uXZl%8?7Ez+k(gqyPT%~^j7eHaMBMGuTtHOwQl4cEcE`k7Oe=G1Zjj-Ov~Nwz zV0(+mm*Z5%SDM{9{~_&RyGMWX#kRBiFh=jkj3QMZpwnP)7_FsP{@|k)78Dnw?|V!A zd<-ZyZsY6E-rgRsB>pt6cDLh|p>+O4fCPqIsqEk;q=r_evJOL-_e2)~dWQ zUvz{NAOH8{2VsRwAs=%<=(1r`AAvOi>=l66=3#`yh(9dukkURm)&1@u%mx`9w>E^`H^>}4ChFVTqUY?4F zhY<7vfs&(N?ps-`58rVeD)Qc$v7Tl3)JQAdl7)m3bc{ccf6*GLu0$j^{i^Pf^Ba7$ zw@~PJ(0e4X{ACMz48TSm->Y&O0eFOz_MajllOMdFn3tDw=B>-QMymzBt1`s2 zD}PX*&LfUabB`_F17F^XQb37fXZbo7JBa=*1B0BKsqBEF=|K_w;WsZ4HD_x7UX-Sh z3rQ?Ly-v`@!E6+$0MS5Z@wpv^SJ-v_>jVPZC0S~wc-rr{1iWtB{^$z&7*fv89H8Wk zFLPzDQdv`-O|29&hg4pv!4|@w$lj^$&f{EnZQqXFSJAi10OkQqDW!kcSNY!nA>#8X z4tbcbF z-4tNSe@4lH5O})T!~pJ%Bxs<~OWbU1ZpuhY8vx{%AajBS|6E~RbZpqHM|ns-tI!Lb zish~MD50-_$AsLnT>6*LKZtTMc=M59yYA26zo?^>$QNL${ydrw%zF;2nQ+inoIg4y z#mbL~Oy#h^1~yj?K!%p5y93Zl!$7!WhJ%AEF(I|n>aiP2NVc99o4f`VlA>*yWgB*f z&a0ZD8mqj_V7L2@qY7uMNWk?m{a^ASkN4N-9dH{1hjpz)(FWE@YU~vKDh=ruLP$Sm_n9v>I7ByI=f=Gv<|^&*Tm}kl z26QMgDQpZ7W%WxuWr12J;PzFP;T&GM|elYmmd@v&Qm4TPWoGOCo*ae&*s@`U*=kxpf9o z|J|I&2*|>KC5o5ou8JH@POWDI+%*I?86?n{0!D z8$7(cCYxNWmcM8AnwfT^;C!(4O5|cV`n;WycZVkop-{uol<>dBTD$HbZK4D7cGP6J z`u4t$^Z3=ZpQdTAyrQBRu<-sZHO6N8-hBs#2rl1mvuak0391!O2DgsWpTT5CKz2!l z&SnUDy$p0`XXlFs2bNz}$90hw)Vt+SOg&>UczRNbyUjQnU-2`I0}VS$YE7f^@ak|9 zX?i?_{)cf0+(NVJ&)^m`ZoyA^5`geg@$)Yp1#W^w5Sz&#%#jSiL16yr{M947rA$l- z&9^{Mwn~@f?f<7xePeW8V3IW1Z$1{2=D#ptF*vIA`v|%GNK)w>;#9QfNR3{0XG7-g zttCuLryl6;i!7S;4-idvL?YW zFB8mED=e{6X=z~v&eq_8!Xn8+rk&03!hT4{aL z7&f=TQT>AM%tedCb(kkSFPYTfs56Znd(C7K6azhp?H@;jg$4bi%A!NuC+EbCUg>|w zyNqClk?8FgrBVwL!Ji6riwz*lLYSiRmq=>sYjQ4&4nR>y2q!PY|#wTw}zQB)d zud8h}?>D|qQNqf??hq&?nR2n5%U^F~5Gh7Y<>U@}wqsuxZ&?;QK!y8`hyPgQcJxHYzp%V5^ z+fpg|(Qo_J+zm{r7lHC9Pem^iCuO>hYM5Qt*~fxQYIi!AA+bvsJyZyRJyyuH+E3Vn z{|0SGgzdH^G|!1zre3xAj2CSnoha3v7j*uGFOK+;*5g37n36RF7J9ZOO5j00$03qG zdv&G>Rrj}R!)~M>zHMEhf>eU(r?roj+G*@>DPNOmdaJ_k=H`#cn!*8gP*bfLie$H+ z|K?xm_-c6WQW0b7&VTB%y;76de3p7i_K}e7n`!4gs>(adeRH{ZELEHa^VOR;oDLdY z+~0#33(GofPPu8{@J2gw^BZH682)=;IyFf-NBoZwrZ&!rFW4u1zD?>SMt`LVyQI05 zXTFFb;4^r&7)I-aV0S?K1Dx6HC67yv_#gt&+CH=4GdsVa?9;a~JV=|VxKVxsKtaYE z$pp4dO*`}#hhOY-P;IqiK}`~S@zRfa8D{lFr&$T72!pNYkOgK=+Eaprv4dE7YM z4X1_hq9%r)&WDDK7WM%lSL->(i}(!)Cc z)#XKts1lhjZL^!ldmxoo^4T`GHA~nvHDEm$33n+|@8V#VpZP^iHiA-EMkCBWn8Rt4 z!uB;3LK+535RJ@O{tGsaZN-5BBX z_!S`JU&(Pil<%qap}{tF!Pb=(pSE#Oezx$= zDe#J)jQZ~#H21Co-*ISZ5B(-Y}ytNhM3&PIz}-8gk#-lf9Zs+CUp@ol0@ z$7*4Q0Xh5-+PF9W;Q0pNsm0$JyTjgOO)&9TXdL;qr@lh+b0g+AL;i1a!S}D`ufuBr zclT)5OBYKx=baNiW4D&V%zbc~C?oh#BK+@fV{z(<)clJHH$QA!O*d>_|!vi+>XbQEx$JTEl}sW{`^uV=t?I&uwyl z>xQ735OelctFxbUGoAwc4cTjt$R$B>+&o&+h=2{8NH)twl-x35`~Q;j810O!A{U?- ztZFWUasA!{@LTSu=$&CHrT>k6(CRj1?08${pNZ;y=a4w^b_VWgKped~*!mN5#zK5H zIMdQG;OAA^Babc?@P2>@-B6OH1w^~TmmASK4c;mw_y3$d);rm)==mVw@A(~`;9Y~h zR*q&&^eb$%dF$&!kL7Z)*ml^6`2Nr>0`JM-!aXKg$mP2)ZwUuKNk4ti%EA0DZHtHI zU>*9qg=u)KW>ut4J)fbcAnk%|a7qE+6m=SYW-?VgG|B_U9f9-1i6c7Y?m;DZ|J7Rp zvTO{@ydk$+{)G7{OE$^xnW2!&!=g=a*vC}l+|o9b*PZ=8ZxPpIU%}dXq}RjDU%lV7 zTC3}GrirqJJMhnxEC4{~6t{oH`DaAe2QS7ewnZ;M2NnT)f zdiXbg)~%rJKVlXF_KMJ6Dvbd=VXnJ?Yxp0r5VwJ!@gZ?hthPH*#V=8`S#uUlj0~g< zSj)znLu+vsHE$gWoX#CL45_(%*hk%^#K|8+eMae>IL)Ia+S%}9k3lCgEv)RY{e%7t zd7^c&`i7e}RUtgOal4zg@BdSTkagC&@(Fx5py6G~Jh`E%g_T4x;Kc3vd`kUQDmomd zeA3|~J7%fmNn}!#&~@@-gB3C6=vNZhsj;f}QEz)`lgiGgl)1oJJvk$uZ(JQd9V{Xk z&2U?gRSL~71XhCrnH9gZSbIdygnfP(@c=t*QmTfw9ny^*U!w}nu4Q1NcZw+3if(dI z`J7XB4|u8*1Pph0a;Sg5eK?r-oi_WP(6E`fQ{k;AoD2QwzTev|L=N8^v;B%!5!?*N zfaB1Wy^VZFC__%=F*ZY0QyxV@_ITkZB^X{x5SO=c% zM=l%zmX3%ce1w?JTA`-QWu&YSmnY?`;VO$+%9tz^|Len)|KERT*2%!q4h#;)NKn4Q5_Ok`VrZ4ke7FYA-_dY+gru<; zMURd0fVybNLI*EJ&q85g;qXYqcaSW$RWGS+&$qbr-$eXsI9{&Xtaz||0}@sz*C!{0 zMHbpfP}6+Oje_0XU6+CPB~r_1XlPb&&X}S#IHDX47eR4s3bM(t4-|nsV<5o>$DmPO zLm#X50|Hv!)3erGen?B+z1Mw$0s?&TQt}`PafB_(NWrb^1(O?B0)jW>AyLftX5f7C zI! z_nrsnd<6qB+r^%qa3xNI`b~&F{+9J@&9`9nSMQnKDN{e-90@UCfXx}_d7Y%(qoGbA zF751m@w1rzlcwgJG(`_>@7#20_F;UL~z|3Rxq4Vkq7{uGXo3?F|c zM?Pq_rplGxf)oQIYVpF-9lBN4>uvf&c|}ug%M!Tw? z>`tPE&QcmmyT3cW{02Ow0GGMxKeOsvf6NU*+7NOPIEtQ6z{$E76BDx&U&n0(Y@7M47JULr1H!S&AIZKKiJv=?Hn2VdUv3ISiG+l`z zesgJfuV9~ytL2T?9LVwIgLxGK(@hRn5)kU1QO2S0=`#n>D#*0eBZWGmtZ8{6&0E)j z=BiQZu&WfbXYFc2l0-76io)JTW*C8Vu`MJ0*i!W;OMQ`ODhHhJ>({Sam$>>|2wU|l zuYEv&=K8j~_BG;D0jfW_Nq4vx zur1z@ZPQ0)^#&6uBjLjf^tYI(Xct?S6LiFbSO@O6BS%3nK`_B)XP9CQrk-N?*hx2+<~O}#6} zjP@4fkODC)U^z)|`olW3{Tmt@*gJ+nNUvgReSLhQyE`Wz&h%BR z@(5$+4n8u4I-h;HJVj3?*nh6gs=&*-qGaj#FV-4QRDhh6lr$(EYYSjaqr2ZytZwe>%kt-L%;kRNSXo}6SyI;k;UDvw0X!rY*NnpzA_uySzJ=z+hQ$Bt)q zTQQP>V@#dioW}5=U)2b$8!h4Z(Y~P7$V1c^;k2js%Vp~FQJW_|5lJZN8x00*} z6(8T7+vg>HadB~})Zy(vs-$6YXF*9xNxYFQSvlF+R}i0Y$<4tID{Ao$y_Ec6SX&J; z4OE!$urN6cFa{$yjT_=+CGTj5tPtb!MwQqJUva|@%JQ)=6V)#6l~|-;i;(`Znl0}c z8yka3$?rAvW2^2os#-!WwT~{mvGIcGG2P&1q@gkMu=rrdmaGS!Im!erMz9SF2|@lI z8ako&?53%yNkAd)mxVKyvh;=s=7Agi^h>{qXLAo-k#V0H;@6+TnDl|Wba}66++uv+ z8X!&ta8UF(KuuxorDYr6)6vmcf)u<~FK0mif%%bkBn>SD1?65~L<|DKuY0){0A+nH zQ$03CV@?z#fP5p7v*Lc0V7fMcnXuvd6cI8O(ZoXA^OEXYuJB|D zap;PU=gm1*m;#f2aNj`Be4Vxo+dQ84Wud7sg6IDfEiEnU00c1yYHk4I@G8Ka{f+_#C`TV=M4%o2Lz|tQjP#$O| zv3#;fq04VBshhXfgI`knW?MUWig+gny$8#Rz`q_1Ijf@LxURI6ad0bB_xrTvA?h5S zNb2y+sG@V->l-rnqNL*rHlhd{d74Rb>LvMEY?5E9!!N*xWM*cDKFeW~8^`uA@oDqy zS#Mhha&vQI0@72LT&ahxb8~Z-czg05azz<d5>NxcD_U3^rHCx{XYMS*ue=VM%0RdL@ zUz&`m{$t8FU)2L`4w8qrU3i4B_3nJ@y-gSD-s6Qp|I0f>{Sb^!`XCoFGQ(xJvIIYu zDs9P)`fsX8#VPAQl>$jZt(csLvp zr+idF?=@%5HEdMntwMC#{1;nd&ecOAlw3ooA%>FH>njyRPfxG4lAVVlP&0K_V9{WQ zuo*UqAZX#;`AbXc>e&VKsUyeDA0%XX$qRKy2A1TItGK~J_x&uCc5-skpOS9_uN}X> z!x@B$20&l}WLycI3rW*SF%ykF2}Gb<5Tpm=@6{DnEQDl8q5EI@aYcv9y9l~sCE!^L z^5UHG;_59@X4q(HX*>G*Xpzks0(Y77(!91>!S)f9K1E}h2xIvruFk#Mtm@1!tJ*ZXm3KUrI z>7w|5>R`}NQJKIteMejbDKHhWc7)s}H{l2?oy z?N1$(q9MVcxAf6Tihf-W&!Orf%L5A!GntaTzm6Y{W=F0$gclXk2 z?XbfFj5uXi7|O6l$$IRbZd+VpZ0wEr*x2fY|4yqRpZ%>J`TxqvV!!ow>+bH30>$iE z37wXRChx*ETSbV7jI50vKzl4b#=YZxaDs{>Q%h2-*Y#7$=SwsU3_I#P#-jE^bBR0? z_GEc0Q10cRrYZ`PuyC-j==hw|IiZ!P6l4I@p0ZyFDX$t;IYYCLo{30FeS}SS<%Z=* zubfuevr{_&^P{XRU>sg+`3pZgf4jFJ_goDkJH{QEc{Ye{yJMZ1*xx2!BX-kxt=`|< z$U7LMZ?hWYPFt3JW=kdllYtLrOB#7xOSTdI9!HZCy7gHi&y^lQ;(I1M*WU;p^x^w! zdUQp%AIc+<;F@kFwOR^F$_@IQ@te?Iqua08cOK^h{5a{x)$=5KDRU~(rYyO;eBw&rVJOI(zoQt0)ez{&J-1)R^lB0LOHnKiP#k^JbrsJdWX$Dq<$ z@s4}?KmX~-9t{_K4^HM@S#k#8ILi$^#N_ZUjR8SBh zxtWZOevaUs7dIS)d1%e`DMwky$QbZmZm_MqpuJg= z2@9?baZi;<-ymZ}O4=k+jse`m(MaoCSs}psKg7fQ!ejor`ClsnpxY&L$z{xeA#Lub z8_T_y4tkVeG3>MmF?!kMAf0XFCM5m=kVG7c)xK0`BtMj>JkTa0iLygv2*Q!+mJH0C zk3HB=f(YRw2l&v4n@2Q(5UbKnSE>Qw50{(Ezif$rNJYC`bOoU)k`FOZ`Ga$j(bPQp z%YfJDsfe%6DnxSlF{3$g|5#krH2D$DJwN$#cM|LLWfVybDqB5ms^=wnn^r}^H;(Gy zrWWW~-|`ls1;-r`dD+(aXlQ;@1=2y211sVj3nLjugcK&5Qsnbv%asO$Fx${u7?q2Y zZ}MOn20bF3B-9I+Oj_|6{5{Y{Hf7CWU7EA~3esEay+4lY>Gj#ik?u6HW#f8$eRq{+ zQ=PtR2a&ig)4e;a0E@&Sk1ZoVo@05f{7irZx@E$jqZ z@i<3>hTiq5SvC!uox5~MDVHQejyRI#-)vpnZ|tvof!vm*?oZexsiJMTgdePYa>yT- zoDsc}c~FrJ^rkGB!+vQYS51ywx5`!a?oSXg)6-jiMo~yZe~~lqH>-KEaB)?OZ;d_P z>7L8-lyb*cuLg@@F|mkE*3{9&S}4otV(^7S3q6n%Uss{c`xriTzAtCCkN9gH|GzH>S~e%N<5@ub_;n46E3-X#zg z{@DIB?(aB%>Dc`uLdwCxtRs@VDP4z8#)|(@>f$&I{hdTHTUpFH5Fp(6eGBWKm>2g| zdEnl9QLo8G`|_yL@YX`Za|fF8W+_~oPjDsil_ z41#2ls;6C#m${JZKTVkd6liwzAM)&4#T3Nf5Ir&JtSWu6_pI6n@r^pRJ2GMNRz<2! zG0DOxS;Q1U{NHys6HI*o>6M4pdNnD!QuJ7T#FGs%9}yRp0u!MEZl>}bpYPu;lhgsn z?E6xM{hrRYx3>$7t~9U;dyw@d#ochjB;MzFM<$cM%htbP9XaLb;4!P5X|hWDfx2-C zy61%xd&Hs{>tC0bmuG;VVMZzNr%9C&zdra?SHESH){Hi-tlTfgl8jDoDx(c99R?84 z={x>4{|_;5%CWuNIF81i%zrppe%n!ACCNkoRm3hFj>~goB)XJrqlVR^rHabr0bm4+ zb%}~+R#47;(Pp<}U(hjrGs|)==&W~ z;d%DFMMbwE4yKeLUX~%YNf`v@bw(9!tOBo|w#D21t+`U)Wk*r)W!)}|h7^<_w0&@S zfPbb#D`>^)+@S?Cr=I1hk0)p4L!ihGbhL{!^inNvN?>*Bk9*+V>Pq+x5fLAmDXI+$mOlhj9rYoIi1Lm*5V?clcVb16Xk%^dWg~I~?EtQvuKRkB zn|xmX&Q3B7p8rpoJJAqs#r1YaW`APLmY-a|V*rukhY?^&r^%VQ6Ar@sc)L~4r3~SV ztd2|W4qU=&aWZJ1`-ka&?CR$B2??E~VdA`{ojQA~P%rahDIp=@16+gtl(9BtOYZnx z!}EV&W^s*dxh2dwY(>hdR@t4lhq{sJLU^u_L2%0JS@o`16q&aF7@twoo~(<(%xlm2 zE>Gpsk1kJEG$fu?-Vk%VRe8W7ul28EV;SU1VkpOoq)%(Au$Hy3V5H%mw1Yn`BZJJ& zz`!8vps5~v+!XS~lgbY_yqaJZ zRvjT4l8hO|1|nRk%O$mKWYc2f3;cY1D-4u~N6+GAwlIGu_bz6L{Hj@hW`z*5v`}pD zk(cb4%0Wl%Np?O$w@{#_D-LP2F_H!M4<27E*s=ARQ{LOKp5we>MK{)otG8Uofm!9LBajQm* zDb*&US1UEP=)>w0-bjs-30$3raNU|_Yr}A`pGSaQY)rxU z(jq|^r*%fG%|03UwL5LUHLLJi^%~Mwp(jFb@}_LfR>&M>OH8$*rtX$}+s?Qk)AZ9* z#DDLBq#f-`d#eExLNDr3Sypy9-%k`$e(*%KK^*QN{*XZ0ztwAJUTR8S<{WuIuj?jzuAIl9H0P-%}Mt#W8wqLjFc2lWXvEfmNT+hnAwq zS)4zCLh;0)4B2*UTm@WO^kFW0obQZ3vq%DBSvEW4cnT^)gz^BHs-xQ1hEa#KFvh{A zW}xy7{AaFx&7M%%S0hiHyU4kA;huDbGM^csTd}?tRqr4vNsX^NXfZ304*a|_t|R)% zzJrJrsO6l6s^RFHKD|b#6{pXQYbs>3E^hO6k8gw@=~4d^pJh9*Z5u1OzG@wM*qZ!L zTL#*WzGhQ)d97GZmD%B9|IXi%|Ad(?FS1AB`vQpu1bci^hFsn1k%2;7N9wdn^k-$+ zjF)DDOY@Egda=}a6iWGrxWTQc#k$8D{iA(ON?Dks?xD!`YTVkK>sTG8M1>pr6?l8c zyA@uDI-67WKWsC->B1uuaaB9xl;%f?-KCIobt*jnEyrYIO{S&Y{)48YcafWE-9yjL zsHVSR$}aW6xM3?|tRSuBh=#P++uUo2Ur_l!ha4OcIgy5khnGT53_84Tcrn>&QHgR`;|eE(yHaY8Jl}UF z_nfEh>4T8R`yTr}yuQNEM8zwA`28@`_wJ6V`&Vg`GFrT{9dD%5&fMEq>yCX6Oe1F1 zUN{eavL?CEs1*zGY{QUgtAWQ-@3jC<>a7HQn$=a_93JR-;M(T)6xTptM%na*e;M7dr zG_O`>DDOv{>*d4aoHoMR=hyBT`fTT+L?eE@ z8}3ga-Ht^3mWoX3m44OC-9+H#?iS(fittBv4QOpjvK=gop%(Yh1H3=OzCv>vQx|5u z<#r}*c3;vw!ij97j<1!8N$%3bgyYb!(vOtd2S}kvD8f<6!t=AU*Nbzo_U7h=e}){C zWuNZ)G}Iystz}fic^7!CyN8e! zuUwxUBb$&bmSVQ;m%;C5Tdz!CXJG;;PhcX4cB=R}`kD&Xs&g&$ zrwBd_toQ1&j{!kiDgz(g-0C@jE6&fpikP)Ln|5{AwaH&=PV`Y4YNJ{lVxk1i@EBYD zp_x8xMw>fd6L-pFU&cO}Rguc+K>AOBpy*d+>os+i3AhnWAUe zcevplB;(J{xjyRHTPSEZcx1S-W4XXK5XKi1m_jP<^AEVpj*4oVbvvq;CB;jWphotv z@w&g6r{4Ye0?OBiM}@B?A5pgEyN6btb+7e5?np`iJ#|goV!vDt`v4YLhh41h_P&JbBjLN5z7BLu27bp-a`D;V833f zT;%*3>-~Rut4bUA=tBaKwP+v%iYW{xp*s@>EG=+*|NeP@Xw-3qNK-b2Br{)>P!FFG z48_?=JS2pmvz{xOji69)(aehgw;b_nrvc0V=11)4GmNO}92rE1nh#ud3z3mf6fYMw G`0_t~Oa}h| diff --git a/util/examples/display-interfaces.rs b/util/examples/display-interfaces.rs deleted file mode 100644 index f04badbb7..000000000 --- a/util/examples/display-interfaces.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::error::Error; - -use webrtc_util::ifaces::ifaces; - -fn main() -> Result<(), Box> { - let interfaces = ifaces()?; - for (index, interface) in interfaces.iter().enumerate() { - println!("{index} {interface:?}"); - } - Ok(()) -} diff --git a/util/src/buffer/buffer_test.rs b/util/src/buffer/buffer_test.rs deleted file mode 100644 index 375098824..000000000 --- a/util/src/buffer/buffer_test.rs +++ /dev/null @@ -1,358 +0,0 @@ -use tokio::sync::mpsc; -use tokio::time::{sleep, Duration}; -use tokio_test::assert_ok; - -use super::*; -use crate::error::Error; - -#[tokio::test] -async fn test_buffer() { - let buffer = Buffer::new(0, 0); - let mut packet: Vec = vec![0; 4]; - - // Write once - let n = assert_ok!(buffer.write(&[0, 1]).await); - assert_eq!(n, 2, "n must be 2"); - - // Read once - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[0, 1]); - - // Read deadline - let result = buffer.read(&mut packet, Some(Duration::new(0, 1))).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), Error::ErrTimeout); - - // Write twice - let n = assert_ok!(buffer.write(&[2, 3, 4]).await); - assert_eq!(n, 3, "n must be 3"); - - let n = assert_ok!(buffer.write(&[5, 6, 7]).await); - assert_eq!(n, 3, "n must be 3"); - - // Read twice - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 3, "n must be 3"); - assert_eq!(&packet[..n], &[2, 3, 4]); - - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 3, "n must be 3"); - assert_eq!(&packet[..n], &[5, 6, 7]); - - // Write once prior to close. - let n = assert_ok!(buffer.write(&[3]).await); - assert_eq!(n, 1, "n must be 1"); - - // Close - buffer.close().await; - - // Future writes will error - let result = buffer.write(&[4]).await; - assert!(result.is_err()); - - // But we can read the remaining data. - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 1, "n must be 1"); - assert_eq!(&packet[..n], &[3]); - - // Until EOF - let result = buffer.read(&mut packet, None).await; - assert!(result.is_err()); - assert_eq!(Error::ErrBufferClosed, result.unwrap_err()); -} - -async fn test_wraparound(grow: bool) { - let buffer = Buffer::new(0, 0); - { - let mut b = buffer.buffer.lock().await; - let result = b.grow(); - assert!(result.is_ok()); - - b.head = b.data.len() - 13; - b.tail = b.head; - } - - let p1 = vec![1, 2, 3]; - let p2 = vec![4, 5, 6]; - let p3 = vec![7, 8, 9]; - let p4 = vec![10, 11, 12]; - - assert_ok!(buffer.write(&p1).await); - assert_ok!(buffer.write(&p2).await); - assert_ok!(buffer.write(&p3).await); - - let mut p = vec![0; 10]; - - let n = assert_ok!(buffer.read(&mut p, None).await); - assert_eq!(&p1[..], &p[..n]); - - if grow { - let mut b = buffer.buffer.lock().await; - let result = b.grow(); - assert!(result.is_ok()); - } - - let n = assert_ok!(buffer.read(&mut p, None).await); - assert_eq!(&p2[..], &p[..n]); - - assert_ok!(buffer.write(&p4).await); - - let n = assert_ok!(buffer.read(&mut p, None).await); - assert_eq!(&p3[..], &p[..n]); - let n = assert_ok!(buffer.read(&mut p, None).await); - assert_eq!(&p4[..], &p[..n]); - - { - let b = buffer.buffer.lock().await; - if !grow { - assert_eq!(b.data.len(), MIN_SIZE); - } else { - assert_eq!(b.data.len(), 2 * MIN_SIZE); - } - } -} - -#[tokio::test] -async fn test_buffer_wraparound() { - test_wraparound(false).await; -} - -#[tokio::test] -async fn test_buffer_wraparound_grow() { - test_wraparound(true).await; -} - -#[tokio::test] -async fn test_buffer_async() { - let buffer = Buffer::new(0, 0); - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - - let buffer2 = buffer.clone(); - tokio::spawn(async move { - let mut packet: Vec = vec![0; 4]; - - let n = assert_ok!(buffer2.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[0, 1]); - - let result = buffer2.read(&mut packet, None).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), Error::ErrBufferClosed); - - drop(done_tx); - }); - - // Wait for the reader to start reading. - sleep(Duration::from_micros(1)).await; - - // Write once - let n = assert_ok!(buffer.write(&[0, 1]).await); - assert_eq!(n, 2, "n must be 2"); - - // Wait for the reader to start reading again. - sleep(Duration::from_micros(1)).await; - - // Close will unblock the reader. - buffer.close().await; - - done_rx.recv().await; -} - -#[tokio::test] -async fn test_buffer_limit_count() { - let buffer = Buffer::new(2, 0); - - assert_eq!(buffer.count().await, 0); - - // Write twice - let n = assert_ok!(buffer.write(&[0, 1]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.count().await, 1); - - let n = assert_ok!(buffer.write(&[2, 3]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.count().await, 2); - - // Over capacity - let result = buffer.write(&[4, 5]).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBufferFull); - } - assert_eq!(buffer.count().await, 2); - - // Read once - let mut packet: Vec = vec![0; 4]; - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[0, 1]); - assert_eq!(buffer.count().await, 1); - - // Write once - let n = assert_ok!(buffer.write(&[6, 7]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.count().await, 2); - - // Over capacity - let result = buffer.write(&[8, 9]).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrBufferFull, err); - } - assert_eq!(buffer.count().await, 2); - - // Read twice - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[2, 3]); - assert_eq!(buffer.count().await, 1); - - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[6, 7]); - assert_eq!(buffer.count().await, 0); - - // Nothing left. - buffer.close().await; -} - -#[tokio::test] -async fn test_buffer_limit_size() { - let buffer = Buffer::new(0, 11); - - assert_eq!(buffer.size().await, 0); - - // Write twice - let n = assert_ok!(buffer.write(&[0, 1]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.size().await, 4); - - let n = assert_ok!(buffer.write(&[2, 3]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.size().await, 8); - - // Over capacity - let result = buffer.write(&[4, 5]).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrBufferFull, err); - } - assert_eq!(buffer.size().await, 8); - - // Cheeky write at exact size. - let n = assert_ok!(buffer.write(&[6]).await); - assert_eq!(n, 1, "n must be 1"); - assert_eq!(buffer.size().await, 11); - - // Read once - let mut packet: Vec = vec![0; 4]; - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[0, 1]); - assert_eq!(buffer.size().await, 7); - - // Write once - let n = assert_ok!(buffer.write(&[7, 8]).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(buffer.size().await, 11); - - // Over capacity - let result = buffer.write(&[9, 10]).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(Error::ErrBufferFull, err); - } - assert_eq!(buffer.size().await, 11); - - // Read everything - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[2, 3]); - assert_eq!(buffer.size().await, 7); - - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 1, "n must be 1"); - assert_eq!(&packet[..n], &[6]); - assert_eq!(buffer.size().await, 4); - - let n = assert_ok!(buffer.read(&mut packet, None).await); - assert_eq!(n, 2, "n must be 2"); - assert_eq!(&packet[..n], &[7, 8]); - assert_eq!(buffer.size().await, 0); - - // Nothing left. - buffer.close().await; -} - -#[tokio::test] -async fn test_buffer_limit_sizes() { - let sizes = vec![ - 128 * 1024, - 1024 * 1024, - 8 * 1024 * 1024, - 0, // default - ]; - const HEADER_SIZE: usize = 2; - const PACKET_SIZE: usize = 0x8000; - - for mut size in sizes { - let mut name = "default".to_owned(); - if size > 0 { - name = format!("{}kbytes", size / 1024); - } - - let buffer = Buffer::new(0, 0); - if size == 0 { - size = MAX_SIZE; - } else { - buffer.set_limit_size(size + HEADER_SIZE).await; - } - - //assert.NoError(buffer.SetReadDeadline(now.Add(5 * time.Second))) // Set deadline to avoid test deadlock - - let n_packets = size / (PACKET_SIZE + HEADER_SIZE); - let pkt = vec![0; PACKET_SIZE]; - for _ in 0..n_packets { - assert_ok!(buffer.write(&pkt).await); - } - - // Next write is expected to be errored. - let result = buffer.write(&pkt).await; - assert!(result.is_err(), "{}", name); - assert_eq!(result.unwrap_err(), Error::ErrBufferFull, "{name}"); - - let mut packet = vec![0; size]; - for _ in 0..n_packets { - let n = assert_ok!(buffer.read(&mut packet, Some(Duration::new(5, 0))).await); - assert_eq!(n, PACKET_SIZE, "{name}"); - } - } -} - -#[tokio::test] -async fn test_buffer_misc() { - let buffer = Buffer::new(0, 0); - - // Write once - let n = assert_ok!(buffer.write(&[0, 1, 2, 3]).await); - assert_eq!(n, 4, "n must be 4"); - - // Try to read with a short buffer - let mut packet: Vec = vec![0; 3]; - let result = buffer.read(&mut packet, None).await; - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err, Error::ErrBufferShort); - } - - // Close - buffer.close().await; - - // check is_close - assert!(buffer.is_closed().await); - - // Make sure you can Close twice - buffer.close().await; -} diff --git a/util/src/buffer/mod.rs b/util/src/buffer/mod.rs deleted file mode 100644 index a39a3e9ec..000000000 --- a/util/src/buffer/mod.rs +++ /dev/null @@ -1,322 +0,0 @@ -#[cfg(test)] -mod buffer_test; - -use std::sync::Arc; - -use tokio::sync::{Mutex, Notify}; -use tokio::time::{timeout, Duration}; - -use crate::error::{Error, Result}; - -const MIN_SIZE: usize = 2048; -const CUTOFF_SIZE: usize = 128 * 1024; -const MAX_SIZE: usize = 4 * 1024 * 1024; - -/// Buffer allows writing packets to an intermediate buffer, which can then be read form. -/// This is verify similar to bytes.Buffer but avoids combining multiple writes into a single read. -#[derive(Debug)] -struct BufferInternal { - data: Vec, - head: usize, - tail: usize, - - closed: bool, - subs: bool, - - count: usize, - limit_count: usize, - limit_size: usize, -} - -impl BufferInternal { - /// available returns true if the buffer is large enough to fit a packet - /// of the given size, taking overhead into account. - fn available(&self, size: usize) -> bool { - let mut available = self.head as isize - self.tail as isize; - if available <= 0 { - available += self.data.len() as isize; - } - // we interpret head=tail as empty, so always keep a byte free - size as isize + 2 < available - } - - /// grow increases the size of the buffer. If it returns nil, then the - /// buffer has been grown. It returns ErrFull if hits a limit. - fn grow(&mut self) -> Result<()> { - let mut newsize = if self.data.len() < CUTOFF_SIZE { - 2 * self.data.len() - } else { - 5 * self.data.len() / 4 - }; - - if newsize < MIN_SIZE { - newsize = MIN_SIZE - } - if (self.limit_size == 0/*|| sizeHardlimit*/) && newsize > MAX_SIZE { - newsize = MAX_SIZE - } - - // one byte slack - if self.limit_size > 0 && newsize > self.limit_size + 1 { - newsize = self.limit_size + 1 - } - - if newsize <= self.data.len() { - return Err(Error::ErrBufferFull); - } - - let mut newdata: Vec = vec![0; newsize]; - - let mut n; - if self.head <= self.tail { - // data was contiguous - n = self.tail - self.head; - newdata[..n].copy_from_slice(&self.data[self.head..self.tail]); - } else { - // data was discontiguous - n = self.data.len() - self.head; - newdata[..n].copy_from_slice(&self.data[self.head..]); - newdata[n..n + self.tail].copy_from_slice(&self.data[..self.tail]); - n += self.tail; - } - self.head = 0; - self.tail = n; - self.data = newdata; - - Ok(()) - } - - fn size(&self) -> usize { - let mut size = self.tail as isize - self.head as isize; - if size < 0 { - size += self.data.len() as isize; - } - size as usize - } -} - -#[derive(Debug, Clone)] -pub struct Buffer { - buffer: Arc>, - notify: Arc, -} - -impl Buffer { - pub fn new(limit_count: usize, limit_size: usize) -> Self { - Buffer { - buffer: Arc::new(Mutex::new(BufferInternal { - data: vec![], - head: 0, - tail: 0, - - closed: false, - subs: false, - - count: 0, - limit_count, - limit_size, - })), - notify: Arc::new(Notify::new()), - } - } - - /// Write appends a copy of the packet data to the buffer. - /// Returns ErrFull if the packet doesn't fit. - /// Note that the packet size is limited to 65536 bytes since v0.11.0 - /// due to the internal data structure. - pub async fn write(&self, packet: &[u8]) -> Result { - if packet.len() >= 0x10000 { - return Err(Error::ErrPacketTooBig); - } - - let mut b = self.buffer.lock().await; - - if b.closed { - return Err(Error::ErrBufferClosed); - } - - if (b.limit_count > 0 && b.count >= b.limit_count) - || (b.limit_size > 0 && b.size() + 2 + packet.len() > b.limit_size) - { - return Err(Error::ErrBufferFull); - } - - // grow the buffer until the packet fits - while !b.available(packet.len()) { - b.grow()?; - } - - // store the length of the packet - let tail = b.tail; - b.data[tail] = (packet.len() >> 8) as u8; - b.tail += 1; - if b.tail >= b.data.len() { - b.tail = 0; - } - - let tail = b.tail; - b.data[tail] = packet.len() as u8; - b.tail += 1; - if b.tail >= b.data.len() { - b.tail = 0; - } - - // store the packet - let end = std::cmp::min(b.data.len(), b.tail + packet.len()); - let n = end - b.tail; - let tail = b.tail; - b.data[tail..end].copy_from_slice(&packet[..n]); - b.tail += n; - if b.tail >= b.data.len() { - // we reached the end, wrap around - let m = packet.len() - n; - b.data[..m].copy_from_slice(&packet[n..]); - b.tail = m; - } - b.count += 1; - - if b.subs { - // we have other are waiting data - self.notify.notify_one(); - b.subs = false; - } - - Ok(packet.len()) - } - - // Read populates the given byte slice, returning the number of bytes read. - // Blocks until data is available or the buffer is closed. - // Returns io.ErrShortBuffer is the packet is too small to copy the Write. - // Returns io.EOF if the buffer is closed. - pub async fn read(&self, packet: &mut [u8], duration: Option) -> Result { - loop { - { - // use {} to let LockGuard RAII - let mut b = self.buffer.lock().await; - - if b.head != b.tail { - // decode the packet size - let n1 = b.data[b.head]; - b.head += 1; - if b.head >= b.data.len() { - b.head = 0; - } - let n2 = b.data[b.head]; - b.head += 1; - if b.head >= b.data.len() { - b.head = 0; - } - let count = ((n1 as usize) << 8) | n2 as usize; - - // determine the number of bytes we'll actually copy - let mut copied = count; - if copied > packet.len() { - copied = packet.len(); - } - - // copy the data - if b.head + copied < b.data.len() { - packet[..copied].copy_from_slice(&b.data[b.head..b.head + copied]); - } else { - let k = b.data.len() - b.head; - packet[..k].copy_from_slice(&b.data[b.head..]); - packet[k..copied].copy_from_slice(&b.data[..copied - k]); - } - - // advance head, discarding any data that wasn't copied - b.head += count; - if b.head >= b.data.len() { - b.head -= b.data.len(); - } - - if b.head == b.tail { - // the buffer is empty, reset to beginning - // in order to improve cache locality. - b.head = 0; - b.tail = 0; - } - - b.count -= 1; - - if copied < count { - return Err(Error::ErrBufferShort); - } - return Ok(copied); - } else { - // Dont have data -> need wait - b.subs = true; - } - - if b.closed { - return Err(Error::ErrBufferClosed); - } - } - - // Wait for signal. - if let Some(d) = duration { - if timeout(d, self.notify.notified()).await.is_err() { - return Err(Error::ErrTimeout); - } - } else { - self.notify.notified().await; - } - } - } - - // Close will unblock any readers and prevent future writes. - // Data in the buffer can still be read, returning io.EOF when fully depleted. - pub async fn close(&self) { - // note: We don't use defer so we can close the notify channel after unlocking. - // This will unblock goroutines that can grab the lock immediately, instead of blocking again. - let mut b = self.buffer.lock().await; - - if b.closed { - return; - } - - b.closed = true; - self.notify.notify_waiters(); - } - - pub async fn is_closed(&self) -> bool { - let b = self.buffer.lock().await; - - b.closed - } - - // Count returns the number of packets in the buffer. - pub async fn count(&self) -> usize { - let b = self.buffer.lock().await; - - b.count - } - - // set_limit_count controls the maximum number of packets that can be buffered. - // Causes Write to return ErrFull when this limit is reached. - // A zero value will disable this limit. - pub async fn set_limit_count(&self, limit: usize) { - let mut b = self.buffer.lock().await; - - b.limit_count = limit - } - - // Size returns the total byte size of packets in the buffer. - pub async fn size(&self) -> usize { - let b = self.buffer.lock().await; - - b.size() - } - - // set_limit_size controls the maximum number of bytes that can be buffered. - // Causes Write to return ErrFull when this limit is reached. - // A zero value means 4MB since v0.11.0. - // - // User can set packetioSizeHardlimit build tag to enable 4MB hardlimit. - // When packetioSizeHardlimit build tag is set, set_limit_size exceeding - // the hardlimit will be silently discarded. - pub async fn set_limit_size(&self, limit: usize) { - let mut b = self.buffer.lock().await; - - b.limit_size = limit - } -} diff --git a/util/src/conn/conn_bridge.rs b/util/src/conn/conn_bridge.rs deleted file mode 100644 index a16ed8931..000000000 --- a/util/src/conn/conn_bridge.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::collections::VecDeque; -use std::io::{Error, ErrorKind}; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use bytes::Bytes; -use portable_atomic::AtomicUsize; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; - -use super::*; - -const TICK_WAIT: Duration = Duration::from_micros(10); - -/// BridgeConn is a Conn that represents an endpoint of the bridge. -struct BridgeConn { - br: Arc, - id: usize, - rd_rx: Mutex>, - loss_chance: u8, -} - -#[async_trait] -impl Conn for BridgeConn { - async fn connect(&self, _addr: SocketAddr) -> Result<()> { - Err(Error::new(ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, b: &mut [u8]) -> Result { - let mut rd_rx = self.rd_rx.lock().await; - let v = match rd_rx.recv().await { - Some(v) => v, - None => return Err(Error::new(ErrorKind::UnexpectedEof, "Unexpected EOF").into()), - }; - let l = std::cmp::min(v.len(), b.len()); - b[..l].copy_from_slice(&v[..l]); - Ok(l) - } - - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - let n = self.recv(buf).await?; - Ok((n, SocketAddr::from_str("0.0.0.0:0")?)) - } - - async fn send(&self, b: &[u8]) -> Result { - if rand::random::() % 100 < self.loss_chance { - return Ok(b.len()); - } - - self.br.push(b, self.id).await - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Err(Error::new(ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> Result { - Err(Error::new(ErrorKind::AddrNotAvailable, "Addr Not Available").into()) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -pub type FilterCbFn = Box bool + Send + Sync>; - -/// Bridge represents a network between the two endpoints. -#[derive(Default)] -pub struct Bridge { - drop_nwrites: [AtomicUsize; 2], - reorder_nwrites: [AtomicUsize; 2], - - stack: [Mutex>; 2], - queue: [Mutex>; 2], - - wr_tx: [Option>; 2], - filter_cb: [Option; 2], -} - -impl Bridge { - pub fn new( - loss_chance: u8, - filter_cb0: Option, - filter_cb1: Option, - ) -> (Arc, impl Conn, impl Conn) { - let (wr_tx0, rd_rx0) = mpsc::channel(1024); - let (wr_tx1, rd_rx1) = mpsc::channel(1024); - - let br = Arc::new(Bridge { - wr_tx: [Some(wr_tx0), Some(wr_tx1)], - filter_cb: [filter_cb0, filter_cb1], - ..Default::default() - }); - let conn0 = BridgeConn { - br: Arc::clone(&br), - id: 0, - rd_rx: Mutex::new(rd_rx0), - loss_chance, - }; - let conn1 = BridgeConn { - br: Arc::clone(&br), - id: 1, - rd_rx: Mutex::new(rd_rx1), - loss_chance, - }; - - (br, conn0, conn1) - } - - /// Len returns number of queued packets. - #[allow(clippy::len_without_is_empty)] - pub async fn len(&self, id: usize) -> usize { - let q = self.queue[id].lock().await; - q.len() - } - - pub async fn push(&self, b: &[u8], id: usize) -> Result { - // Push rate should be limited as same as Tick rate. - // Otherwise, queue grows too fast on free running Write. - tokio::time::sleep(TICK_WAIT).await; - - let d = Bytes::from(b.to_vec()); - if self.drop_nwrites[id].load(Ordering::SeqCst) > 0 { - self.drop_nwrites[id].fetch_sub(1, Ordering::SeqCst); - } else if self.reorder_nwrites[id].load(Ordering::SeqCst) > 0 { - let mut stack = self.stack[id].lock().await; - stack.push_back(d); - if self.reorder_nwrites[id].fetch_sub(1, Ordering::SeqCst) == 1 { - let ok = inverse(&mut stack); - if ok { - let mut queue = self.queue[id].lock().await; - queue.append(&mut stack); - } - } - } else if let Some(filter_cb) = &self.filter_cb[id] { - if filter_cb(&d) { - let mut queue = self.queue[id].lock().await; - queue.push_back(d); - } - } else { - //log::debug!("queue [{}] enter lock", id); - let mut queue = self.queue[id].lock().await; - queue.push_back(d); - //log::debug!("queue [{}] exit lock", id); - } - - Ok(b.len()) - } - - /// Reorder inverses the order of packets currently in the specified queue. - pub async fn reorder(&self, id: usize) -> bool { - let mut queue = self.queue[id].lock().await; - inverse(&mut queue) - } - - /// Drop drops the specified number of packets from the given offset index - /// of the specified queue. - pub async fn drop_offset(&self, id: usize, offset: usize, n: usize) { - let mut queue = self.queue[id].lock().await; - queue.drain(offset..offset + n); - } - - /// drop_next_nwrites drops the next n packets that will be written - /// to the specified queue. - pub fn drop_next_nwrites(&self, id: usize, n: usize) { - self.drop_nwrites[id].store(n, Ordering::SeqCst); - } - - /// reorder_next_nwrites drops the next n packets that will be written - /// to the specified queue. - pub fn reorder_next_nwrites(&self, id: usize, n: usize) { - self.reorder_nwrites[id].store(n, Ordering::SeqCst); - } - - pub async fn clear(&self) { - for id in 0..2 { - let mut queue = self.queue[id].lock().await; - queue.clear(); - } - } - - /// Tick attempts to hand a packet from the queue for each directions, to readers, - /// if there are waiting on the queue. If there's no reader, it will return - /// immediately. - pub async fn tick(&self) -> usize { - let mut n = 0; - - for id in 0..2 { - let mut queue = self.queue[id].lock().await; - if let Some(d) = queue.pop_front() { - n += 1; - if let Some(wr_tx) = &self.wr_tx[1 - id] { - let _ = wr_tx.send(d).await; - } - } - } - - n - } - - /// Process repeats tick() calls until no more outstanding packet in the queues. - pub async fn process(&self) { - loop { - tokio::time::sleep(TICK_WAIT).await; - self.tick().await; - if self.len(0).await == 0 && self.len(1).await == 0 { - break; - } - } - } -} - -pub(crate) fn inverse(s: &mut VecDeque) -> bool { - if s.len() < 2 { - return false; - } - - let (mut i, mut j) = (0, s.len() - 1); - while i < j { - s.swap(i, j); - i += 1; - j -= 1; - } - - true -} diff --git a/util/src/conn/conn_bridge_test.rs b/util/src/conn/conn_bridge_test.rs deleted file mode 100644 index d775a169d..000000000 --- a/util/src/conn/conn_bridge_test.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::collections::VecDeque; -use std::sync::Arc; - -use bytes::Bytes; -use tokio::sync::mpsc; - -use super::conn_bridge::*; -use super::*; - -static MSG1: Bytes = Bytes::from_static(b"ADC"); -static MSG2: Bytes = Bytes::from_static(b"DEFG"); - -#[tokio::test] -async fn test_bridge_normal() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn0.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn1.recv(&mut buf).await?; - let _ = tx.send(n).await; - Result::<()>::Ok(()) - }); - - br.process().await; - - let n = rx.recv().await.unwrap(); - assert_eq!(n, MSG1.len(), "unexpected length"); - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_drop_first_packet_from_conn0() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn0.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn0.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn1.recv(&mut buf).await?; - let _ = tx.send(n).await; - Result::<()>::Ok(()) - }); - - br.drop_offset(0, 0, 1).await; - br.process().await; - - let n = rx.recv().await.unwrap(); - assert_eq!(n, MSG2.len(), "unexpected length"); - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_drop_second_packet_from_conn0() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn0.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn0.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn1.recv(&mut buf).await?; - let _ = tx.send(n).await; - Result::<()>::Ok(()) - }); - - br.drop_offset(0, 1, 1).await; - br.process().await; - - let n = rx.recv().await.unwrap(); - assert_eq!(n, MSG1.len(), "unexpected length"); - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_drop_first_packet_from_conn1() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn1.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn1.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn0.recv(&mut buf).await?; - let _ = tx.send(n).await; - Result::<()>::Ok(()) - }); - - br.drop_offset(1, 0, 1).await; - br.process().await; - - let n = rx.recv().await.unwrap(); - assert_eq!(n, MSG2.len(), "unexpected length"); - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_drop_second_packet_from_conn1() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn1.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn1.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn0.recv(&mut buf).await?; - let _ = tx.send(n).await; - Result::<()>::Ok(()) - }); - - br.drop_offset(1, 1, 1).await; - br.process().await; - - let n = rx.recv().await.unwrap(); - assert_eq!(n, MSG1.len(), "unexpected length"); - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_reorder_packets_from_conn0() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn0.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn0.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn1.recv(&mut buf).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - let n = conn1.recv(&mut buf).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - - let _ = rx.recv().await; - - Result::<()>::Ok(()) - }); - - br.reorder(0).await; - br.process().await; - - let _ = tx.send(()).await; - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_reorder_packets_from_conn1() -> Result<()> { - let (br, conn0, conn1) = Bridge::new(0, None, None); - - let n = conn1.send(&MSG1).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - let n = conn1.send(&MSG2).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - - let (tx, mut rx) = mpsc::channel(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - let n = conn0.recv(&mut buf).await?; - assert_eq!(n, MSG2.len(), "unexpected length"); - let n = conn0.recv(&mut buf).await?; - assert_eq!(n, MSG1.len(), "unexpected length"); - - let _ = rx.recv().await; - - Result::<()>::Ok(()) - }); - - br.reorder(1).await; - br.process().await; - - let _ = tx.send(()).await; - - Ok(()) -} - -#[tokio::test] -async fn test_bridge_inverse_error() -> Result<()> { - let mut q = VecDeque::new(); - q.push_back(MSG1.clone()); - assert!(!inverse(&mut q)); - Ok(()) -} - -#[tokio::test] -async fn test_bridge_drop_next_n_packets() -> Result<()> { - for id in 0..2 { - let (br, conn0, conn1) = Bridge::new(0, None, None); - br.drop_next_nwrites(id, 3); - let conns: Vec> = vec![Arc::new(conn0), Arc::new(conn1)]; - let src_conn = Arc::clone(&conns[id]); - let dst_conn = Arc::clone(&conns[1 - id]); - - let (tx, mut rx) = mpsc::channel(5); - - tokio::spawn(async move { - let mut buf = vec![0u8; 256]; - for _ in 0..2u8 { - let n = dst_conn.recv(&mut buf).await?; - let _ = tx.send(buf[..n].to_vec()).await; - } - - Result::<()>::Ok(()) - }); - - let mut msgs = vec![]; - for i in 0..5u8 { - let msg = format!("msg{i}"); - let n = src_conn.send(msg.as_bytes()).await?; - assert_eq!(n, msg.len(), "[{id}] unexpected length"); - msgs.push(msg); - br.process().await; - } - - for i in 0..2 { - if let Some(buf) = rx.recv().await { - assert_eq!(msgs[i + 3].as_bytes(), &buf); - } else { - panic!("{id} unexpected number of packets"); - } - } - } - - Ok(()) -} diff --git a/util/src/conn/conn_disconnected_packet.rs b/util/src/conn/conn_disconnected_packet.rs deleted file mode 100644 index ec4230304..000000000 --- a/util/src/conn/conn_disconnected_packet.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::net::Ipv4Addr; -use std::sync::Arc; - -use super::*; -use crate::sync::RwLock; - -/// Since UDP is connectionless, as a server, it doesn't know how to reply -/// simply using the `Write` method. So, to make it work, `disconnectedPacketConn` -/// will infer the last packet that it reads as the reply address for `Write` -pub struct DisconnectedPacketConn { - raddr: RwLock, - pconn: Arc, -} - -impl DisconnectedPacketConn { - pub fn new(conn: Arc) -> Self { - DisconnectedPacketConn { - raddr: RwLock::new(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0)), - pconn: conn, - } - } -} - -#[async_trait] -impl Conn for DisconnectedPacketConn { - async fn connect(&self, addr: SocketAddr) -> Result<()> { - self.pconn.connect(addr).await - } - - async fn recv(&self, buf: &mut [u8]) -> Result { - let (n, addr) = self.pconn.recv_from(buf).await?; - *self.raddr.write() = addr; - Ok(n) - } - - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - self.pconn.recv_from(buf).await - } - - async fn send(&self, buf: &[u8]) -> Result { - let addr = *self.raddr.read(); - self.pconn.send_to(buf, addr).await - } - - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> Result { - self.pconn.send_to(buf, target).await - } - - fn local_addr(&self) -> Result { - self.pconn.local_addr() - } - - fn remote_addr(&self) -> Option { - let raddr = *self.raddr.read(); - Some(raddr) - } - - async fn close(&self) -> Result<()> { - self.pconn.close().await - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/util/src/conn/conn_pipe.rs b/util/src/conn/conn_pipe.rs deleted file mode 100644 index 3427a9100..000000000 --- a/util/src/conn/conn_pipe.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::io::{Error, ErrorKind}; -use std::str::FromStr; - -use tokio::sync::{mpsc, Mutex}; - -use super::*; - -struct Pipe { - rd_rx: Mutex>>, - wr_tx: Mutex>>, -} - -pub fn pipe() -> (impl Conn, impl Conn) { - let (cb1_tx, cb1_rx) = mpsc::channel(16); - let (cb2_tx, cb2_rx) = mpsc::channel(16); - - let p1 = Pipe { - rd_rx: Mutex::new(cb1_rx), - wr_tx: Mutex::new(cb2_tx), - }; - - let p2 = Pipe { - rd_rx: Mutex::new(cb2_rx), - wr_tx: Mutex::new(cb1_tx), - }; - - (p1, p2) -} - -#[async_trait] -impl Conn for Pipe { - async fn connect(&self, _addr: SocketAddr) -> Result<()> { - Err(Error::new(ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, b: &mut [u8]) -> Result { - let mut rd_rx = self.rd_rx.lock().await; - let v = match rd_rx.recv().await { - Some(v) => v, - None => return Err(Error::new(ErrorKind::UnexpectedEof, "Unexpected EOF").into()), - }; - let l = std::cmp::min(v.len(), b.len()); - b[..l].copy_from_slice(&v[..l]); - Ok(l) - } - - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - let n = self.recv(buf).await?; - Ok((n, SocketAddr::from_str("0.0.0.0:0")?)) - } - - async fn send(&self, b: &[u8]) -> Result { - let wr_tx = self.wr_tx.lock().await; - match wr_tx.send(b.to_vec()).await { - Ok(_) => {} - Err(err) => return Err(Error::new(ErrorKind::Other, err.to_string()).into()), - }; - Ok(b.len()) - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Err(Error::new(ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> Result { - Err(Error::new(ErrorKind::AddrNotAvailable, "Addr Not Available").into()) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/util/src/conn/conn_pipe_test.rs b/util/src/conn/conn_pipe_test.rs deleted file mode 100644 index 6fc896070..000000000 --- a/util/src/conn/conn_pipe_test.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::conn_pipe::*; -use super::*; - -#[tokio::test] -async fn test_pipe() -> Result<()> { - let (c1, c2) = pipe(); - let mut b1 = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let n = c1.send(&b1).await?; - assert_eq!(n, 10); - - let mut b2 = vec![133; 100]; - let n = c2.recv(&mut b2).await?; - assert_eq!(n, 10); - assert_eq!(&b2[..n], &b1[..]); - - let n = c2.send(&b2[..10]).await?; - assert_eq!(n, 10); - let n = c2.send(&b2[..5]).await?; - assert_eq!(n, 5); - - let n = c1.recv(&mut b1).await?; - assert_eq!(n, 10); - let n = c1.recv(&mut b1).await?; - assert_eq!(n, 5); - - Ok(()) -} diff --git a/util/src/conn/conn_test.rs b/util/src/conn/conn_test.rs deleted file mode 100644 index 1e82688f8..000000000 --- a/util/src/conn/conn_test.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::*; - -#[tokio::test] -async fn test_conn_lookup_host() -> Result<()> { - let stun_serv_addr = "stun1.l.google.com:19302"; - - if let Ok(ipv4_addr) = lookup_host(true, stun_serv_addr).await { - assert!( - ipv4_addr.is_ipv4(), - "expected ipv4 but got ipv6: {ipv4_addr}" - ); - } - - if let Ok(ipv6_addr) = lookup_host(false, stun_serv_addr).await { - assert!( - ipv6_addr.is_ipv6(), - "expected ipv6 but got ipv4: {ipv6_addr}" - ); - } - - Ok(()) -} diff --git a/util/src/conn/conn_udp.rs b/util/src/conn/conn_udp.rs deleted file mode 100644 index a6fa8d542..000000000 --- a/util/src/conn/conn_udp.rs +++ /dev/null @@ -1,42 +0,0 @@ -use tokio::net::UdpSocket; - -use super::*; - -#[async_trait] -impl Conn for UdpSocket { - async fn connect(&self, addr: SocketAddr) -> Result<()> { - Ok(self.connect(addr).await?) - } - - async fn recv(&self, buf: &mut [u8]) -> Result { - Ok(self.recv(buf).await?) - } - - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - Ok(self.recv_from(buf).await?) - } - - async fn send(&self, buf: &[u8]) -> Result { - Ok(self.send(buf).await?) - } - - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> Result { - Ok(self.send_to(buf, target).await?) - } - - fn local_addr(&self) -> Result { - Ok(self.local_addr()?) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/util/src/conn/conn_udp_listener.rs b/util/src/conn/conn_udp_listener.rs deleted file mode 100644 index 47dcd5c86..000000000 --- a/util/src/conn/conn_udp_listener.rs +++ /dev/null @@ -1,298 +0,0 @@ -use core::sync::atomic::Ordering; -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; - -use portable_atomic::AtomicBool; -use tokio::net::UdpSocket; -use tokio::sync::{mpsc, watch, Mutex}; - -use super::*; -use crate::error::Error; -use crate::Buffer; - -const RECEIVE_MTU: usize = 8192; -const DEFAULT_LISTEN_BACKLOG: usize = 128; // same as Linux default - -pub type AcceptFilterFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -type AcceptDoneCh = (mpsc::Receiver>, watch::Receiver<()>); - -/// listener is used in the [DTLS](https://github.com/webrtc-rs/dtls) and -/// [SCTP](https://github.com/webrtc-rs/sctp) transport to provide a connection-oriented -/// listener over a UDP. -struct ListenerImpl { - pconn: Arc, - accepting: Arc, - accept_ch_tx: Arc>>>>, - done_ch_tx: Arc>>>, - ch_rx: Arc>, - conns: Arc>>>, -} - -#[async_trait] -impl Listener for ListenerImpl { - /// accept waits for and returns the next connection to the listener. - async fn accept(&self) -> Result<(Arc, SocketAddr)> { - let (accept_ch_rx, done_ch_rx) = &mut *self.ch_rx.lock().await; - - tokio::select! { - c = accept_ch_rx.recv() =>{ - if let Some(c) = c{ - let raddr = c.raddr; - Ok((c, raddr)) - }else{ - Err(Error::ErrClosedListenerAcceptCh) - } - } - _ = done_ch_rx.changed() => Err(Error::ErrClosedListener), - } - } - - /// close closes the listener. - /// Any blocked Accept operations will be unblocked and return errors. - async fn close(&self) -> Result<()> { - if self.accepting.load(Ordering::SeqCst) { - self.accepting.store(false, Ordering::SeqCst); - { - let mut done_ch = self.done_ch_tx.lock().await; - done_ch.take(); - } - { - let mut accept_ch = self.accept_ch_tx.lock().await; - accept_ch.take(); - } - } - - Ok(()) - } - - /// Addr returns the listener's network address. - async fn addr(&self) -> Result { - self.pconn.local_addr() - } -} - -/// ListenConfig stores options for listening to an address. -#[derive(Default)] -pub struct ListenConfig { - /// Backlog defines the maximum length of the queue of pending - /// connections. It is equivalent of the backlog argument of - /// POSIX listen function. - /// If a connection request arrives when the queue is full, - /// the request will be silently discarded, unlike TCP. - /// Set zero to use default value 128 which is same as Linux default. - pub backlog: usize, - - /// AcceptFilter determines whether the new conn should be made for - /// the incoming packet. If not set, any packet creates new conn. - pub accept_filter: Option, -} - -pub async fn listen(laddr: A) -> Result { - ListenConfig::default().listen(laddr).await -} - -impl ListenConfig { - /// Listen creates a new listener based on the ListenConfig. - pub async fn listen(&mut self, laddr: A) -> Result { - if self.backlog == 0 { - self.backlog = DEFAULT_LISTEN_BACKLOG; - } - - let pconn = Arc::new(UdpSocket::bind(laddr).await?); - let (accept_ch_tx, accept_ch_rx) = mpsc::channel(self.backlog); - let (done_ch_tx, done_ch_rx) = watch::channel(()); - - let l = ListenerImpl { - pconn, - accepting: Arc::new(AtomicBool::new(true)), - accept_ch_tx: Arc::new(Mutex::new(Some(accept_ch_tx))), - done_ch_tx: Arc::new(Mutex::new(Some(done_ch_tx))), - ch_rx: Arc::new(Mutex::new((accept_ch_rx, done_ch_rx.clone()))), - conns: Arc::new(Mutex::new(HashMap::new())), - }; - - let pconn = Arc::clone(&l.pconn); - let accepting = Arc::clone(&l.accepting); - let accept_filter = self.accept_filter.take(); - let accept_ch_tx = Arc::clone(&l.accept_ch_tx); - let conns = Arc::clone(&l.conns); - tokio::spawn(async move { - ListenConfig::read_loop( - done_ch_rx, - pconn, - accepting, - accept_filter, - accept_ch_tx, - conns, - ) - .await; - }); - - Ok(l) - } - - /// read_loop has to tasks: - /// 1. Dispatching incoming packets to the correct Conn. - /// It can therefore not be ended until all Conns are closed. - /// 2. Creating a new Conn when receiving from a new remote. - async fn read_loop( - mut done_ch_rx: watch::Receiver<()>, - pconn: Arc, - accepting: Arc, - accept_filter: Option, - accept_ch_tx: Arc>>>>, - conns: Arc>>>, - ) { - let mut buf = vec![0u8; RECEIVE_MTU]; - - loop { - tokio::select! { - _ = done_ch_rx.changed() => { - break; - } - result = pconn.recv_from(&mut buf) => { - match result { - Ok((n, raddr)) => { - let udp_conn = match ListenConfig::get_udp_conn( - &pconn, - &accepting, - &accept_filter, - &accept_ch_tx, - &conns, - raddr, - &buf[..n], - ) - .await - { - Ok(conn) => conn, - Err(_) => continue, - }; - - if let Some(conn) = udp_conn { - let _ = conn.buffer.write(&buf[..n]).await; - } - } - Err(err) => { - log::warn!("ListenConfig pconn.recv_from error: {}", err); - break; - } - }; - } - } - } - } - - async fn get_udp_conn( - pconn: &Arc, - accepting: &Arc, - accept_filter: &Option, - accept_ch_tx: &Arc>>>>, - conns: &Arc>>>, - raddr: SocketAddr, - buf: &[u8], - ) -> Result>> { - { - let m = conns.lock().await; - if let Some(conn) = m.get(raddr.to_string().as_str()) { - return Ok(Some(conn.clone())); - } - } - - if !accepting.load(Ordering::SeqCst) { - return Err(Error::ErrClosedListener); - } - - if let Some(f) = accept_filter { - if !(f(buf).await) { - return Ok(None); - } - } - - let udp_conn = Arc::new(UdpConn::new(Arc::clone(pconn), Arc::clone(conns), raddr)); - { - let accept_ch = accept_ch_tx.lock().await; - if let Some(tx) = &*accept_ch { - if tx.try_send(Arc::clone(&udp_conn)).is_err() { - return Err(Error::ErrListenQueueExceeded); - } - } else { - return Err(Error::ErrClosedListenerAcceptCh); - } - } - - { - let mut m = conns.lock().await; - m.insert(raddr.to_string(), Arc::clone(&udp_conn)); - } - - Ok(Some(udp_conn)) - } -} - -/// UdpConn augments a connection-oriented connection over a UdpSocket -pub struct UdpConn { - pconn: Arc, - conns: Arc>>>, - raddr: SocketAddr, - buffer: Buffer, -} - -impl UdpConn { - fn new( - pconn: Arc, - conns: Arc>>>, - raddr: SocketAddr, - ) -> Self { - UdpConn { - pconn, - conns, - raddr, - buffer: Buffer::new(0, 0), - } - } -} - -#[async_trait] -impl Conn for UdpConn { - async fn connect(&self, addr: SocketAddr) -> Result<()> { - self.pconn.connect(addr).await - } - - async fn recv(&self, buf: &mut [u8]) -> Result { - Ok(self.buffer.read(buf, None).await?) - } - - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - let n = self.buffer.read(buf, None).await?; - Ok((n, self.raddr)) - } - - async fn send(&self, buf: &[u8]) -> Result { - self.pconn.send_to(buf, self.raddr).await - } - - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> Result { - self.pconn.send_to(buf, target).await - } - - fn local_addr(&self) -> Result { - self.pconn.local_addr() - } - - fn remote_addr(&self) -> Option { - Some(self.raddr) - } - - async fn close(&self) -> Result<()> { - let mut conns = self.conns.lock().await; - conns.remove(self.raddr.to_string().as_str()); - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/util/src/conn/conn_udp_listener_test.rs b/util/src/conn/conn_udp_listener_test.rs deleted file mode 100644 index 3ac0a93c0..000000000 --- a/util/src/conn/conn_udp_listener_test.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::future::Future; -use std::pin::Pin; - -use tokio::net::UdpSocket; -use tokio::sync::mpsc; -use tokio::time::Duration; - -use super::conn_udp_listener::*; -use super::*; -use crate::error::{Error, Result}; - -async fn pipe() -> Result<( - Arc, - Arc, - UdpSocket, -)> { - // Start listening - let listener = Arc::new(listen("0.0.0.0:0").await?); - - // Open a connection - let d_conn = UdpSocket::bind("0.0.0.0:0").await?; - d_conn.connect(listener.addr().await?).await?; - - // Write to the connection to initiate it - let handshake = "hello"; - d_conn.send(handshake.as_bytes()).await?; - let daddr = d_conn.local_addr()?; - - // Accept the connection - let (l_conn, raddr) = listener.accept().await?; - assert_eq!(daddr, raddr, "remote address should be match"); - - let raddr = l_conn.remote_addr(); - if let Some(raddr) = raddr { - assert_eq!(daddr, raddr, "remote address should be match"); - } else { - panic!("expected Some, but got None, for remote_addr()"); - } - - let mut buf = vec![0u8; handshake.len()]; - let n = l_conn.recv(&mut buf).await?; - - let result = String::from_utf8(buf[..n].to_vec())?; - if handshake != result { - Err(Error::Other(format!( - "errHandshakeFailed: {handshake} != {result}" - ))) - } else { - Ok((listener, l_conn, d_conn)) - } -} - -#[tokio::test] -async fn test_listener_close_timeout() -> Result<()> { - let (listener, ca, _) = pipe().await?; - - listener.close().await?; - - // Close client after server closes to cleanup - ca.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_listener_close_unaccepted() -> Result<()> { - const BACKLOG: usize = 2; - - let listener = ListenConfig { - backlog: BACKLOG, - ..Default::default() - } - .listen("0.0.0.0:0") - .await?; - - for i in 0..BACKLOG as u8 { - let conn = UdpSocket::bind("0.0.0.0:0").await?; - conn.connect(listener.addr().await?).await?; - conn.send(&[i]).await?; - conn.close().await?; - } - - // Wait all packets being processed by readLoop - tokio::time::sleep(Duration::from_millis(100)).await; - - // Unaccepted connections must be closed by listener.Close() - listener.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_listener_accept_filter() -> Result<()> { - let tests = vec![("CreateConn", &[0xAA], true), ("Discarded", &[0x00], false)]; - - for (name, packet, expected) in tests { - let accept_filter: Option = Some(Box::new( - |pkt: &[u8]| -> Pin + Send + 'static>> { - let p0 = pkt[0]; - Box::pin(async move { p0 == 0xAA }) - }, - )); - - let listener = Arc::new( - ListenConfig { - accept_filter, - ..Default::default() - } - .listen("0.0.0.0:0") - .await?, - ); - - let conn = UdpSocket::bind("0.0.0.0:0").await?; - conn.connect(listener.addr().await?).await?; - conn.send(packet).await?; - - let (ch_accepted_tx, mut ch_accepted_rx) = mpsc::channel::<()>(1); - let mut ch_accepted_tx = Some(ch_accepted_tx); - let listener2 = Arc::clone(&listener); - tokio::spawn(async move { - let (c, _raddr) = match listener2.accept().await { - Ok((c, raddr)) => (c, raddr), - Err(err) => { - assert_eq!(Error::ErrClosedListener, err); - return Result::<()>::Ok(()); - } - }; - - ch_accepted_tx.take(); - c.close().await?; - - Result::<()>::Ok(()) - }); - - let mut accepted = false; - let mut timeout = false; - let timer = tokio::time::sleep(Duration::from_millis(10)); - tokio::pin!(timer); - tokio::select! { - _= ch_accepted_rx.recv()=>{ - accepted = true; - } - _ = timer.as_mut() => { - timeout = true; - } - } - - assert_eq!(accepted, expected, "{name}: unexpected result"); - assert_eq!(!timeout, expected, "{name}: unexpected result"); - - conn.close().await?; - listener.close().await?; - } - Ok(()) -} - -#[tokio::test] -async fn test_listener_concurrent() -> Result<()> { - const BACKLOG: usize = 2; - - let listener = Arc::new( - ListenConfig { - backlog: BACKLOG, - ..Default::default() - } - .listen("0.0.0.0:0") - .await?, - ); - - for i in 0..BACKLOG as u8 + 1 { - let conn = UdpSocket::bind("0.0.0.0:0").await?; - conn.connect(listener.addr().await?).await?; - conn.send(&[i]).await?; - conn.close().await?; - } - - // Wait all packets being processed by readLoop - tokio::time::sleep(Duration::from_millis(100)).await; - - let mut b = vec![0u8; 1]; - for i in 0..BACKLOG as u8 { - let (conn, _raddr) = listener.accept().await?; - let n = conn.recv(&mut b).await?; - assert_eq!( - &b[..n], - &[i], - "Packet from connection {} is wrong, expected: [{}], got: {:?}", - i, - i, - &b[..n] - ); - conn.close().await?; - } - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let mut done_tx = Some(done_tx); - let listener2 = Arc::clone(&listener); - tokio::spawn(async move { - match listener2.accept().await { - Ok((conn, _raddr)) => { - conn.close().await?; - } - Err(err) => { - assert!(Error::ErrClosedListener == err || Error::ErrClosedListenerAcceptCh == err); - } - } - - done_tx.take(); - - Result::<()>::Ok(()) - }); - - tokio::time::sleep(Duration::from_millis(100)).await; - - listener.close().await?; - - let _ = done_rx.recv().await; - - Ok(()) -} diff --git a/util/src/conn/mod.rs b/util/src/conn/mod.rs deleted file mode 100644 index 5f72ff17f..000000000 --- a/util/src/conn/mod.rs +++ /dev/null @@ -1,73 +0,0 @@ -pub mod conn_bridge; -pub mod conn_disconnected_packet; -pub mod conn_pipe; -pub mod conn_udp; -pub mod conn_udp_listener; - -#[cfg(test)] -mod conn_bridge_test; -#[cfg(test)] -mod conn_pipe_test; -#[cfg(test)] -mod conn_test; - -//TODO: remove this conditional test -#[cfg(not(target_os = "windows"))] -#[cfg(test)] -mod conn_udp_listener_test; - -use std::net::SocketAddr; -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::net::ToSocketAddrs; - -use crate::error::Result; - -#[async_trait] -pub trait Conn { - async fn connect(&self, addr: SocketAddr) -> Result<()>; - async fn recv(&self, buf: &mut [u8]) -> Result; - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)>; - async fn send(&self, buf: &[u8]) -> Result; - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> Result; - fn local_addr(&self) -> Result; - fn remote_addr(&self) -> Option; - async fn close(&self) -> Result<()>; - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync); -} - -/// A Listener is a generic network listener for connection-oriented protocols. -/// Multiple connections may invoke methods on a Listener simultaneously. -#[async_trait] -pub trait Listener { - /// accept waits for and returns the next connection to the listener. - async fn accept(&self) -> Result<(Arc, SocketAddr)>; - - /// close closes the listener. - /// Any blocked accept operations will be unblocked and return errors. - async fn close(&self) -> Result<()>; - - /// addr returns the listener's network address. - async fn addr(&self) -> Result; -} - -pub async fn lookup_host(use_ipv4: bool, host: T) -> Result -where - T: ToSocketAddrs, -{ - for remote_addr in tokio::net::lookup_host(host).await? { - if (use_ipv4 && remote_addr.is_ipv4()) || (!use_ipv4 && remote_addr.is_ipv6()) { - return Ok(remote_addr); - } - } - - Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "No available {} IP address found!", - if use_ipv4 { "ipv4" } else { "ipv6" }, - ), - ) - .into()) -} diff --git a/util/src/error.rs b/util/src/error.rs deleted file mode 100644 index 71899d4c1..000000000 --- a/util/src/error.rs +++ /dev/null @@ -1,174 +0,0 @@ -#![allow(dead_code)] - -use std::num::ParseIntError; -use std::string::FromUtf8Error; -use std::{io, net}; - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - #[error("buffer: full")] - ErrBufferFull, - #[error("buffer: closed")] - ErrBufferClosed, - #[error("buffer: short")] - ErrBufferShort, - #[error("packet too big")] - ErrPacketTooBig, - #[error("i/o timeout")] - ErrTimeout, - #[error("udp: listener closed")] - ErrClosedListener, - #[error("udp: listen queue exceeded")] - ErrListenQueueExceeded, - #[error("udp: listener accept ch closed")] - ErrClosedListenerAcceptCh, - #[error("obs cannot be nil")] - ErrObsCannotBeNil, - #[error("se of closed network connection")] - ErrUseClosedNetworkConn, - #[error("addr is not a net.UDPAddr")] - ErrAddrNotUdpAddr, - #[error("something went wrong with locAddr")] - ErrLocAddr, - #[error("already closed")] - ErrAlreadyClosed, - #[error("no remAddr defined")] - ErrNoRemAddr, - #[error("address already in use")] - ErrAddressAlreadyInUse, - #[error("no such UDPConn")] - ErrNoSuchUdpConn, - #[error("cannot remove unspecified IP by the specified IP")] - ErrCannotRemoveUnspecifiedIp, - #[error("no address assigned")] - ErrNoAddressAssigned, - #[error("1:1 NAT requires more than one mapping")] - ErrNatRequiresMapping, - #[error("length mismtach between mappedIPs and localIPs")] - ErrMismatchLengthIp, - #[error("non-udp translation is not supported yet")] - ErrNonUdpTranslationNotSupported, - #[error("no associated local address")] - ErrNoAssociatedLocalAddress, - #[error("no NAT binding found")] - ErrNoNatBindingFound, - #[error("has no permission")] - ErrHasNoPermission, - #[error("host name must not be empty")] - ErrHostnameEmpty, - #[error("failed to parse IP address")] - ErrFailedToParseIpaddr, - #[error("no interface is available")] - ErrNoInterface, - #[error("not found")] - ErrNotFound, - #[error("unexpected network")] - ErrUnexpectedNetwork, - #[error("can't assign requested address")] - ErrCantAssignRequestedAddr, - #[error("unknown network")] - ErrUnknownNetwork, - #[error("no router linked")] - ErrNoRouterLinked, - #[error("invalid port number")] - ErrInvalidPortNumber, - #[error("unexpected type-switch failure")] - ErrUnexpectedTypeSwitchFailure, - #[error("bind failed")] - ErrBindFailed, - #[error("end port is less than the start")] - ErrEndPortLessThanStart, - #[error("port space exhausted")] - ErrPortSpaceExhausted, - #[error("vnet is not enabled")] - ErrVnetDisabled, - #[error("invalid local IP in static_ips")] - ErrInvalidLocalIpInStaticIps, - #[error("mapped in static_ips is beyond subnet")] - ErrLocalIpBeyondStaticIpsSubset, - #[error("all static_ips must have associated local IPs")] - ErrLocalIpNoStaticsIpsAssociated, - #[error("router already started")] - ErrRouterAlreadyStarted, - #[error("router already stopped")] - ErrRouterAlreadyStopped, - #[error("static IP is beyond subnet")] - ErrStaticIpIsBeyondSubnet, - #[error("address space exhausted")] - ErrAddressSpaceExhausted, - #[error("no IP address is assigned for eth0")] - ErrNoIpaddrEth0, - #[error("Invalid mask")] - ErrInvalidMask, - #[error("parse ipnet: {0}")] - ParseIpnet(#[from] ipnet::AddrParseError), - #[error("parse ip: {0}")] - ParseIp(#[from] net::AddrParseError), - #[error("parse int: {0}")] - ParseInt(#[from] ParseIntError), - #[error("{0}")] - Io(#[source] IoError), - #[error("utf8: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("{0}")] - Std(#[source] StdError), - #[error("{0}")] - Other(String), -} - -impl Error { - pub fn from_std(error: T) -> Self - where - T: std::error::Error + Send + Sync + 'static, - { - Error::Std(StdError(Box::new(error))) - } - - pub fn downcast_ref(&self) -> Option<&T> { - if let Error::Std(s) = self { - return s.0.downcast_ref(); - } - - None - } -} - -#[derive(Debug, Error)] -#[error("io error: {0}")] -pub struct IoError(#[from] pub io::Error); - -// Workaround for wanting PartialEq for io::Error. -impl PartialEq for IoError { - fn eq(&self, other: &Self) -> bool { - self.0.kind() == other.0.kind() - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(IoError(e)) - } -} - -/// An escape hatch to preserve stack traces when we don't know the error. -/// -/// This crate exports some traits such as `Conn` and `Listener`. The trait functions -/// produce the local error `util::Error`. However when used in crates higher up the stack, -/// we are forced to handle errors that are local to that crate. For example we use -/// `Listener` the `dtls` crate and it needs to handle `dtls::Error`. -/// -/// By using `util::Error::from_std` we can preserve the underlying error (and stack trace!). -#[derive(Debug, Error)] -#[error("{0}")] -pub struct StdError(pub Box); - -impl PartialEq for StdError { - fn eq(&self, _: &Self) -> bool { - false - } -} diff --git a/util/src/fixed_big_int/fixed_big_int_test.rs b/util/src/fixed_big_int/fixed_big_int_test.rs deleted file mode 100644 index 4bad3c7b3..000000000 --- a/util/src/fixed_big_int/fixed_big_int_test.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::*; - -#[test] -fn test_fixed_big_int_set_bit() { - let mut bi = FixedBigInt::new(224); - - bi.set_bit(0); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000000000000000000000001" - ); - - bi.lsh(1); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000000000000000000000002" - ); - - bi.lsh(0); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000000000000000000000002" - ); - - bi.set_bit(10); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000000000000000000000402" - ); - bi.lsh(20); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000000000000000040200000" - ); - - bi.set_bit(80); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000000100000000000040200000" - ); - bi.lsh(4); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000000000000001000000000000402000000" - ); - - bi.set_bit(130); - assert_eq!( - bi.to_string(), - "0000000000000000000000000000000400000000001000000000000402000000" - ); - bi.lsh(64); - assert_eq!( - bi.to_string(), - "0000000000000004000000000010000000000004020000000000000000000000" - ); - - bi.set_bit(7); - assert_eq!( - bi.to_string(), - "0000000000000004000000000010000000000004020000000000000000000080" - ); - - bi.lsh(129); - assert_eq!( - bi.to_string(), - "0000000004000000000000000000010000000000000000000000000000000000" - ); - - for _ in 0..256 { - bi.lsh(1); - bi.set_bit(0); - } - assert_eq!( - bi.to_string(), - "00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - ); -} diff --git a/util/src/fixed_big_int/mod.rs b/util/src/fixed_big_int/mod.rs deleted file mode 100644 index 400e04851..000000000 --- a/util/src/fixed_big_int/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -#[cfg(test)] -mod fixed_big_int_test; - -use std::fmt; - -// FixedBigInt is the fix-sized multi-word integer. -pub(crate) struct FixedBigInt { - bits: Vec, - n: usize, - msb_mask: u64, -} - -impl fmt::Display for FixedBigInt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = String::new(); - for i in (0..self.bits.len()).rev() { - out += format!("{:016X}", self.bits[i]).as_str(); - } - - write!(f, "{out}") - } -} - -impl FixedBigInt { - pub(crate) fn new(n: usize) -> Self { - let mut chunk_size = (n + 63) / 64; - if chunk_size == 0 { - chunk_size = 1; - } - - FixedBigInt { - bits: vec![0; chunk_size], - n, - msb_mask: if n % 64 == 0 { - u64::MAX - } else { - (1 << (64 - n % 64)) - 1 - }, - } - } - - // lsh is the left shift operation. - pub(crate) fn lsh(&mut self, n: usize) { - if n == 0 { - return; - } - let n_chunk = (n / 64) as isize; - let n_n = n % 64; - - for i in (0..self.bits.len() as isize).rev() { - let mut carry: u64 = 0; - if i - n_chunk >= 0 { - carry = if n_n >= 64 { - 0 - } else { - self.bits[(i - n_chunk) as usize] << n_n - }; - if i - n_chunk > 0 { - carry |= if n_n == 0 { - 0 - } else { - self.bits[(i - n_chunk - 1) as usize] >> (64 - n_n) - }; - } - } - self.bits[i as usize] = if n >= 64 { - carry - } else { - (self.bits[i as usize] << n) | carry - }; - } - - let last = self.bits.len() - 1; - self.bits[last] &= self.msb_mask; - } - - // bit returns i-th bit of the fixedBigInt. - pub(crate) fn bit(&self, i: usize) -> usize { - if i >= self.n { - return 0; - } - let chunk = i / 64; - let pos = i % 64; - usize::from(self.bits[chunk] & (1 << pos) != 0) - } - - // set_bit sets i-th bit to 1. - pub(crate) fn set_bit(&mut self, i: usize) { - if i >= self.n { - return; - } - let chunk = i / 64; - let pos = i % 64; - self.bits[chunk] |= 1 << pos; - } -} diff --git a/util/src/ifaces/ffi/mod.rs b/util/src/ifaces/ffi/mod.rs deleted file mode 100644 index c8323f313..000000000 --- a/util/src/ifaces/ffi/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(target_family = "windows")] -mod windows; -#[cfg(target_family = "windows")] -pub use self::windows::ifaces; - -#[cfg(target_family = "unix")] -mod unix; -#[cfg(target_family = "unix")] -pub use self::unix::ifaces; diff --git a/util/src/ifaces/ffi/unix/mod.rs b/util/src/ifaces/ffi/unix/mod.rs deleted file mode 100644 index 1d834e187..000000000 --- a/util/src/ifaces/ffi/unix/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::ifaces::{Interface, Kind, NextHop}; - -use nix::sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}; -use std::io::Error; -use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6}; - -fn ss_to_netsa(ss: &SockaddrStorage) -> Option { - match ss.family() { - Some(AddressFamily::Inet) => ss.as_sockaddr_in().map(|sin| { - SocketAddr::V4(SocketAddrV4::new( - std::net::Ipv4Addr::from(sin.ip()), - sin.port(), - )) - }), - Some(AddressFamily::Inet6) => ss.as_sockaddr_in6().map(|sin6| { - SocketAddr::V6(SocketAddrV6::new( - sin6.ip(), - sin6.port(), - sin6.flowinfo(), - sin6.scope_id(), - )) - }), - _ => None, - } -} - -/// Query the local system for all interface addresses. -pub fn ifaces() -> Result, Error> { - let mut ret = Vec::new(); - for ifa in nix::ifaddrs::getifaddrs()? { - if let Some(kind) = ifa - .address - .as_ref() - .and_then(SockaddrStorage::family) - .and_then(|af| match af { - AddressFamily::Inet => Some(Kind::Ipv4), - AddressFamily::Inet6 => Some(Kind::Ipv6), - #[cfg(any( - target_os = "android", - target_os = "linux", - target_os = "illumos", - target_os = "fuchsia", - target_os = "solaris" - ))] - AddressFamily::Packet => Some(Kind::Packet), - #[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "ios", - target_os = "macos", - target_os = "illumos", - target_os = "netbsd", - target_os = "openbsd" - ))] - AddressFamily::Link => Some(Kind::Link), - _ => None, - }) - { - let name = ifa.interface_name; - let dst = ifa.destination.as_ref().and_then(ss_to_netsa); - let broadcast = ifa.broadcast.as_ref().and_then(ss_to_netsa); - let hop = dst - .map(NextHop::Destination) - .or(broadcast.map(NextHop::Broadcast)); - let addr = ifa.address.as_ref().and_then(ss_to_netsa); - let mask = ifa.netmask.as_ref().and_then(ss_to_netsa); - - ret.push(Interface { - name, - kind, - addr, - mask, - hop, - }); - } - } - - Ok(ret) -} diff --git a/util/src/ifaces/ffi/windows/mod.rs b/util/src/ifaces/ffi/windows/mod.rs deleted file mode 100644 index 415a73e98..000000000 --- a/util/src/ifaces/ffi/windows/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -use winapi::shared::basetsd::{UINT32, UINT8, ULONG64}; -use winapi::shared::guiddef::GUID; -use winapi::shared::minwindef::{BYTE, DWORD, PULONG, ULONG}; -use winapi::shared::ws2def::SOCKET_ADDRESS; -use winapi::um::winnt::{PCHAR, PVOID, PWCHAR, WCHAR}; - -const MAX_ADAPTER_ADDRESS_LENGTH: usize = 8; -const ZONE_INDICES_LENGTH: usize = 16; -const MAX_DHCPV6_DUID_LENGTH: usize = 130; -const MAX_DNS_SUFFIX_STRING_LENGTH: usize = 256; - -pub const IP_ADAPTER_IPV4_ENABLED: DWORD = 0x0080; -pub const IP_ADAPTER_IPV6_ENABLED: DWORD = 0x0100; - -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; -use std::{io, mem, ptr}; - -use winapi::shared::winerror::{ - ERROR_ADDRESS_NOT_ASSOCIATED, ERROR_BUFFER_OVERFLOW, ERROR_INVALID_PARAMETER, - ERROR_NOT_ENOUGH_MEMORY, ERROR_NO_DATA, ERROR_SUCCESS, -}; -use winapi::shared::ws2def::{AF_INET, AF_INET6, AF_UNSPEC, SOCKADDR_IN}; -use winapi::shared::ws2ipdef::SOCKADDR_IN6; - -const PREALLOC_ADAPTERS_LEN: usize = 15 * 1024; - -use crate::ifaces::{Interface, Kind, NextHop}; - -#[link(name = "iphlpapi")] -extern "system" { - pub fn GetAdaptersAddresses( - family: ULONG, - flags: ULONG, - reserved: PVOID, - addresses: *mut u8, - size: PULONG, - ) -> ULONG; -} - -#[repr(C)] -pub struct IpAdapterAddresses { - pub head: IpAdapterAddressesHead, - pub all: IpAdaptersAddressesAll, - pub xp: IpAdaptersAddressesXp, - pub vista: IpAdaptersAddressesVista, -} - -#[repr(C)] -pub struct IpAdapterAddressesHead { - pub length: ULONG, - if_index: DWORD, -} - -/// All Windows & Later -#[repr(C)] -pub struct IpAdaptersAddressesAll { - pub next: *const IpAdapterAddresses, - pub adapter_name: PCHAR, - pub first_unicast_address: *const IpAdapterUnicastAddress, - first_anycast_address: *const IpAdapterAnycastAddress, - first_multicast_address: *const IpAdapterMulticastAddress, - first_dns_server_address: *const IpAdapterDnsServerAddress, - dns_suffix: PWCHAR, - pub description: PWCHAR, - friendly_name: PWCHAR, - pub physical_address: [BYTE; MAX_ADAPTER_ADDRESS_LENGTH], - pub physical_address_length: DWORD, - pub flags: DWORD, - mtu: DWORD, - pub if_type: DWORD, - oper_status: IfOperStatus, -} - -/// Windows XP & Later -#[repr(C)] -pub struct IpAdaptersAddressesXp { - pub ipv6_if_index: DWORD, - pub zone_indices: [DWORD; ZONE_INDICES_LENGTH], - first_prefix: *const IpAdapterPrefix, -} - -/// Windows Vista & Later -#[repr(C)] -pub struct IpAdaptersAddressesVista { - transmit_link_speed: ULONG64, - receive_link_speed: ULONG64, - first_wins_server_address: *const IpAdapterWinsServerAddress, - first_gateway_address: *const IpAdapterGatewayAddress, - ipv4_metric: ULONG, - ipv6_metric: ULONG, - luid: IfLuid, - dhcpv4_server: SOCKET_ADDRESS, - compartment_id: UINT32, - network_guid: GUID, - connection_type: NetIfConnectionType, - tunnel_type: TunnelType, - dhcpv6_server: SOCKET_ADDRESS, - dhcpv6_client_duid: [BYTE; MAX_DHCPV6_DUID_LENGTH], - dhcpv6_client_duid_length: ULONG, - dhcpv6_iaid: ULONG, - first_dns_suffix: *const IpAdapterDnsSuffix, -} - -#[repr(C)] -pub struct IpAdapterUnicastAddress { - pub length: ULONG, - flags: DWORD, - pub next: *const IpAdapterUnicastAddress, - pub address: SOCKET_ADDRESS, - prefix_origin: IpPrefixOrigin, - suffix_origin: IpSuffixOrigin, - pub dad_state: IpDadState, - valid_lifetime: ULONG, - preferred_lifetime: ULONG, - lease_lifetime: ULONG, - on_link_prefix_length: UINT8, -} - -#[repr(C)] -pub struct IpAdapterAnycastAddress { - length: ULONG, - flags: DWORD, - next: *const IpAdapterAnycastAddress, - address: SOCKET_ADDRESS, -} - -#[repr(C)] -pub struct IpAdapterMulticastAddress { - length: ULONG, - flags: DWORD, - next: *const IpAdapterMulticastAddress, - address: SOCKET_ADDRESS, -} - -#[repr(C)] -pub struct IpAdapterDnsServerAddress { - length: ULONG, - reserved: DWORD, - next: *const IpAdapterDnsServerAddress, - address: SOCKET_ADDRESS, -} - -#[repr(C)] -pub struct IpAdapterPrefix { - length: ULONG, - flags: DWORD, - next: *const IpAdapterPrefix, - address: SOCKET_ADDRESS, - prefix_length: ULONG, -} - -#[repr(C)] -pub struct IpAdapterWinsServerAddress { - length: ULONG, - reserved: DWORD, - next: *const IpAdapterWinsServerAddress, - address: SOCKET_ADDRESS, -} - -#[repr(C)] -pub struct IpAdapterGatewayAddress { - length: ULONG, - reserved: DWORD, - next: *const IpAdapterGatewayAddress, - address: SOCKET_ADDRESS, -} - -#[repr(C)] -pub struct IpAdapterDnsSuffix { - next: *const IpAdapterDnsSuffix, - string: [WCHAR; MAX_DNS_SUFFIX_STRING_LENGTH], -} - -bitflags! { - struct IfLuid: ULONG64 { - const Reserved = 0x0000000000FFFFFF; - const NetLuidIndex = 0x0000FFFFFF000000; - const IfType = 0xFFFF00000000000; - } -} - -#[repr(C)] -pub enum IpPrefixOrigin { - IpPrefixOriginOther = 0, - IpPrefixOriginManual, - IpPrefixOriginWellKnown, - IpPrefixOriginDhcp, - IpPrefixOriginRouterAdvertisement, - IpPrefixOriginUnchanged = 16, -} - -#[repr(C)] -pub enum IpSuffixOrigin { - IpSuffixOriginOther = 0, - IpSuffixOriginManual, - IpSuffixOriginWellKnown, - IpSuffixOriginDhcp, - IpSuffixOriginLinkLayerAddress, - IpSuffixOriginRandom, - IpSuffixOriginUnchanged = 16, -} - -#[derive(PartialEq, Eq)] -#[repr(C)] -pub enum IpDadState { - IpDadStateInvalid = 0, - IpDadStateTentative, - IpDadStateDuplicate, - IpDadStateDeprecated, - IpDadStatePreferred, -} - -#[repr(C)] -pub enum IfOperStatus { - IfOperStatusUp = 1, - IfOperStatusDown = 2, - IfOperStatusTesting = 3, - IfOperStatusUnknown = 4, - IfOperStatusDormant = 5, - IfOperStatusNotPresent = 6, - IfOperStatusLowerLayerDown = 7, -} - -#[repr(C)] -pub enum NetIfConnectionType { - NetIfConnectionDedicated = 1, - NetIfConnectionPassive = 2, - NetIfConnectionDemand = 3, - NetIfConnectionMaximum = 4, -} - -#[repr(C)] -pub enum TunnelType { - TunnelTypeNone = 0, - TunnelTypeOther = 1, - TunnelTypeDirect = 2, - TunnelType6To4 = 11, - TunnelTypeIsatap = 13, - TunnelTypeTeredo = 14, - TunnelTypeIpHttps = 15, -} - -unsafe fn v4_socket_from_adapter(unicast_addr: &IpAdapterUnicastAddress) -> SocketAddrV4 { - let socket_addr = &unicast_addr.address; - - let in_addr: SOCKADDR_IN = mem::transmute(*socket_addr.lpSockaddr); - let sin_addr = in_addr.sin_addr.S_un; - - let v4_addr = Ipv4Addr::new( - *sin_addr.S_addr() as u8, - (*sin_addr.S_addr() >> 8) as u8, - (*sin_addr.S_addr() >> 16) as u8, - (*sin_addr.S_addr() >> 24) as u8, - ); - - SocketAddrV4::new(v4_addr, 0) -} - -unsafe fn v6_socket_from_adapter(unicast_addr: &IpAdapterUnicastAddress) -> SocketAddrV6 { - let socket_addr = &unicast_addr.address; - - let sock_addr6: *const SOCKADDR_IN6 = socket_addr.lpSockaddr as *const SOCKADDR_IN6; - let in6_addr: SOCKADDR_IN6 = *sock_addr6; - - let v6_addr = (*in6_addr.sin6_addr.u.Word()).into(); - - SocketAddrV6::new( - v6_addr, - 0, - in6_addr.sin6_flowinfo, - *in6_addr.u.sin6_scope_id(), - ) -} - -unsafe fn local_ifaces_with_buffer(buffer: &mut Vec) -> io::Result<()> { - let mut length = buffer.capacity() as u32; - - let ret_code = GetAdaptersAddresses( - AF_UNSPEC as u32, - 0, - ptr::null_mut(), - buffer.as_mut_ptr(), - &mut length, - ); - match ret_code { - ERROR_SUCCESS => Ok(()), - ERROR_ADDRESS_NOT_ASSOCIATED => Err(io::Error::new( - io::ErrorKind::AddrNotAvailable, - "An address has not yet been associated with the network endpoint.", - )), - ERROR_BUFFER_OVERFLOW => { - buffer.reserve_exact(length as usize); - - local_ifaces_with_buffer(buffer) - } - ERROR_INVALID_PARAMETER => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "One of the parameters is invalid.", - )), - ERROR_NOT_ENOUGH_MEMORY => Err(io::Error::new( - io::ErrorKind::Other, - "Insufficient memory resources are available to complete the operation.", - )), - ERROR_NO_DATA => Err(io::Error::new( - io::ErrorKind::AddrNotAvailable, - "No addresses were found for the requested parameters.", - )), - _ => Err(io::Error::new( - io::ErrorKind::Other, - "Some Other Error Occurred.", - )), - } -} - -unsafe fn map_adapter_addresses(mut adapter_addr: *const IpAdapterAddresses) -> Vec { - let mut adapter_addresses = Vec::new(); - - while !adapter_addr.is_null() { - let curr_adapter_addr = &*adapter_addr; - - let mut unicast_addr = curr_adapter_addr.all.first_unicast_address; - while !unicast_addr.is_null() { - let curr_unicast_addr = &*unicast_addr; - - // For some reason, some IpDadState::IpDadStateDeprecated addresses are return - // These contain BOGUS interface indices and will cause problesm if used - if curr_unicast_addr.dad_state != IpDadState::IpDadStateDeprecated { - if is_ipv4_enabled(curr_unicast_addr) { - adapter_addresses.push(Interface { - name: "".to_string(), - kind: Kind::Ipv4, - addr: Some(SocketAddr::V4(v4_socket_from_adapter(curr_unicast_addr))), - mask: None, - hop: None, - }); - } else if is_ipv6_enabled(curr_unicast_addr) { - let mut v6_sock = v6_socket_from_adapter(curr_unicast_addr); - // Make sure the scope id is set for ALL interfaces, not just link-local - v6_sock.set_scope_id(curr_adapter_addr.xp.ipv6_if_index); - adapter_addresses.push(Interface { - name: "".to_string(), - kind: Kind::Ipv6, - addr: Some(SocketAddr::V6(v6_sock)), - mask: None, - hop: None, - }); - } - } - - unicast_addr = curr_unicast_addr.next; - } - - adapter_addr = curr_adapter_addr.all.next; - } - - adapter_addresses -} - -/// Query the local system for all interface addresses. -pub fn ifaces() -> Result, ::std::io::Error> { - let mut adapters_list = Vec::with_capacity(PREALLOC_ADAPTERS_LEN); - unsafe { - local_ifaces_with_buffer(&mut adapters_list)?; - - Ok(map_adapter_addresses( - adapters_list.as_ptr() as *const IpAdapterAddresses - )) - } -} - -unsafe fn is_ipv4_enabled(unicast_addr: &IpAdapterUnicastAddress) -> bool { - if unicast_addr.length != 0 { - let socket_addr = &unicast_addr.address; - let sa_family = (*socket_addr.lpSockaddr).sa_family; - - sa_family == AF_INET as u16 - } else { - false - } -} - -unsafe fn is_ipv6_enabled(unicast_addr: &IpAdapterUnicastAddress) -> bool { - if unicast_addr.length != 0 { - let socket_addr = &unicast_addr.address; - let sa_family = (*socket_addr.lpSockaddr).sa_family; - - sa_family == AF_INET6 as u16 - } else { - false - } -} diff --git a/util/src/ifaces/mod.rs b/util/src/ifaces/mod.rs deleted file mode 100644 index b786659e6..000000000 --- a/util/src/ifaces/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -pub mod ffi; -pub use ffi::ifaces; - -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum NextHop { - Broadcast(::std::net::SocketAddr), - Destination(::std::net::SocketAddr), -} - -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Kind { - Packet, - Link, - Ipv4, - Ipv6, - Unknow(i32), -} - -#[derive(Debug, Clone)] -pub struct Interface { - pub name: String, - pub kind: Kind, - pub addr: Option<::std::net::SocketAddr>, - pub mask: Option<::std::net::SocketAddr>, - pub hop: Option, -} diff --git a/util/src/lib.rs b/util/src/lib.rs deleted file mode 100644 index b149bbb50..000000000 --- a/util/src/lib.rs +++ /dev/null @@ -1,88 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -use std::io; - -use async_trait::async_trait; -use thiserror::Error; - -#[cfg(feature = "vnet")] -#[macro_use] -extern crate lazy_static; - -#[cfg(target_family = "windows")] -#[macro_use] -extern crate bitflags; - -pub mod fixed_big_int; -pub mod replay_detector; - -/// KeyingMaterialExporter to extract keying material. -/// -/// This trait sits here to avoid getting a direct dependency between -/// the dtls and srtp crates. -#[async_trait] -pub trait KeyingMaterialExporter { - async fn export_keying_material( - &self, - label: &str, - context: &[u8], - length: usize, - ) -> std::result::Result, KeyingMaterialExporterError>; -} - -/// Possible errors while exporting keying material. -/// -/// These errors might have been more logically kept in the dtls -/// crate, but that would have required a direct dependency between -/// srtp and dtls. -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum KeyingMaterialExporterError { - #[error("tls handshake is in progress")] - HandshakeInProgress, - #[error("context is not supported for export_keying_material")] - ContextUnsupported, - #[error("export_keying_material can not be used with a reserved label")] - ReservedExportKeyingMaterial, - #[error("no cipher suite for export_keying_material")] - CipherSuiteUnset, - #[error("export_keying_material io: {0}")] - Io(#[source] error::IoError), - #[error("export_keying_material hash: {0}")] - Hash(String), -} - -impl From for KeyingMaterialExporterError { - fn from(e: io::Error) -> Self { - KeyingMaterialExporterError::Io(error::IoError(e)) - } -} - -#[cfg(feature = "buffer")] -pub mod buffer; - -#[cfg(feature = "conn")] -pub mod conn; - -#[cfg(feature = "ifaces")] -pub mod ifaces; - -#[cfg(feature = "vnet")] -pub mod vnet; - -#[cfg(feature = "marshal")] -pub mod marshal; - -#[cfg(feature = "buffer")] -pub use crate::buffer::Buffer; -#[cfg(feature = "conn")] -pub use crate::conn::Conn; -#[cfg(feature = "marshal")] -pub use crate::marshal::{exact_size_buf::ExactSizeBuf, Marshal, MarshalSize, Unmarshal}; - -mod error; -pub use error::{Error, Result}; - -#[cfg(feature = "sync")] -pub mod sync; diff --git a/util/src/marshal/exact_size_buf.rs b/util/src/marshal/exact_size_buf.rs deleted file mode 100644 index daf826759..000000000 --- a/util/src/marshal/exact_size_buf.rs +++ /dev/null @@ -1,96 +0,0 @@ -// FIXME(regexident): -// Replace with `bytes::ExactSizeBuf` once merged: -// https://github.com/tokio-rs/bytes/pull/496 - -use bytes::buf::{Chain, Take}; -use bytes::{Bytes, BytesMut}; - -/// A trait for buffers that know their exact length. -pub trait ExactSizeBuf { - /// Returns the exact length of the buffer. - fn len(&self) -> usize; - - /// Returns `true` if the buffer is empty. - /// - /// This method has a default implementation using `ExactSizeBuf::len()`, - /// so you don't need to implement it yourself. - #[inline] - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl ExactSizeBuf for Bytes { - #[inline] - fn len(&self) -> usize { - Bytes::len(self) - } - - #[inline] - fn is_empty(&self) -> bool { - Bytes::is_empty(self) - } -} - -impl ExactSizeBuf for BytesMut { - #[inline] - fn len(&self) -> usize { - BytesMut::len(self) - } - - #[inline] - fn is_empty(&self) -> bool { - BytesMut::is_empty(self) - } -} - -impl ExactSizeBuf for [u8] { - #[inline] - fn len(&self) -> usize { - <[u8]>::len(self) - } - - #[inline] - fn is_empty(&self) -> bool { - <[u8]>::is_empty(self) - } -} - -impl ExactSizeBuf for Chain -where - T: ExactSizeBuf, - U: ExactSizeBuf, -{ - fn len(&self) -> usize { - let first_ref = self.first_ref(); - let last_ref = self.last_ref(); - - first_ref.len() + last_ref.len() - } - - fn is_empty(&self) -> bool { - let first_ref = self.first_ref(); - let last_ref = self.last_ref(); - - first_ref.is_empty() && last_ref.is_empty() - } -} - -impl ExactSizeBuf for Take -where - T: ExactSizeBuf, -{ - fn len(&self) -> usize { - let inner_ref = self.get_ref(); - let limit = self.limit(); - - limit.min(inner_ref.len()) - } - - fn is_empty(&self) -> bool { - let inner_ref = self.get_ref(); - let limit = self.limit(); - - (limit == 0) || inner_ref.is_empty() - } -} diff --git a/util/src/marshal/mod.rs b/util/src/marshal/mod.rs deleted file mode 100644 index aa4bb56cb..000000000 --- a/util/src/marshal/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub mod exact_size_buf; - -use bytes::{Buf, Bytes, BytesMut}; - -use crate::error::{Error, Result}; - -pub trait MarshalSize { - fn marshal_size(&self) -> usize; -} - -pub trait Marshal: MarshalSize { - fn marshal_to(&self, buf: &mut [u8]) -> Result; - - fn marshal(&self) -> Result { - let l = self.marshal_size(); - let mut buf = BytesMut::with_capacity(l); - buf.resize(l, 0); - let n = self.marshal_to(&mut buf)?; - if n != l { - Err(Error::Other(format!( - "marshal_to output size {n}, but expect {l}" - ))) - } else { - Ok(buf.freeze()) - } - } -} - -pub trait Unmarshal: MarshalSize { - fn unmarshal(buf: &mut B) -> Result - where - Self: Sized, - B: Buf; -} diff --git a/util/src/replay_detector/mod.rs b/util/src/replay_detector/mod.rs deleted file mode 100644 index 127707be6..000000000 --- a/util/src/replay_detector/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -#[cfg(test)] -mod replay_detector_test; - -use super::fixed_big_int::*; - -// ReplayDetector is the interface of sequence replay detector. -pub trait ReplayDetector { - // Check returns true if given sequence number is not replayed. - // Call accept() to mark the packet is received properly. - fn check(&mut self, seq: u64) -> bool; - fn accept(&mut self); -} - -pub struct SlidingWindowDetector { - accepted: bool, - seq: u64, - latest_seq: u64, - max_seq: u64, - window_size: usize, - mask: FixedBigInt, -} - -impl SlidingWindowDetector { - // New creates ReplayDetector. - // Created ReplayDetector doesn't allow wrapping. - // It can handle monotonically increasing sequence number up to - // full 64bit number. It is suitable for DTLS replay protection. - pub fn new(window_size: usize, max_seq: u64) -> Self { - SlidingWindowDetector { - accepted: false, - seq: 0, - latest_seq: 0, - max_seq, - window_size, - mask: FixedBigInt::new(window_size), - } - } -} - -impl ReplayDetector for SlidingWindowDetector { - fn check(&mut self, seq: u64) -> bool { - self.accepted = false; - - if seq > self.max_seq { - // Exceeded upper limit. - return false; - } - - if seq <= self.latest_seq { - if self.latest_seq >= self.window_size as u64 + seq { - return false; - } - if self.mask.bit((self.latest_seq - seq) as usize) != 0 { - // The sequence number is duplicated. - return false; - } - } - - self.accepted = true; - self.seq = seq; - true - } - - fn accept(&mut self) { - if !self.accepted { - return; - } - - if self.seq > self.latest_seq { - // Update the head of the window. - self.mask.lsh((self.seq - self.latest_seq) as usize); - self.latest_seq = self.seq; - } - let diff = (self.latest_seq - self.seq) % self.max_seq; - self.mask.set_bit(diff as usize); - } -} - -pub struct WrappedSlidingWindowDetector { - accepted: bool, - seq: u64, - latest_seq: u64, - max_seq: u64, - window_size: usize, - mask: FixedBigInt, - init: bool, -} - -impl WrappedSlidingWindowDetector { - // WithWrap creates ReplayDetector allowing sequence wrapping. - // This is suitable for short bitwidth counter like SRTP and SRTCP. - pub fn new(window_size: usize, max_seq: u64) -> Self { - WrappedSlidingWindowDetector { - accepted: false, - seq: 0, - latest_seq: 0, - max_seq, - window_size, - mask: FixedBigInt::new(window_size), - init: false, - } - } -} - -impl ReplayDetector for WrappedSlidingWindowDetector { - fn check(&mut self, seq: u64) -> bool { - self.accepted = false; - - if seq > self.max_seq { - // Exceeded upper limit. - return false; - } - if !self.init { - if seq != 0 { - self.latest_seq = seq - 1; - } else { - self.latest_seq = self.max_seq; - } - self.init = true; - } - - let mut diff = self.latest_seq as i64 - seq as i64; - // Wrap the number. - if diff > self.max_seq as i64 / 2 { - diff -= (self.max_seq + 1) as i64; - } else if diff <= -(self.max_seq as i64 / 2) { - diff += (self.max_seq + 1) as i64; - } - - if diff >= self.window_size as i64 { - // Too old. - return false; - } - if diff >= 0 && self.mask.bit(diff as usize) != 0 { - // The sequence number is duplicated. - return false; - } - - self.accepted = true; - self.seq = seq; - true - } - - fn accept(&mut self) { - if !self.accepted { - return; - } - - let mut diff = self.latest_seq as i64 - self.seq as i64; - // Wrap the number. - if diff > self.max_seq as i64 / 2 { - diff -= (self.max_seq + 1) as i64; - } else if diff <= -(self.max_seq as i64 / 2) { - diff += (self.max_seq + 1) as i64; - } - - assert!(diff < self.window_size as i64); - - if diff < 0 { - // Update the head of the window. - self.mask.lsh((-diff) as usize); - self.latest_seq = self.seq; - } - self.mask - .set_bit((self.latest_seq as isize - self.seq as isize) as usize); - } -} - -#[derive(Default)] -pub struct NoOpReplayDetector; - -impl ReplayDetector for NoOpReplayDetector { - fn check(&mut self, _: u64) -> bool { - true - } - fn accept(&mut self) {} -} diff --git a/util/src/replay_detector/replay_detector_test.rs b/util/src/replay_detector/replay_detector_test.rs deleted file mode 100644 index 9537e8987..000000000 --- a/util/src/replay_detector/replay_detector_test.rs +++ /dev/null @@ -1,278 +0,0 @@ -use super::*; - -#[test] -fn test_replay_detector() { - const LARGE_SEQ: u64 = 0x100000000000; - - #[allow(clippy::type_complexity)] - let tests: Vec<(&str, usize, u64, Vec, Vec, Vec, Vec)> = vec![ - ( - "Continuous", - 16, - 0x0000FFFFFFFFFFFF, - vec![ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, - ], - vec![ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - ], - vec![], - ), - ( - "ValidLargeJump", - 16, - 0x0000FFFFFFFFFFFF, - vec![ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - LARGE_SEQ, - 11, - LARGE_SEQ + 1, - LARGE_SEQ + 2, - LARGE_SEQ + 3, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, - ], - vec![ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - LARGE_SEQ, - LARGE_SEQ + 1, - LARGE_SEQ + 2, - LARGE_SEQ + 3, - ], - vec![], - ), - ( - "InvalidLargeJump", - 16, - 0x0000FFFFFFFFFFFF, - vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, LARGE_SEQ, 11, 12, 13, 14, 15], - vec![ - true, true, true, true, true, true, true, true, true, true, false, true, true, - true, true, true, - ], - vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15], - vec![], - ), - ( - "DuplicateAfterValidJump", - 196, - 0x0000FFFFFFFFFFFF, - vec![0, 1, 2, 129, 0, 1, 2], - vec![true, true, true, true, true, true, true], - vec![0, 1, 2, 129], - vec![], - ), - ( - "DuplicateAfterInvalidJump", - 196, - 0x0000FFFFFFFFFFFF, - vec![0, 1, 2, 128, 0, 1, 2], - vec![true, true, true, false, true, true, true], - vec![0, 1, 2], - vec![], - ), - ( - "ContinuousOffset", - 16, - 0x0000FFFFFFFFFFFF, - vec![ - 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, - ], - vec![ - 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, - ], - vec![], - ), - ( - "Reordered", - 128, - 0x0000FFFFFFFFFFFF, - vec![ - 96, 64, 16, 80, 32, 48, 8, 24, 88, 40, 128, 56, 72, 112, 104, 120, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, - ], - vec![ - 96, 64, 16, 80, 32, 48, 8, 24, 88, 40, 128, 56, 72, 112, 104, 120, - ], - vec![], - ), - ( - "Old", - 100, - 0x0000FFFFFFFFFFFF, - vec![ - 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 8, 16, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, - ], - vec![24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128], - vec![], - ), - ( - "ContinuouesReplayed", - 8, - 0x0000FFFFFFFFFFFF, - vec![ - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, - ], - vec![16, 17, 18, 19, 20, 21, 22, 23, 24, 25], - vec![], - ), - ( - "ReplayedLater", - 128, - 0x0000FFFFFFFFFFFF, - vec![ - 16, 32, 48, 64, 80, 96, 112, 128, 16, 32, 48, 64, 80, 96, 112, 128, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, - ], - vec![16, 32, 48, 64, 80, 96, 112, 128], - vec![], - ), - ( - "ReplayedQuick", - 128, - 0x0000FFFFFFFFFFFF, - vec![ - 16, 16, 32, 32, 48, 48, 64, 64, 80, 80, 96, 96, 112, 112, 128, 128, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, - ], - vec![16, 32, 48, 64, 80, 96, 112, 128], - vec![], - ), - ( - "Strict", - 0, - 0x0000FFFFFFFFFFFF, - vec![1, 3, 2, 4, 5, 6, 7, 8, 9, 10], - vec![true, true, true, true, true, true, true, true, true, true], - vec![1, 3, 4, 5, 6, 7, 8, 9, 10], - vec![], - ), - ( - "Overflow", - 128, - 0x0000FFFFFFFFFFFF, - vec![ - 0x0000FFFFFFFFFFFE, - 0x0000FFFFFFFFFFFF, - 0x0001000000000000, - 0x0001000000000001, - ], - vec![true, true, true, true], - vec![0x0000FFFFFFFFFFFE, 0x0000FFFFFFFFFFFF], - vec![], - ), - ( - "WrapContinuous", - 64, - 0xFFFF, - vec![ - 0xFFFC, 0xFFFD, 0xFFFE, 0xFFFF, 0x0000, 0x0001, 0x0002, 0x0003, - ], - vec![true, true, true, true, true, true, true, true], - vec![0xFFFC, 0xFFFD, 0xFFFE, 0xFFFF], - vec![ - 0xFFFC, 0xFFFD, 0xFFFE, 0xFFFF, 0x0000, 0x0001, 0x0002, 0x0003, - ], - ), - ( - "WrapReordered", - 64, - 0xFFFF, - vec![ - 0xFFFD, 0xFFFC, 0x0002, 0xFFFE, 0x0000, 0x0001, 0xFFFF, 0x0003, - ], - vec![true, true, true, true, true, true, true, true], - vec![0xFFFD, 0xFFFC, 0xFFFE, 0xFFFF], - vec![ - 0xFFFD, 0xFFFC, 0x0002, 0xFFFE, 0x0000, 0x0001, 0xFFFF, 0x0003, - ], - ), - ( - "WrapReorderedReplayed", - 64, - 0xFFFF, - vec![ - 0xFFFD, 0xFFFC, 0xFFFC, 0x0002, 0xFFFE, 0xFFFC, 0x0000, 0x0001, 0x0001, 0xFFFF, - 0x0001, 0x0003, - ], - vec![ - true, true, true, true, true, true, true, true, true, true, true, true, - ], - vec![0xFFFD, 0xFFFC, 0xFFFE, 0xFFFF], - vec![ - 0xFFFD, 0xFFFC, 0x0002, 0xFFFE, 0x0000, 0x0001, 0xFFFF, 0x0003, - ], - ), - ]; - - for (name, windows_size, max_seq, input, valid, expected, mut expected_wrap) in tests { - if expected_wrap.is_empty() { - expected_wrap.extend_from_slice(&expected); - } - - for k in 0..2 { - let mut det: Box = if k == 0 { - Box::new(SlidingWindowDetector::new(windows_size, max_seq)) - } else { - Box::new(WrappedSlidingWindowDetector::new(windows_size, max_seq)) - }; - let exp = if k == 0 { &expected } else { &expected_wrap }; - - let mut out = vec![]; - for (i, seq) in input.iter().enumerate() { - let ok = det.check(*seq); - if ok && valid[i] { - out.push(*seq); - det.accept(); - } - } - - assert_eq!(&out, exp, "{name} failed"); - } - } -} diff --git a/util/src/sync/mod.rs b/util/src/sync/mod.rs deleted file mode 100644 index cab9050cf..000000000 --- a/util/src/sync/mod.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ops, sync}; - -/// A synchronous mutual exclusion primitive useful for protecting shared data. -#[derive(Default, Debug)] -pub struct Mutex(sync::Mutex); - -impl Mutex { - /// Creates a new mutex in an unlocked state ready for use. - pub fn new(value: T) -> Self { - Self(sync::Mutex::new(value)) - } - - /// Acquires a mutex, blocking the current thread until it is able to do so. - pub fn lock(&self) -> MutexGuard<'_, T> { - let guard = self.0.lock().unwrap(); - - MutexGuard(guard) - } - - /// Consumes this mutex, returning the underlying data. - pub fn into_inner(self) -> T { - self.0.into_inner().unwrap() - } -} - -/// An RAII implementation of a "scoped lock" of a mutex. When this structure is -/// dropped (falls out of scope), the lock will be unlocked. -pub struct MutexGuard<'a, T>(sync::MutexGuard<'a, T>); - -impl<'a, T> ops::Deref for MutexGuard<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, T> ops::DerefMut for MutexGuard<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// A synchronous reader-writer lock. -#[derive(Default, Debug)] -pub struct RwLock(sync::RwLock); - -impl RwLock { - /// Creates a new mutex in an unlocked state ready for use. - pub fn new(value: T) -> Self { - Self(sync::RwLock::new(value)) - } - - /// Locks this rwlock with shared read access, blocking the current thread - /// until it can be acquired. - pub fn read(&self) -> RwLockReadGuard<'_, T> { - let guard = self.0.read().unwrap(); - - RwLockReadGuard(guard) - } - - /// Locks this rwlock with exclusive write access, blocking the current - /// thread until it can be acquired. - pub fn write(&self) -> RwLockWriteGuard<'_, T> { - let guard = self.0.write().unwrap(); - - RwLockWriteGuard(guard) - } -} - -/// RAII structure used to release the shared read access of a lock when -/// dropped. -pub struct RwLockReadGuard<'a, T>(sync::RwLockReadGuard<'a, T>); - -impl<'a, T> ops::Deref for RwLockReadGuard<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// RAII structure used to release the exclusive write access of a lock when -/// dropped. -pub struct RwLockWriteGuard<'a, T>(sync::RwLockWriteGuard<'a, T>); - -impl<'a, T> ops::Deref for RwLockWriteGuard<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, T> ops::DerefMut for RwLockWriteGuard<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} diff --git a/util/src/vnet/chunk.rs b/util/src/vnet/chunk.rs deleted file mode 100644 index 6300aff8d..000000000 --- a/util/src/vnet/chunk.rs +++ /dev/null @@ -1,352 +0,0 @@ -#[cfg(test)] -mod chunk_test; - -use std::fmt; -use std::net::{IpAddr, SocketAddr}; -use std::ops::{BitAnd, BitOr}; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::time::SystemTime; - -use portable_atomic::AtomicU64; - -use super::net::*; -use crate::error::Result; - -lazy_static! { - static ref TAG_CTR: AtomicU64 = AtomicU64::new(0); -} - -/// Encodes a u64 value to a lowercase base 36 string. -pub fn base36(value: impl Into) -> String { - let mut digits: Vec = vec![]; - - let mut value = value.into(); - while value > 0 { - let digit = (value % 36) as usize; - value /= 36; - - digits.push(b"0123456789abcdefghijklmnopqrstuvwxyz"[digit]); - } - - digits.reverse(); - format!("{:0>8}", String::from_utf8(digits).unwrap()) -} - -// Generate a base36-encoded unique tag -// See: https://play.golang.org/p/0ZaAID1q-HN -fn assign_chunk_tag() -> String { - let n = TAG_CTR.fetch_add(1, Ordering::SeqCst); - base36(n) -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub(crate) struct TcpFlag(pub(crate) u8); - -pub(crate) const TCP_FLAG_ZERO: TcpFlag = TcpFlag(0x00); -pub(crate) const TCP_FLAG_FIN: TcpFlag = TcpFlag(0x01); -pub(crate) const TCP_FLAG_SYN: TcpFlag = TcpFlag(0x02); -pub(crate) const TCP_FLAG_RST: TcpFlag = TcpFlag(0x04); -pub(crate) const TCP_FLAG_PSH: TcpFlag = TcpFlag(0x08); -pub(crate) const TCP_FLAG_ACK: TcpFlag = TcpFlag(0x10); - -impl BitOr for TcpFlag { - type Output = Self; - - // rhs is the "right-hand side" of the expression `a | b` - fn bitor(self, rhs: Self) -> Self::Output { - Self(self.0 | rhs.0) - } -} - -impl BitAnd for TcpFlag { - type Output = Self; - - // rhs is the "right-hand side" of the expression `a & b` - fn bitand(self, rhs: Self) -> Self::Output { - Self(self.0 & rhs.0) - } -} - -impl fmt::Display for TcpFlag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut sa = vec![]; - if *self & TCP_FLAG_FIN != TCP_FLAG_ZERO { - sa.push("FIN"); - } - if *self & TCP_FLAG_SYN != TCP_FLAG_ZERO { - sa.push("SYN"); - } - if *self & TCP_FLAG_RST != TCP_FLAG_ZERO { - sa.push("RST"); - } - if *self & TCP_FLAG_PSH != TCP_FLAG_ZERO { - sa.push("PSH"); - } - if *self & TCP_FLAG_ACK != TCP_FLAG_ZERO { - sa.push("ACK"); - } - - write!(f, "{}", sa.join("-")) - } -} - -// Chunk represents a packet passed around in the vnet -pub trait Chunk: fmt::Display + fmt::Debug { - fn set_timestamp(&mut self) -> SystemTime; // used by router - fn get_timestamp(&self) -> SystemTime; // used by router - fn get_source_ip(&self) -> IpAddr; // used by routee - fn get_destination_ip(&self) -> IpAddr; // used by router - fn set_source_addr(&mut self, address: &str) -> Result<()>; // used by nat - fn set_destination_addr(&mut self, address: &str) -> Result<()>; // used by nat - - fn source_addr(&self) -> SocketAddr; - fn destination_addr(&self) -> SocketAddr; - fn user_data(&self) -> Vec; - fn tag(&self) -> String; - fn network(&self) -> String; // returns "udp" or "tcp" - fn clone_to(&self) -> Box; -} - -#[derive(PartialEq, Debug)] -pub(crate) struct ChunkIp { - pub(crate) timestamp: SystemTime, - pub(crate) source_ip: IpAddr, - pub(crate) destination_ip: IpAddr, - pub(crate) tag: String, -} - -impl ChunkIp { - fn set_timestamp(&mut self) -> SystemTime { - self.timestamp = SystemTime::now(); - self.timestamp - } - - fn get_timestamp(&self) -> SystemTime { - self.timestamp - } - - fn get_destination_ip(&self) -> IpAddr { - self.destination_ip - } - - fn get_source_ip(&self) -> IpAddr { - self.source_ip - } - - fn tag(&self) -> String { - self.tag.clone() - } -} - -#[derive(PartialEq, Debug)] -pub(crate) struct ChunkUdp { - pub(crate) chunk_ip: ChunkIp, - pub(crate) source_port: u16, - pub(crate) destination_port: u16, - pub(crate) user_data: Vec, -} - -impl fmt::Display for ChunkUdp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} chunk {} {} => {}", - self.network(), - self.tag(), - self.source_addr(), - self.destination_addr(), - ) - } -} - -impl Chunk for ChunkUdp { - fn set_timestamp(&mut self) -> SystemTime { - self.chunk_ip.set_timestamp() - } - - fn get_timestamp(&self) -> SystemTime { - self.chunk_ip.get_timestamp() - } - - fn get_destination_ip(&self) -> IpAddr { - self.chunk_ip.get_destination_ip() - } - - fn get_source_ip(&self) -> IpAddr { - self.chunk_ip.get_source_ip() - } - - fn tag(&self) -> String { - self.chunk_ip.tag() - } - - fn source_addr(&self) -> SocketAddr { - SocketAddr::new(self.chunk_ip.source_ip, self.source_port) - } - - fn destination_addr(&self) -> SocketAddr { - SocketAddr::new(self.chunk_ip.destination_ip, self.destination_port) - } - - fn user_data(&self) -> Vec { - self.user_data.clone() - } - - fn clone_to(&self) -> Box { - Box::new(ChunkUdp { - chunk_ip: ChunkIp { - timestamp: self.chunk_ip.timestamp, - source_ip: self.chunk_ip.source_ip, - destination_ip: self.chunk_ip.destination_ip, - tag: self.chunk_ip.tag.clone(), - }, - source_port: self.source_port, - destination_port: self.destination_port, - user_data: self.user_data.clone(), - }) - } - - fn network(&self) -> String { - UDP_STR.to_owned() - } - - fn set_source_addr(&mut self, address: &str) -> Result<()> { - let addr = SocketAddr::from_str(address)?; - self.chunk_ip.source_ip = addr.ip(); - self.source_port = addr.port(); - Ok(()) - } - - fn set_destination_addr(&mut self, address: &str) -> Result<()> { - let addr = SocketAddr::from_str(address)?; - self.chunk_ip.destination_ip = addr.ip(); - self.destination_port = addr.port(); - Ok(()) - } -} - -impl ChunkUdp { - pub(crate) fn new(src_addr: SocketAddr, dst_addr: SocketAddr) -> Self { - ChunkUdp { - chunk_ip: ChunkIp { - timestamp: SystemTime::now(), - source_ip: src_addr.ip(), - destination_ip: dst_addr.ip(), - tag: assign_chunk_tag(), - }, - source_port: src_addr.port(), - destination_port: dst_addr.port(), - user_data: vec![], - } - } -} - -#[derive(PartialEq, Debug)] -pub(crate) struct ChunkTcp { - chunk_ip: ChunkIp, - source_port: u16, - destination_port: u16, - flags: TcpFlag, // control bits - user_data: Vec, // only with PSH flag - // seq :u32, // always starts with 0 - // ack :u32, // always starts with 0 -} - -impl fmt::Display for ChunkTcp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} chunk {} {} => {}", - self.network(), - self.flags, - self.chunk_ip.tag, - self.source_addr(), - self.destination_addr(), - ) - } -} - -impl Chunk for ChunkTcp { - fn set_timestamp(&mut self) -> SystemTime { - self.chunk_ip.set_timestamp() - } - - fn get_timestamp(&self) -> SystemTime { - self.chunk_ip.get_timestamp() - } - - fn get_destination_ip(&self) -> IpAddr { - self.chunk_ip.get_destination_ip() - } - - fn get_source_ip(&self) -> IpAddr { - self.chunk_ip.get_source_ip() - } - - fn tag(&self) -> String { - self.chunk_ip.tag() - } - - fn source_addr(&self) -> SocketAddr { - SocketAddr::new(self.chunk_ip.source_ip, self.source_port) - } - - fn destination_addr(&self) -> SocketAddr { - SocketAddr::new(self.chunk_ip.destination_ip, self.destination_port) - } - - fn user_data(&self) -> Vec { - self.user_data.clone() - } - - fn clone_to(&self) -> Box { - Box::new(ChunkTcp { - chunk_ip: ChunkIp { - timestamp: self.chunk_ip.timestamp, - source_ip: self.chunk_ip.source_ip, - destination_ip: self.chunk_ip.destination_ip, - tag: self.chunk_ip.tag.clone(), - }, - source_port: self.source_port, - destination_port: self.destination_port, - flags: self.flags, - user_data: self.user_data.clone(), - }) - } - - fn network(&self) -> String { - "tcp".to_owned() - } - - fn set_source_addr(&mut self, address: &str) -> Result<()> { - let addr = SocketAddr::from_str(address)?; - self.chunk_ip.source_ip = addr.ip(); - self.source_port = addr.port(); - Ok(()) - } - - fn set_destination_addr(&mut self, address: &str) -> Result<()> { - let addr = SocketAddr::from_str(address)?; - self.chunk_ip.destination_ip = addr.ip(); - self.destination_port = addr.port(); - Ok(()) - } -} - -impl ChunkTcp { - pub(crate) fn new(src_addr: SocketAddr, dst_addr: SocketAddr, flags: TcpFlag) -> Self { - ChunkTcp { - chunk_ip: ChunkIp { - timestamp: SystemTime::now(), - source_ip: src_addr.ip(), - destination_ip: dst_addr.ip(), - tag: assign_chunk_tag(), - }, - source_port: src_addr.port(), - destination_port: dst_addr.port(), - flags, - user_data: vec![], - } - } -} diff --git a/util/src/vnet/chunk/chunk_test.rs b/util/src/vnet/chunk/chunk_test.rs deleted file mode 100644 index 92b5ae381..000000000 --- a/util/src/vnet/chunk/chunk_test.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::*; -use crate::error::Result; - -#[test] -fn test_tcp_frag_string() { - let f = TCP_FLAG_FIN; - assert_eq!(f.to_string(), "FIN", "should match"); - let f = TCP_FLAG_SYN; - assert_eq!(f.to_string(), "SYN", "should match"); - let f = TCP_FLAG_RST; - assert_eq!(f.to_string(), "RST", "should match"); - let f = TCP_FLAG_PSH; - assert_eq!(f.to_string(), "PSH", "should match"); - let f = TCP_FLAG_ACK; - assert_eq!(f.to_string(), "ACK", "should match"); - let f = TCP_FLAG_SYN | TCP_FLAG_ACK; - assert_eq!(f.to_string(), "SYN-ACK", "should match"); -} - -const DEMO_IP: &str = "1.2.3.4"; - -#[test] -fn test_chunk_udp() -> Result<()> { - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str(&(DEMO_IP.to_owned() + ":5678"))?; - - let mut c = ChunkUdp::new(src, dst); - let s = c.to_string(); - log::debug!("chunk: {}", s); - assert_eq!(c.network(), UDP_STR, "should match"); - assert!(s.contains(&src.to_string()), "should include address"); - assert!(s.contains(&dst.to_string()), "should include address"); - assert_eq!(c.get_source_ip(), src.ip(), "ip should match"); - assert_eq!(c.get_destination_ip(), dst.ip(), "ip should match"); - - // Test timestamp - let ts = c.set_timestamp(); - assert_eq!(ts, c.get_timestamp(), "timestamp should match"); - - c.user_data = "Hello".as_bytes().to_vec(); - - let cloned = c.clone_to(); - - // Test setSourceAddr - c.set_source_addr("2.3.4.5:4000")?; - assert_eq!(c.source_addr().to_string(), "2.3.4.5:4000"); - - // Test Tag() - assert!(!c.tag().is_empty(), "should not be empty"); - - // Verify cloned chunk was not affected by the changes to original chunk - c.user_data[0] = b'!'; // oroginal: "Hello" -> "Hell!" - assert_eq!(cloned.user_data(), "Hello".as_bytes(), "should match"); - assert_eq!(cloned.source_addr().to_string(), "192.168.0.2:1234"); - assert_eq!(cloned.get_source_ip(), src.ip(), "ip should match"); - assert_eq!(cloned.get_destination_ip(), dst.ip(), "ip should match"); - - Ok(()) -} diff --git a/util/src/vnet/chunk_queue.rs b/util/src/vnet/chunk_queue.rs deleted file mode 100644 index c90d342e3..000000000 --- a/util/src/vnet/chunk_queue.rs +++ /dev/null @@ -1,44 +0,0 @@ -#[cfg(test)] -mod chunk_queue_test; - -use std::collections::VecDeque; - -use tokio::sync::RwLock; - -use super::chunk::*; - -#[derive(Default)] -pub(crate) struct ChunkQueue { - chunks: RwLock>>, - max_size: usize, // 0 or negative value: unlimited -} - -impl ChunkQueue { - pub(crate) fn new(max_size: usize) -> Self { - ChunkQueue { - chunks: RwLock::new(VecDeque::new()), - max_size, - } - } - - pub(crate) async fn push(&self, c: Box) -> bool { - let mut chunks = self.chunks.write().await; - - if self.max_size > 0 && chunks.len() >= self.max_size { - false // dropped - } else { - chunks.push_back(c); - true - } - } - - pub(crate) async fn pop(&self) -> Option> { - let mut chunks = self.chunks.write().await; - chunks.pop_front() - } - - pub(crate) async fn peek(&self) -> Option> { - let chunks = self.chunks.read().await; - chunks.front().map(|chunk| chunk.clone_to()) - } -} diff --git a/util/src/vnet/chunk_queue/chunk_queue_test.rs b/util/src/vnet/chunk_queue/chunk_queue_test.rs deleted file mode 100644 index b865d3e01..000000000 --- a/util/src/vnet/chunk_queue/chunk_queue_test.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::net::SocketAddr; -use std::str::FromStr; - -use super::*; -use crate::error::Result; - -const DEMO_IP: &str = "1.2.3.4"; - -#[tokio::test] -async fn test_chunk_queue() -> Result<()> { - let c: Box = Box::new(ChunkUdp::new( - SocketAddr::from_str("192.188.0.2:1234")?, - SocketAddr::from_str(&(DEMO_IP.to_owned() + ":5678"))?, - )); - - let q = ChunkQueue::new(0); - - let d = q.peek().await; - assert!(d.is_none(), "should return none"); - - let ok = q.push(c.clone_to()).await; - assert!(ok, "should succeed"); - - let d = q.pop().await; - assert!(d.is_some(), "should succeed"); - if let Some(d) = d { - assert_eq!(c.to_string(), d.to_string(), "should be the same"); - } - - let d = q.pop().await; - assert!(d.is_none(), "should fail"); - - let q = ChunkQueue::new(1); - let ok = q.push(c.clone_to()).await; - assert!(ok, "should succeed"); - - let ok = q.push(c.clone_to()).await; - assert!(!ok, "should fail"); - - let d = q.peek().await; - assert!(d.is_some(), "should succeed"); - if let Some(d) = d { - assert_eq!(c.to_string(), d.to_string(), "should be the same"); - } - - Ok(()) -} diff --git a/util/src/vnet/conn.rs b/util/src/vnet/conn.rs deleted file mode 100644 index 676ca495d..000000000 --- a/util/src/vnet/conn.rs +++ /dev/null @@ -1,164 +0,0 @@ -#[cfg(test)] -mod conn_test; - -use std::net::{IpAddr, SocketAddr}; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use portable_atomic::AtomicBool; -use tokio::sync::{mpsc, Mutex}; - -use crate::conn::Conn; -use crate::error::*; -use crate::sync::RwLock; -use crate::vnet::chunk::{Chunk, ChunkUdp}; - -const MAX_READ_QUEUE_SIZE: usize = 1024; - -/// vNet implements this -#[async_trait] -pub(crate) trait ConnObserver { - async fn write(&self, c: Box) -> Result<()>; - async fn on_closed(&self, addr: SocketAddr); - fn determine_source_ip(&self, loc_ip: IpAddr, dst_ip: IpAddr) -> Option; -} - -pub(crate) type ChunkChTx = mpsc::Sender>; - -/// UDPConn is the implementation of the Conn and PacketConn interfaces for UDP network connections. -/// compatible with net.PacketConn and net.Conn -pub(crate) struct UdpConn { - loc_addr: SocketAddr, - rem_addr: RwLock>, - read_ch_tx: Arc>>, - read_ch_rx: Mutex>>, - closed: AtomicBool, - obs: Arc>, -} - -impl UdpConn { - pub(crate) fn new( - loc_addr: SocketAddr, - rem_addr: Option, - obs: Arc>, - ) -> Self { - let (read_ch_tx, read_ch_rx) = mpsc::channel(MAX_READ_QUEUE_SIZE); - - UdpConn { - loc_addr, - rem_addr: RwLock::new(rem_addr), - read_ch_tx: Arc::new(Mutex::new(Some(read_ch_tx))), - read_ch_rx: Mutex::new(read_ch_rx), - closed: AtomicBool::new(false), - obs, - } - } - - pub(crate) fn get_inbound_ch(&self) -> Arc>> { - Arc::clone(&self.read_ch_tx) - } -} - -#[async_trait] -impl Conn for UdpConn { - async fn connect(&self, addr: SocketAddr) -> Result<()> { - self.rem_addr.write().replace(addr); - - Ok(()) - } - async fn recv(&self, buf: &mut [u8]) -> Result { - let (n, _) = self.recv_from(buf).await?; - Ok(n) - } - - /// recv_from reads a packet from the connection, - /// copying the payload into p. It returns the number of - /// bytes copied into p and the return address that - /// was on the packet. - /// It returns the number of bytes read (0 <= n <= len(p)) - /// and any error encountered. Callers should always process - /// the n > 0 bytes returned before considering the error err. - async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - let mut read_ch = self.read_ch_rx.lock().await; - let rem_addr = *self.rem_addr.read(); - while let Some(chunk) = read_ch.recv().await { - let user_data = chunk.user_data(); - let n = std::cmp::min(buf.len(), user_data.len()); - buf[..n].copy_from_slice(&user_data[..n]); - let addr = chunk.source_addr(); - { - if let Some(rem_addr) = &rem_addr { - if &addr != rem_addr { - continue; // discard (shouldn't happen) - } - } - } - return Ok((n, addr)); - } - - Err(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "Connection Aborted").into()) - } - - async fn send(&self, buf: &[u8]) -> Result { - let rem_addr = *self.rem_addr.read(); - if let Some(rem_addr) = rem_addr { - self.send_to(buf, rem_addr).await - } else { - Err(Error::ErrNoRemAddr) - } - } - - /// send_to writes a packet with payload p to addr. - /// send_to can be made to time out and return - async fn send_to(&self, buf: &[u8], target: SocketAddr) -> Result { - let src_ip = { - let obs = self.obs.lock().await; - match obs.determine_source_ip(self.loc_addr.ip(), target.ip()) { - Some(ip) => ip, - None => return Err(Error::ErrLocAddr), - } - }; - - let src_addr = SocketAddr::new(src_ip, self.loc_addr.port()); - - let mut chunk = ChunkUdp::new(src_addr, target); - chunk.user_data = buf.to_vec(); - { - let c: Box = Box::new(chunk); - let obs = self.obs.lock().await; - obs.write(c).await? - } - - Ok(buf.len()) - } - - fn local_addr(&self) -> Result { - Ok(self.loc_addr) - } - - fn remote_addr(&self) -> Option { - *self.rem_addr.read() - } - - async fn close(&self) -> Result<()> { - if self.closed.load(Ordering::SeqCst) { - return Err(Error::ErrAlreadyClosed); - } - self.closed.store(true, Ordering::SeqCst); - { - let mut reach_ch = self.read_ch_tx.lock().await; - reach_ch.take(); - } - { - let obs = self.obs.lock().await; - obs.on_closed(self.loc_addr).await; - } - - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/util/src/vnet/conn/conn_test.rs b/util/src/vnet/conn/conn_test.rs deleted file mode 100644 index b8fe072e5..000000000 --- a/util/src/vnet/conn/conn_test.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::str::FromStr; - -use portable_atomic::AtomicUsize; - -use super::*; - -#[derive(Default)] -struct DummyObserver { - nclosed: Arc, - #[allow(clippy::type_complexity)] - read_ch_tx: Arc>>>>, -} - -#[async_trait] -impl ConnObserver for DummyObserver { - async fn write(&self, c: Box) -> Result<()> { - let mut chunk = ChunkUdp::new(c.destination_addr(), c.source_addr()); - chunk.user_data = c.user_data(); - - let read_ch_tx = self.read_ch_tx.lock().await; - if let Some(tx) = &*read_ch_tx { - tx.send(Box::new(chunk)) - .await - .map_err(|e| Error::Other(e.to_string()))?; - } - Ok(()) - } - - async fn on_closed(&self, _addr: SocketAddr) { - self.nclosed.fetch_add(1, Ordering::SeqCst); - } - - fn determine_source_ip(&self, loc_ip: IpAddr, _dst_ip: IpAddr) -> Option { - Some(loc_ip) - } -} - -//use std::io::Write; - -#[tokio::test] -async fn test_udp_conn_send_to_recv_from() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let nclosed = Arc::new(AtomicUsize::new(0)); - let data = b"Hello".to_vec(); - let src_addr = SocketAddr::from_str("127.0.0.1:1234")?; - let dst_addr = SocketAddr::from_str("127.0.0.1:5678")?; - - let dummy_obs = Arc::new(Mutex::new(DummyObserver::default())); - let dummy_obs2 = Arc::clone(&dummy_obs); - let obs = dummy_obs2 as Arc>; - - let conn = Arc::new(UdpConn::new(src_addr, None, obs)); - { - let mut dummy = dummy_obs.lock().await; - dummy.nclosed = Arc::clone(&nclosed); - dummy.read_ch_tx = conn.get_inbound_ch(); - } - - let conn_rx = Arc::clone(&conn); - let data_rx = data.clone(); - - let (rcvd_ch_tx, mut rcvd_ch_rx) = mpsc::channel(1); - let (done_ch_tx, mut done_ch_rx) = mpsc::channel::<()>(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - - loop { - let (n, addr) = match conn_rx.recv_from(&mut buf).await { - Ok((n, addr)) => (n, addr), - Err(err) => { - log::debug!("conn closed. exiting the read loop with err {}", err); - break; - } - }; - - log::debug!("read data"); - assert_eq!(data_rx.len(), n, "should match"); - assert_eq!(&data_rx, &buf[..n], "should match"); - log::debug!("dst_addr {} vs add {}", dst_addr, addr); - assert_eq!(dst_addr.to_string(), addr.to_string(), "should match"); - let _ = rcvd_ch_tx.send(()).await; - } - - drop(done_ch_tx); - }); - - let n = conn.send_to(&data, dst_addr).await.unwrap(); - assert_eq!(n, data.len(), "should match"); - - loop { - tokio::select! { - result = rcvd_ch_rx.recv() =>{ - if result.is_some(){ - log::debug!("closing soon..."); - conn.close().await?; - } - } - _ = done_ch_rx.recv() => { - log::debug!("recv done_ch_rx..."); - break; - } - } - } - - assert_eq!(1, nclosed.load(Ordering::SeqCst), "should be closed once"); - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_udp_conn_send_recv() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let nclosed = Arc::new(AtomicUsize::new(0)); - let data = b"Hello".to_vec(); - let src_addr = SocketAddr::from_str("127.0.0.1:1234")?; - let dst_addr = SocketAddr::from_str("127.0.0.1:5678")?; - - let dummy_obs = Arc::new(Mutex::new(DummyObserver::default())); - let dummy_obs2 = Arc::clone(&dummy_obs); - let obs = dummy_obs2 as Arc>; - - let conn = Arc::new(UdpConn::new(src_addr, Some(dst_addr), obs)); - { - let mut dummy = dummy_obs.lock().await; - dummy.nclosed = Arc::clone(&nclosed); - dummy.read_ch_tx = conn.get_inbound_ch(); - } - - let conn_rx = Arc::clone(&conn); - let data_rx = data.clone(); - - let (rcvd_ch_tx, mut rcvd_ch_rx) = mpsc::channel(1); - let (done_ch_tx, mut done_ch_rx) = mpsc::channel::<()>(1); - - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - - loop { - let n = match conn_rx.recv(&mut buf).await { - Ok(n) => n, - Err(err) => { - log::debug!("conn closed. exiting the read loop with err {}", err); - break; - } - }; - - log::debug!("read data"); - assert_eq!(data_rx.len(), n, "should match"); - assert_eq!(&data_rx, &buf[..n], "should match"); - let _ = rcvd_ch_tx.send(()).await; - } - - drop(done_ch_tx); - }); - - let n = conn.send(&data).await.unwrap(); - assert_eq!(n, data.len(), "should match"); - - loop { - tokio::select! { - result = rcvd_ch_rx.recv() =>{ - if result.is_some(){ - log::debug!("closing soon..."); - conn.close().await?; - } - } - _ = done_ch_rx.recv() => { - log::debug!("recv done_ch_rx..."); - break; - } - } - } - - assert_eq!(1, nclosed.load(Ordering::SeqCst), "should be closed once"); - - Ok(()) -} diff --git a/util/src/vnet/conn_map.rs b/util/src/vnet/conn_map.rs deleted file mode 100644 index 27dca8268..000000000 --- a/util/src/vnet/conn_map.rs +++ /dev/null @@ -1,120 +0,0 @@ -#[cfg(test)] -mod conn_map_test; - -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; - -use tokio::sync::Mutex; - -use crate::error::*; -use crate::vnet::conn::UdpConn; -use crate::Conn; - -type PortMap = Mutex>>>; - -#[derive(Default)] -pub(crate) struct UdpConnMap { - port_map: PortMap, -} - -impl UdpConnMap { - pub(crate) fn new() -> Self { - UdpConnMap { - port_map: Mutex::new(HashMap::new()), - } - } - - pub(crate) async fn insert(&self, conn: Arc) -> Result<()> { - let addr = conn.local_addr()?; - - let mut port_map = self.port_map.lock().await; - if let Some(conns) = port_map.get(&addr.port()) { - if addr.ip().is_unspecified() { - return Err(Error::ErrAddressAlreadyInUse); - } - - for c in conns { - let laddr = c.local_addr()?; - if laddr.ip().is_unspecified() || laddr.ip() == addr.ip() { - return Err(Error::ErrAddressAlreadyInUse); - } - } - } - - if let Some(conns) = port_map.get_mut(&addr.port()) { - conns.push(conn); - } else { - port_map.insert(addr.port(), vec![conn]); - } - Ok(()) - } - - pub(crate) async fn find(&self, addr: &SocketAddr) -> Option> { - let port_map = self.port_map.lock().await; - if let Some(conns) = port_map.get(&addr.port()) { - if addr.ip().is_unspecified() { - // pick the first one appears in the iteration - if let Some(c) = conns.first() { - return Some(Arc::clone(c)); - } else { - return None; - } - } - - for c in conns { - let laddr = { - match c.local_addr() { - Ok(laddr) => laddr, - Err(_) => return None, - } - }; - if laddr.ip().is_unspecified() || laddr.ip() == addr.ip() { - return Some(Arc::clone(c)); - } - } - } - - None - } - - pub(crate) async fn delete(&self, addr: &SocketAddr) -> Result<()> { - let mut port_map = self.port_map.lock().await; - let mut new_conns = vec![]; - if let Some(conns) = port_map.get(&addr.port()) { - if !addr.ip().is_unspecified() { - for c in conns { - let laddr = c.local_addr()?; - if laddr.ip().is_unspecified() { - // This can't happen! - return Err(Error::ErrCannotRemoveUnspecifiedIp); - } - - if laddr.ip() == addr.ip() { - continue; - } - new_conns.push(Arc::clone(c)); - } - } - } else { - return Err(Error::ErrNoSuchUdpConn); - } - - if new_conns.is_empty() { - port_map.remove(&addr.port()); - } else { - port_map.insert(addr.port(), new_conns); - } - - Ok(()) - } - - pub(crate) async fn len(&self) -> usize { - let port_map = self.port_map.lock().await; - let mut n = 0; - for conns in port_map.values() { - n += conns.len(); - } - n - } -} diff --git a/util/src/vnet/conn_map/conn_map_test.rs b/util/src/vnet/conn_map/conn_map_test.rs deleted file mode 100644 index a3a6e7049..000000000 --- a/util/src/vnet/conn_map/conn_map_test.rs +++ /dev/null @@ -1,314 +0,0 @@ -use std::net::IpAddr; -use std::str::FromStr; - -use async_trait::async_trait; - -use super::*; -use crate::vnet::chunk::*; -use crate::vnet::conn::*; - -#[derive(Default)] -struct DummyObserver; - -#[async_trait] -impl ConnObserver for DummyObserver { - async fn write(&self, _c: Box) -> Result<()> { - Ok(()) - } - - async fn on_closed(&self, _addr: SocketAddr) {} - - fn determine_source_ip(&self, loc_ip: IpAddr, _dst_ip: IpAddr) -> Option { - Some(loc_ip) - } -} - -#[tokio::test] -async fn test_udp_conn_map_insert_remove() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in = Arc::new(UdpConn::new( - SocketAddr::from_str("127.0.0.1:1234")?, - None, - obs, - )); - - conn_map.insert(Arc::clone(&conn_in)).await?; - - let conn_out = conn_map.find(&conn_in.local_addr()?).await; - assert!(conn_out.is_some(), "should succeed"); - if let Some(conn_out) = conn_out { - assert_eq!( - conn_in.local_addr()?, - conn_out.local_addr()?, - "should match" - ); - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 1, "should match"); - } - - conn_map.delete(&conn_in.local_addr()?).await?; - { - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 0, "should match"); - } - - let result = conn_map.delete(&conn_in.local_addr()?).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_insert_0_remove() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in = Arc::new(UdpConn::new( - SocketAddr::from_str("0.0.0.0:1234")?, - None, - obs, - )); - - conn_map.insert(Arc::clone(&conn_in)).await?; - - let conn_out = conn_map.find(&conn_in.local_addr()?).await; - assert!(conn_out.is_some(), "should succeed"); - if let Some(conn_out) = conn_out { - assert_eq!( - conn_in.local_addr()?, - conn_out.local_addr()?, - "should match" - ); - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 1, "should match"); - } - - conn_map.delete(&conn_in.local_addr()?).await?; - { - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 0, "should match"); - } - - let result = conn_map.delete(&conn_in.local_addr()?).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_find_0() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in = Arc::new(UdpConn::new( - SocketAddr::from_str("0.0.0.0:1234")?, - None, - obs, - )); - - conn_map.insert(Arc::clone(&conn_in)).await?; - - let addr = SocketAddr::from_str("192.168.0.1:1234")?; - let conn_out = conn_map.find(&addr).await; - assert!(conn_out.is_some(), "should succeed"); - if let Some(conn_out) = conn_out { - let addr_in = conn_in.local_addr()?; - let addr_out = conn_out.local_addr()?; - assert_eq!(addr_in, addr_out, "should match"); - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 1, "should match"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_insert_many_ips_with_same_port() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in1 = Arc::new(UdpConn::new( - SocketAddr::from_str("10.1.2.1:5678")?, - None, - Arc::clone(&obs), - )); - - let conn_in2 = Arc::new(UdpConn::new( - SocketAddr::from_str("10.1.2.2:5678")?, - None, - Arc::clone(&obs), - )); - - conn_map.insert(Arc::clone(&conn_in1)).await?; - conn_map.insert(Arc::clone(&conn_in2)).await?; - - let addr1 = SocketAddr::from_str("10.1.2.1:5678")?; - let conn_out1 = conn_map.find(&addr1).await; - assert!(conn_out1.is_some(), "should succeed"); - if let Some(conn_out1) = conn_out1 { - let addr_in = conn_in1.local_addr()?; - let addr_out = conn_out1.local_addr()?; - assert_eq!(addr_in, addr_out, "should match"); - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 1, "should match"); - } - - let addr2 = SocketAddr::from_str("10.1.2.2:5678")?; - let conn_out2 = conn_map.find(&addr2).await; - assert!(conn_out2.is_some(), "should succeed"); - if let Some(conn_out2) = conn_out2 { - let addr_in = conn_in2.local_addr()?; - let addr_out = conn_out2.local_addr()?; - assert_eq!(addr_in, addr_out, "should match"); - let port_map = conn_map.port_map.lock().await; - assert_eq!(port_map.len(), 1, "should match"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_already_inuse_when_insert_0() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in1 = Arc::new(UdpConn::new( - SocketAddr::from_str("10.1.2.1:5678")?, - None, - Arc::clone(&obs), - )); - let conn_in2 = Arc::new(UdpConn::new( - SocketAddr::from_str("0.0.0.0:5678")?, - None, - Arc::clone(&obs), - )); - - conn_map.insert(Arc::clone(&conn_in1)).await?; - let result = conn_map.insert(Arc::clone(&conn_in2)).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_already_inuse_when_insert_a_specified_ip() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in1 = Arc::new(UdpConn::new( - SocketAddr::from_str("0.0.0.0:5678")?, - None, - Arc::clone(&obs), - )); - let conn_in2 = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - Arc::clone(&obs), - )); - - conn_map.insert(Arc::clone(&conn_in1)).await?; - let result = conn_map.insert(Arc::clone(&conn_in2)).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_already_inuse_when_insert_same_specified_ip() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in1 = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - Arc::clone(&obs), - )); - let conn_in2 = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - Arc::clone(&obs), - )); - - conn_map.insert(Arc::clone(&conn_in1)).await?; - let result = conn_map.insert(Arc::clone(&conn_in2)).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_find_failure_1() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - obs, - )); - - conn_map.insert(Arc::clone(&conn_in)).await?; - - let addr = SocketAddr::from_str("192.168.0.2:5678")?; - let result = conn_map.find(&addr).await; - assert!(result.is_none(), "should be none"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_find_failure_2() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - obs, - )); - - conn_map.insert(Arc::clone(&conn_in)).await?; - - let addr = SocketAddr::from_str("192.168.0.1:1234")?; - let result = conn_map.find(&addr).await; - assert!(result.is_none(), "should be none"); - - Ok(()) -} - -#[tokio::test] -async fn test_udp_conn_map_insert_two_on_same_port_then_remove() -> Result<()> { - let conn_map = UdpConnMap::new(); - - let obs: Arc> = Arc::new(Mutex::new(DummyObserver)); - - let conn_in1 = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.1:5678")?, - None, - Arc::clone(&obs), - )); - let conn_in2 = Arc::new(UdpConn::new( - SocketAddr::from_str("192.168.0.2:5678")?, - None, - Arc::clone(&obs), - )); - - conn_map.insert(Arc::clone(&conn_in1)).await?; - conn_map.insert(Arc::clone(&conn_in2)).await?; - - conn_map.delete(&conn_in1.local_addr()?).await?; - conn_map.delete(&conn_in2.local_addr()?).await?; - - Ok(()) -} diff --git a/util/src/vnet/interface.rs b/util/src/vnet/interface.rs deleted file mode 100644 index 7aac9e7e3..000000000 --- a/util/src/vnet/interface.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::net::SocketAddr; - -use ipnet::*; - -use crate::error::*; - -#[derive(Debug, Clone, Default)] -pub struct Interface { - pub(crate) name: String, - pub(crate) addrs: Vec, -} - -impl Interface { - pub fn new(name: String, addrs: Vec) -> Self { - Interface { name, addrs } - } - - pub fn add_addr(&mut self, addr: IpNet) { - self.addrs.push(addr); - } - - pub fn name(&self) -> &str { - &self.name - } - pub fn addrs(&self) -> &[IpNet] { - &self.addrs - } - - pub fn convert(addr: SocketAddr, mask: Option) -> Result { - if let Some(mask) = mask { - Ok(IpNet::with_netmask(addr.ip(), mask.ip()).map_err(|_| Error::ErrInvalidMask)?) - } else { - Ok(IpNet::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 }) - .expect("ipv4 should always work with prefix 32 and ipv6 with prefix 128")) - } - } -} diff --git a/util/src/vnet/mod.rs b/util/src/vnet/mod.rs deleted file mode 100644 index 41b7c459f..000000000 --- a/util/src/vnet/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod chunk; -pub(crate) mod chunk_queue; -pub(crate) mod conn; -pub(crate) mod conn_map; -pub mod interface; -pub mod nat; -pub mod net; -pub(crate) mod resolver; -pub mod router; diff --git a/util/src/vnet/nat.rs b/util/src/vnet/nat.rs deleted file mode 100644 index c4905d3b3..000000000 --- a/util/src/vnet/nat.rs +++ /dev/null @@ -1,464 +0,0 @@ -#[cfg(test)] -mod nat_test; - -use std::collections::{HashMap, HashSet}; -use std::net::IpAddr; -use std::ops::Add; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use portable_atomic::AtomicU16; -use tokio::sync::Mutex; -use tokio::time::Duration; - -use crate::error::*; -use crate::vnet::chunk::Chunk; -use crate::vnet::net::UDP_STR; - -const DEFAULT_NAT_MAPPING_LIFE_TIME: Duration = Duration::from_secs(30); - -// EndpointDependencyType defines a type of behavioral dependency on the -// remote endpoint's IP address or port number. This is used for the two -// kinds of behaviors: -// - Port Mapping behavior -// - Filtering behavior -// See: https://tools.ietf.org/html/rfc4787 -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum EndpointDependencyType { - // EndpointIndependent means the behavior is independent of the endpoint's address or port - #[default] - EndpointIndependent, - // EndpointAddrDependent means the behavior is dependent on the endpoint's address - EndpointAddrDependent, - // EndpointAddrPortDependent means the behavior is dependent on the endpoint's address and port - EndpointAddrPortDependent, -} - -// NATMode defines basic behavior of the NAT -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum NatMode { - // NATModeNormal means the NAT behaves as a standard NAPT (RFC 2663). - #[default] - Normal, - // NATModeNAT1To1 exhibits 1:1 DNAT where the external IP address is statically mapped to - // a specific local IP address with port number is preserved always between them. - // When this mode is selected, mapping_behavior, filtering_behavior, port_preservation and - // mapping_life_time of NATType are ignored. - Nat1To1, -} - -// NATType has a set of parameters that define the behavior of NAT. -#[derive(Default, Debug, Copy, Clone)] -pub struct NatType { - pub mode: NatMode, - pub mapping_behavior: EndpointDependencyType, - pub filtering_behavior: EndpointDependencyType, - pub hair_pining: bool, // Not implemented yet - pub port_preservation: bool, // Not implemented yet - pub mapping_life_time: Duration, -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct NatConfig { - pub(crate) name: String, - pub(crate) nat_type: NatType, - pub(crate) mapped_ips: Vec, // mapped IPv4 - pub(crate) local_ips: Vec, // local IPv4, required only when the mode is NATModeNAT1To1 -} - -#[derive(Debug, Clone)] -pub(crate) struct Mapping { - proto: String, // "udp" or "tcp" - local: String, // ":" - mapped: String, // ":" - bound: String, // key: "[[:]]" - filters: Arc>>, // key: "[[:]]" - expires: Arc>, // time to expire -} - -impl Default for Mapping { - fn default() -> Self { - Mapping { - proto: String::new(), // "udp" or "tcp" - local: String::new(), // ":" - mapped: String::new(), // ":" - bound: String::new(), // key: "[[:]]" - filters: Arc::new(Mutex::new(HashSet::new())), // key: "[[:]]" - expires: Arc::new(Mutex::new(SystemTime::now())), // time to expire - } - } -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct NetworkAddressTranslator { - pub(crate) name: String, - pub(crate) nat_type: NatType, - pub(crate) mapped_ips: Vec, // mapped IPv4 - pub(crate) local_ips: Vec, // local IPv4, required only when the mode is NATModeNAT1To1 - pub(crate) outbound_map: Arc>>>, // key: "::[:remote-ip[:remote-port]] - pub(crate) inbound_map: Arc>>>, // key: "::" - pub(crate) udp_port_counter: Arc, -} - -impl NetworkAddressTranslator { - pub(crate) fn new(config: NatConfig) -> Result { - let mut nat_type = config.nat_type; - - if nat_type.mode == NatMode::Nat1To1 { - // 1:1 NAT behavior - nat_type.mapping_behavior = EndpointDependencyType::EndpointIndependent; - nat_type.filtering_behavior = EndpointDependencyType::EndpointIndependent; - nat_type.port_preservation = true; - nat_type.mapping_life_time = Duration::from_secs(0); - - if config.mapped_ips.is_empty() { - return Err(Error::ErrNatRequiresMapping); - } - if config.mapped_ips.len() != config.local_ips.len() { - return Err(Error::ErrMismatchLengthIp); - } - } else { - // Normal (NAPT) behavior - nat_type.mode = NatMode::Normal; - if nat_type.mapping_life_time == Duration::from_secs(0) { - nat_type.mapping_life_time = DEFAULT_NAT_MAPPING_LIFE_TIME; - } - } - - Ok(NetworkAddressTranslator { - name: config.name, - nat_type, - mapped_ips: config.mapped_ips, - local_ips: config.local_ips, - outbound_map: Arc::new(Mutex::new(HashMap::new())), - inbound_map: Arc::new(Mutex::new(HashMap::new())), - udp_port_counter: Arc::new(AtomicU16::new(0)), - }) - } - - pub(crate) fn get_paired_mapped_ip(&self, loc_ip: &IpAddr) -> Option<&IpAddr> { - for (i, ip) in self.local_ips.iter().enumerate() { - if ip == loc_ip { - return self.mapped_ips.get(i); - } - } - None - } - - pub(crate) fn get_paired_local_ip(&self, mapped_ip: &IpAddr) -> Option<&IpAddr> { - for (i, ip) in self.mapped_ips.iter().enumerate() { - if ip == mapped_ip { - return self.local_ips.get(i); - } - } - None - } - - pub(crate) async fn translate_outbound( - &self, - from: &(dyn Chunk + Send + Sync), - ) -> Result>> { - let mut to = from.clone_to(); - - if from.network() == UDP_STR { - if self.nat_type.mode == NatMode::Nat1To1 { - // 1:1 NAT behavior - let src_addr = from.source_addr(); - if let Some(src_ip) = self.get_paired_mapped_ip(&src_addr.ip()) { - to.set_source_addr(&format!("{}:{}", src_ip, src_addr.port()))?; - } else { - log::debug!( - "[{}] drop outbound chunk {} with not route", - self.name, - from - ); - return Ok(None); // silently discard - } - } else { - // Normal (NAPT) behavior - let bound = match self.nat_type.mapping_behavior { - EndpointDependencyType::EndpointIndependent => "".to_owned(), - EndpointDependencyType::EndpointAddrDependent => { - from.get_destination_ip().to_string() - } - EndpointDependencyType::EndpointAddrPortDependent => { - from.destination_addr().to_string() - } - }; - - let filter_key = match self.nat_type.filtering_behavior { - EndpointDependencyType::EndpointIndependent => "".to_owned(), - EndpointDependencyType::EndpointAddrDependent => { - from.get_destination_ip().to_string() - } - EndpointDependencyType::EndpointAddrPortDependent => { - from.destination_addr().to_string() - } - }; - - let o_key = format!("udp:{}:{}", from.source_addr(), bound); - let name = self.name.clone(); - - let m_mapped = if let Some(m) = self.find_outbound_mapping(&o_key).await { - let mut filters = m.filters.lock().await; - if !filters.contains(&filter_key) { - log::debug!( - "[{}] permit access from {} to {}", - name, - filter_key, - m.mapped - ); - filters.insert(filter_key); - } - m.mapped.clone() - } else { - // Create a new Mapping - let udp_port_counter = self.udp_port_counter.load(Ordering::SeqCst); - let mapped_port = 0xC000 + udp_port_counter; - if udp_port_counter == 0xFFFF - 0xC000 { - self.udp_port_counter.store(0, Ordering::SeqCst); - } else { - self.udp_port_counter.fetch_add(1, Ordering::SeqCst); - } - - let m = if let Some(mapped_ips_first) = self.mapped_ips.first() { - Mapping { - proto: "udp".to_owned(), - local: from.source_addr().to_string(), - bound, - mapped: format!("{mapped_ips_first}:{mapped_port}"), - filters: Arc::new(Mutex::new(HashSet::new())), - expires: Arc::new(Mutex::new( - SystemTime::now().add(self.nat_type.mapping_life_time), - )), - } - } else { - return Err(Error::ErrNatRequiresMapping); - }; - - { - let mut outbound_map = self.outbound_map.lock().await; - outbound_map.insert(o_key.clone(), Arc::new(m.clone())); - } - - let i_key = format!("udp:{}", m.mapped); - - log::debug!( - "[{}] created a new NAT binding oKey={} i_key={}", - self.name, - o_key, - i_key - ); - log::debug!( - "[{}] permit access from {} to {}", - self.name, - filter_key, - m.mapped - ); - - { - let mut filters = m.filters.lock().await; - filters.insert(filter_key); - } - - let m_mapped = m.mapped.clone(); - { - let mut inbound_map = self.inbound_map.lock().await; - inbound_map.insert(i_key, Arc::new(m)); - } - m_mapped - }; - - to.set_source_addr(&m_mapped)?; - } - - log::debug!( - "[{}] translate outbound chunk from {} to {}", - self.name, - from, - to - ); - - return Ok(Some(to)); - } - - Err(Error::ErrNonUdpTranslationNotSupported) - } - - pub(crate) async fn translate_inbound( - &self, - from: &(dyn Chunk + Send + Sync), - ) -> Result>> { - let mut to = from.clone_to(); - - if from.network() == UDP_STR { - if self.nat_type.mode == NatMode::Nat1To1 { - // 1:1 NAT behavior - let dst_addr = from.destination_addr(); - if let Some(dst_ip) = self.get_paired_local_ip(&dst_addr.ip()) { - let dst_port = from.destination_addr().port(); - to.set_destination_addr(&format!("{dst_ip}:{dst_port}"))?; - } else { - return Err(Error::Other(format!( - "drop {from} as {:?}", - Error::ErrNoAssociatedLocalAddress - ))); - } - } else { - // Normal (NAPT) behavior - let filter_key = match self.nat_type.filtering_behavior { - EndpointDependencyType::EndpointIndependent => "".to_owned(), - EndpointDependencyType::EndpointAddrDependent => { - from.get_source_ip().to_string() - } - EndpointDependencyType::EndpointAddrPortDependent => { - from.source_addr().to_string() - } - }; - - let i_key = format!("udp:{}", from.destination_addr()); - if let Some(m) = self.find_inbound_mapping(&i_key).await { - { - let filters = m.filters.lock().await; - if !filters.contains(&filter_key) { - return Err(Error::Other(format!( - "drop {} as the remote {} {:?}", - from, - filter_key, - Error::ErrHasNoPermission - ))); - } - } - - // See RFC 4847 Section 4.3. Mapping Refresh - // a) Inbound refresh may be useful for applications with no outgoing - // UDP traffic. However, allowing inbound refresh may allow an - // external attacker or misbehaving application to keep a Mapping - // alive indefinitely. This may be a security risk. Also, if the - // process is repeated with different ports, over time, it could - // use up all the ports on the NAT. - - to.set_destination_addr(&m.local)?; - } else { - return Err(Error::Other(format!( - "drop {} as {:?}", - from, - Error::ErrNoNatBindingFound - ))); - } - } - - log::debug!( - "[{}] translate inbound chunk from {} to {}", - self.name, - from, - to - ); - - return Ok(Some(to)); - } - - Err(Error::ErrNonUdpTranslationNotSupported) - } - - // caller must hold the mutex - pub(crate) async fn find_outbound_mapping(&self, o_key: &str) -> Option> { - let mapping_life_time = self.nat_type.mapping_life_time; - let mut expired = false; - let (in_key, out_key) = { - let outbound_map = self.outbound_map.lock().await; - if let Some(m) = outbound_map.get(o_key) { - let now = SystemTime::now(); - - { - let mut expires = m.expires.lock().await; - // check if this Mapping is expired - if now.duration_since(*expires).is_ok() { - expired = true; - } else { - *expires = now.add(mapping_life_time); - } - } - ( - NetworkAddressTranslator::get_inbound_map_key(m), - NetworkAddressTranslator::get_outbound_map_key(m), - ) - } else { - (String::new(), String::new()) - } - }; - - if expired { - { - let mut inbound_map = self.inbound_map.lock().await; - inbound_map.remove(&in_key); - } - { - let mut outbound_map = self.outbound_map.lock().await; - outbound_map.remove(&out_key); - } - } - - let outbound_map = self.outbound_map.lock().await; - outbound_map.get(o_key).cloned() - } - - // caller must hold the mutex - pub(crate) async fn find_inbound_mapping(&self, i_key: &str) -> Option> { - let mut expired = false; - let (in_key, out_key) = { - let inbound_map = self.inbound_map.lock().await; - if let Some(m) = inbound_map.get(i_key) { - let now = SystemTime::now(); - - { - let expires = m.expires.lock().await; - // check if this Mapping is expired - if now.duration_since(*expires).is_ok() { - expired = true; - } - } - ( - NetworkAddressTranslator::get_inbound_map_key(m), - NetworkAddressTranslator::get_outbound_map_key(m), - ) - } else { - (String::new(), String::new()) - } - }; - - if expired { - { - let mut inbound_map = self.inbound_map.lock().await; - inbound_map.remove(&in_key); - } - { - let mut outbound_map = self.outbound_map.lock().await; - outbound_map.remove(&out_key); - } - } - - let inbound_map = self.inbound_map.lock().await; - inbound_map.get(i_key).cloned() - } - - // caller must hold the mutex - fn get_outbound_map_key(m: &Mapping) -> String { - format!("{}:{}:{}", m.proto, m.local, m.bound) - } - - fn get_inbound_map_key(m: &Mapping) -> String { - format!("{}:{}", m.proto, m.mapped) - } - - async fn inbound_map_len(&self) -> usize { - let inbound_map = self.inbound_map.lock().await; - inbound_map.len() - } - - async fn outbound_map_len(&self) -> usize { - let outbound_map = self.outbound_map.lock().await; - outbound_map.len() - } -} diff --git a/util/src/vnet/nat/nat_test.rs b/util/src/vnet/nat/nat_test.rs deleted file mode 100644 index 461db9491..000000000 --- a/util/src/vnet/nat/nat_test.rs +++ /dev/null @@ -1,638 +0,0 @@ -use std::net::SocketAddr; -use std::str::FromStr; - -use super::*; -use crate::vnet::chunk::ChunkUdp; - -// oic: outbound internal chunk -// oec: outbound external chunk -// iic: inbound internal chunk -// iec: inbound external chunk - -const DEMO_IP: &str = "1.2.3.4"; - -#[test] -fn test_nat_type_default() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - assert_eq!( - nat.nat_type.mapping_behavior, - EndpointDependencyType::EndpointIndependent, - "should match" - ); - assert_eq!( - nat.nat_type.filtering_behavior, - EndpointDependencyType::EndpointIndependent, - "should match" - ); - assert!(!nat.nat_type.hair_pining, "should be false"); - assert!(!nat.nat_type.port_preservation, "should be false"); - assert_eq!( - nat.nat_type.mapping_life_time, DEFAULT_NAT_MAPPING_LIFE_TIME, - "should be false" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_behavior_full_cone_nat() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointIndependent, - hair_pining: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - log::debug!("i-original : {}", iec); - - let iic = nat.translate_inbound(&iec).await?.unwrap(); - - log::debug!("i-translated: {}", iic); - - assert_eq!(oic.source_addr(), iic.destination_addr(), "should match"); - - // packet with dest addr that does not exist in the mapping table - // will be dropped - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port() + 1), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - // packet from any addr will be accepted (full-cone) - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), 7777), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_ok(), "should succeed"); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_behavior_addr_restricted_cone_nat() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointAddrDependent, - hair_pining: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - log::debug!("o-original : {}", oic); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - log::debug!("o-translated: {}", oec); - - // sending different (IP: 5.6.7.9) won't create a new mapping - let oic2 = ChunkUdp::new( - SocketAddr::from_str("192.168.0.2:1234")?, - SocketAddr::from_str("5.6.7.9:9000")?, - ); - let oec2 = nat.translate_outbound(&oic2).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - log::debug!("o-translated: {}", oec2); - - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - log::debug!("i-original : {}", iec); - - let iic = nat.translate_inbound(&iec).await?.unwrap(); - - log::debug!("i-translated: {}", iic); - - assert_eq!(oic.source_addr(), iic.destination_addr(), "should match"); - - // packet with dest addr that does not exist in the mapping table - // will be dropped - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port() + 1), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - // packet from any port will be accepted (restricted-cone) - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), 7777), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_ok(), "should succeed"); - - // packet from different addr will be dropped (restricted-cone) - let iec = ChunkUdp::new( - SocketAddr::from_str(&format!("{}:{}", "6.6.6.6", dst.port()))?, - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_behavior_port_restricted_cone_nat() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointAddrPortDependent, - hair_pining: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - log::debug!("o-original : {}", oic); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - log::debug!("o-translated: {}", oec); - - // sending different (IP: 5.6.7.9) won't create a new mapping - let oic2 = ChunkUdp::new( - SocketAddr::from_str("192.168.0.2:1234")?, - SocketAddr::from_str("5.6.7.9:9000")?, - ); - let oec2 = nat.translate_outbound(&oic2).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - log::debug!("o-translated: {}", oec2); - - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - log::debug!("i-original : {}", iec); - - let iic = nat.translate_inbound(&iec).await?.unwrap(); - - log::debug!("i-translated: {}", iic); - - assert_eq!(oic.source_addr(), iic.destination_addr(), "should match"); - - // packet with dest addr that does not exist in the mapping table - // will be dropped - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port() + 1), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - // packet from different port will be dropped (port-restricted-cone) - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), 7777), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - // packet from different addr will be dropped (restricted-cone) - let iec = ChunkUdp::new( - SocketAddr::from_str(&format!("{}:{}", "6.6.6.6", dst.port()))?, - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should fail (dropped)"); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_behavior_symmetric_nat_addr_dependent_mapping() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointAddrDependent, - filtering_behavior: EndpointDependencyType::EndpointAddrDependent, - hair_pining: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst1 = SocketAddr::from_str("5.6.7.8:5678")?; - let dst2 = SocketAddr::from_str("5.6.7.100:5678")?; - let dst3 = SocketAddr::from_str("5.6.7.8:6000")?; - - let oic1 = ChunkUdp::new(src, dst1); - let oic2 = ChunkUdp::new(src, dst2); - let oic3 = ChunkUdp::new(src, dst3); - - log::debug!("o-original : {}", oic1); - log::debug!("o-original : {}", oic2); - log::debug!("o-original : {}", oic3); - - let oec1 = nat.translate_outbound(&oic1).await?.unwrap(); - let oec2 = nat.translate_outbound(&oic2).await?.unwrap(); - let oec3 = nat.translate_outbound(&oic3).await?.unwrap(); - - assert_eq!(nat.outbound_map_len().await, 2, "should match"); - assert_eq!(nat.inbound_map_len().await, 2, "should match"); - - log::debug!("o-translated: {}", oec1); - log::debug!("o-translated: {}", oec2); - log::debug!("o-translated: {}", oec3); - - assert_ne!( - oec1.source_addr().port(), - oec2.source_addr().port(), - "should not match" - ); - assert_eq!( - oec1.source_addr().port(), - oec3.source_addr().port(), - "should match" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_behavior_symmetric_nat_port_dependent_mapping() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointAddrPortDependent, - filtering_behavior: EndpointDependencyType::EndpointAddrPortDependent, - hair_pining: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst1 = SocketAddr::from_str("5.6.7.8:5678")?; - let dst2 = SocketAddr::from_str("5.6.7.100:5678")?; - let dst3 = SocketAddr::from_str("5.6.7.8:6000")?; - - let oic1 = ChunkUdp::new(src, dst1); - let oic2 = ChunkUdp::new(src, dst2); - let oic3 = ChunkUdp::new(src, dst3); - - log::debug!("o-original : {}", oic1); - log::debug!("o-original : {}", oic2); - log::debug!("o-original : {}", oic3); - - let oec1 = nat.translate_outbound(&oic1).await?.unwrap(); - let oec2 = nat.translate_outbound(&oic2).await?.unwrap(); - let oec3 = nat.translate_outbound(&oic3).await?.unwrap(); - - assert_eq!(nat.outbound_map_len().await, 3, "should match"); - assert_eq!(nat.inbound_map_len().await, 3, "should match"); - - log::debug!("o-translated: {}", oec1); - log::debug!("o-translated: {}", oec2); - log::debug!("o-translated: {}", oec3); - - assert_ne!( - oec1.source_addr().port(), - oec2.source_addr().port(), - "should not match" - ); - assert_ne!( - oec1.source_addr().port(), - oec3.source_addr().port(), - "should match" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_timeout_refresh_on_outbound() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointIndependent, - hair_pining: false, - mapping_life_time: Duration::from_millis(200), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - // record mapped addr - let mapped = oec.source_addr().to_string(); - - tokio::time::sleep(Duration::from_millis(5)).await; - - // refresh - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - assert_eq!( - mapped, - oec.source_addr().to_string(), - "mapped addr should match" - ); - - // sleep long enough for the mapping to expire - tokio::time::sleep(Duration::from_millis(225)).await; - - // refresh after expiration - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - assert_ne!( - oec.source_addr().to_string(), - mapped, - "mapped addr should not match" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_nat_mapping_timeout_outbound_detects_timeout() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointIndependent, - hair_pining: false, - mapping_life_time: Duration::from_millis(100), - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("192.168.0.2:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 1, "should match"); - assert_eq!(nat.inbound_map_len().await, 1, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - // sleep long enough for the mapping to expire - tokio::time::sleep(Duration::from_millis(125)).await; - - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - log::debug!("i-original : {}", iec); - - let result = nat.translate_inbound(&iec).await; - assert!(result.is_err(), "should drop"); - assert_eq!(nat.outbound_map_len().await, 0, "should match"); - assert_eq!(nat.inbound_map_len().await, 0, "should match"); - - Ok(()) -} - -#[tokio::test] -async fn test_nat1to1_behavior_one_mapping() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - local_ips: vec![IpAddr::from_str("10.0.0.1")?], - ..Default::default() - })?; - - let src = SocketAddr::from_str("10.0.0.1:1234")?; - let dst = SocketAddr::from_str("5.6.7.8:5678")?; - - let oic = ChunkUdp::new(src, dst); - - let oec = nat.translate_outbound(&oic).await?.unwrap(); - assert_eq!(nat.outbound_map_len().await, 0, "should match"); - assert_eq!(nat.inbound_map_len().await, 0, "should match"); - - log::debug!("o-original : {}", oic); - log::debug!("o-translated: {}", oec); - - assert_eq!( - "1.2.3.4:1234", - oec.source_addr().to_string(), - "should match" - ); - - let iec = ChunkUdp::new( - SocketAddr::new(dst.ip(), dst.port()), - SocketAddr::new(oec.source_addr().ip(), oec.source_addr().port()), - ); - - log::debug!("i-original : {}", iec); - - let iic = nat.translate_inbound(&iec).await?.unwrap(); - - log::debug!("i-translated: {}", iic); - - assert_eq!(oic.source_addr(), iic.destination_addr(), "should match"); - - Ok(()) -} - -#[tokio::test] -async fn test_nat1to1_behavior_more_mapping() -> Result<()> { - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?, IpAddr::from_str("1.2.3.5")?], - local_ips: vec![IpAddr::from_str("10.0.0.1")?, IpAddr::from_str("10.0.0.2")?], - ..Default::default() - })?; - - // outbound translation - - let before = ChunkUdp::new( - SocketAddr::from_str("10.0.0.1:1234")?, - SocketAddr::from_str("5.6.7.8:5678")?, - ); - - let after = nat.translate_outbound(&before).await?.unwrap(); - assert_eq!( - after.source_addr().to_string(), - "1.2.3.4:1234", - "should match" - ); - - let before = ChunkUdp::new( - SocketAddr::from_str("10.0.0.2:1234")?, - SocketAddr::from_str("5.6.7.8:5678")?, - ); - - let after = nat.translate_outbound(&before).await?.unwrap(); - assert_eq!( - after.source_addr().to_string(), - "1.2.3.5:1234", - "should match" - ); - - // inbound translation - - let before = ChunkUdp::new( - SocketAddr::from_str("5.6.7.8:5678")?, - SocketAddr::from_str(&format!("{}:{}", DEMO_IP, 2525))?, - ); - - let after = nat.translate_inbound(&before).await?.unwrap(); - assert_eq!( - after.destination_addr().to_string(), - "10.0.0.1:2525", - "should match" - ); - - let before = ChunkUdp::new( - SocketAddr::from_str("5.6.7.8:5678")?, - SocketAddr::from_str("1.2.3.5:9847")?, - ); - - let after = nat.translate_inbound(&before).await?.unwrap(); - assert_eq!( - after.destination_addr().to_string(), - "10.0.0.2:9847", - "should match" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_nat1to1_behavior_failure() -> Result<()> { - // 1:1 NAT requires more than one mapping - let result = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }, - ..Default::default() - }); - assert!(result.is_err(), "should fail"); - - // 1:1 NAT requires the same number of mappedIPs and localIPs - let result = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?, IpAddr::from_str("1.2.3.5")?], - local_ips: vec![IpAddr::from_str("10.0.0.1")?], - ..Default::default() - }); - assert!(result.is_err(), "should fail"); - - // drop outbound or inbound chunk with no route in 1:1 NAT - let nat = NetworkAddressTranslator::new(NatConfig { - nat_type: NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }, - mapped_ips: vec![IpAddr::from_str(DEMO_IP)?], - local_ips: vec![IpAddr::from_str("10.0.0.1")?], - ..Default::default() - })?; - - let before = ChunkUdp::new( - SocketAddr::from_str("10.0.0.2:1234")?, // no external mapping for this - SocketAddr::from_str("5.6.7.8:5678")?, - ); - - let after = nat.translate_outbound(&before).await?; - assert!(after.is_none(), "should be nil"); - - let before = ChunkUdp::new( - SocketAddr::from_str("5.6.7.8:5678")?, - SocketAddr::from_str("10.0.0.2:1234")?, // no local mapping for this - ); - - let result = nat.translate_inbound(&before).await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} diff --git a/util/src/vnet/net.rs b/util/src/vnet/net.rs deleted file mode 100644 index 5586e59b7..000000000 --- a/util/src/vnet/net.rs +++ /dev/null @@ -1,566 +0,0 @@ -#[cfg(test)] -mod net_test; - -use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use ipnet::IpNet; -use portable_atomic::AtomicU64; -use tokio::net::UdpSocket; -use tokio::sync::Mutex; - -use super::conn_map::*; -use super::interface::*; -use crate::error::*; -use crate::vnet::chunk::Chunk; -use crate::vnet::conn::{ConnObserver, UdpConn}; -use crate::vnet::router::*; -use crate::{conn, ifaces, Conn}; - -pub(crate) const LO0_STR: &str = "lo0"; -pub(crate) const UDP_STR: &str = "udp"; - -lazy_static! { - pub static ref MAC_ADDR_COUNTER: AtomicU64 = AtomicU64::new(0xBEEFED910200); -} - -pub(crate) type HardwareAddr = Vec; - -pub(crate) fn new_mac_address() -> HardwareAddr { - let b = MAC_ADDR_COUNTER - .fetch_add(1, Ordering::SeqCst) - .to_be_bytes(); - b[2..].to_vec() -} - -#[derive(Default)] -pub(crate) struct VNetInternal { - pub(crate) interfaces: Vec, // read-only - pub(crate) router: Option>>, // read-only - pub(crate) udp_conns: UdpConnMap, // read-only -} - -impl VNetInternal { - fn get_interface(&self, ifc_name: &str) -> Option<&Interface> { - self.interfaces.iter().find(|ifc| ifc.name == ifc_name) - } -} - -#[async_trait] -impl ConnObserver for VNetInternal { - async fn write(&self, c: Box) -> Result<()> { - if c.network() == UDP_STR && c.get_destination_ip().is_loopback() { - if let Some(conn) = self.udp_conns.find(&c.destination_addr()).await { - let read_ch_tx = conn.get_inbound_ch(); - let ch_tx = read_ch_tx.lock().await; - if let Some(tx) = &*ch_tx { - let _ = tx.send(c).await; - } - } - return Ok(()); - } - - if let Some(r) = &self.router { - let p = r.lock().await; - p.push(c).await; - Ok(()) - } else { - Err(Error::ErrNoRouterLinked) - } - } - - async fn on_closed(&self, addr: SocketAddr) { - let _ = self.udp_conns.delete(&addr).await; - } - - // This method determines the srcIP based on the dstIP when locIP - // is any IP address ("0.0.0.0" or "::"). If locIP is a non-any addr, - // this method simply returns locIP. - // caller must hold the mutex - fn determine_source_ip(&self, loc_ip: IpAddr, dst_ip: IpAddr) -> Option { - if !loc_ip.is_unspecified() { - return Some(loc_ip); - } - - if dst_ip.is_loopback() { - let src_ip = if let Ok(src_ip) = IpAddr::from_str("127.0.0.1") { - Some(src_ip) - } else { - None - }; - return src_ip; - } - - if let Some(ifc) = self.get_interface("eth0") { - for ipnet in ifc.addrs() { - if (ipnet.addr().is_ipv4() && loc_ip.is_ipv4()) - || (ipnet.addr().is_ipv6() && loc_ip.is_ipv6()) - { - return Some(ipnet.addr()); - } - } - } - - None - } -} - -#[derive(Default)] -pub struct VNet { - pub(crate) interfaces: Vec, // read-only - pub(crate) static_ips: Vec, // read-only - pub(crate) vi: Arc>, -} - -#[async_trait] -impl Nic for VNet { - async fn get_interface(&self, ifc_name: &str) -> Option { - for ifc in &self.interfaces { - if ifc.name == ifc_name { - return Some(ifc.clone()); - } - } - None - } - - async fn add_addrs_to_interface(&mut self, ifc_name: &str, addrs: &[IpNet]) -> Result<()> { - { - let mut vi = self.vi.lock().await; - for ifc in &mut vi.interfaces { - if ifc.name == ifc_name { - for addr in addrs { - ifc.add_addr(*addr); - } - break; - } - } - } - - for ifc in &mut self.interfaces { - if ifc.name == ifc_name { - for addr in addrs { - ifc.add_addr(*addr); - } - return Ok(()); - } - } - - Err(Error::ErrNotFound) - } - - async fn set_router(&self, r: Arc>) -> Result<()> { - let mut vi = self.vi.lock().await; - vi.router = Some(r); - - Ok(()) - } - - async fn on_inbound_chunk(&self, c: Box) { - if c.network() == UDP_STR { - let vi = self.vi.lock().await; - if let Some(conn) = vi.udp_conns.find(&c.destination_addr()).await { - let read_ch_tx = conn.get_inbound_ch(); - let ch_tx = read_ch_tx.lock().await; - if let Some(tx) = &*ch_tx { - let _ = tx.send(c).await; - } - } - } - } - - async fn get_static_ips(&self) -> Vec { - self.static_ips.clone() - } -} - -impl VNet { - pub(crate) fn get_interfaces(&self) -> &[Interface] { - &self.interfaces - } - - // caller must hold the mutex - pub(crate) fn get_all_ipaddrs(&self, ipv6: bool) -> Vec { - let mut ips = vec![]; - - for ifc in &self.interfaces { - for ipnet in ifc.addrs() { - if (ipv6 && ipnet.addr().is_ipv6()) || (!ipv6 && ipnet.addr().is_ipv4()) { - ips.push(ipnet.addr()); - } - } - } - - ips - } - - // caller must hold the mutex - pub(crate) fn has_ipaddr(&self, ip: IpAddr) -> bool { - for ifc in &self.interfaces { - for ipnet in ifc.addrs() { - let loc_ip = ipnet.addr(); - - match ip.to_string().as_str() { - "0.0.0.0" => { - if loc_ip.is_ipv4() { - return true; - } - } - "::" => { - if loc_ip.is_ipv6() { - return true; - } - } - _ => { - if loc_ip == ip { - return true; - } - } - } - } - } - - false - } - - // caller must hold the mutex - pub(crate) async fn allocate_local_addr(&self, ip: IpAddr, port: u16) -> Result<()> { - // gather local IP addresses to bind - let mut ips = vec![]; - if ip.is_unspecified() { - ips = self.get_all_ipaddrs(ip.is_ipv6()); - } else if self.has_ipaddr(ip) { - ips.push(ip); - } - - if ips.is_empty() { - return Err(Error::ErrBindFailed); - } - - // check if all these transport addresses are not in use - for ip2 in ips { - let addr = SocketAddr::new(ip2, port); - let vi = self.vi.lock().await; - if vi.udp_conns.find(&addr).await.is_some() { - return Err(Error::ErrAddressAlreadyInUse); - } - } - - Ok(()) - } - - // caller must hold the mutex - pub(crate) async fn assign_port(&self, ip: IpAddr, start: u16, end: u16) -> Result { - // choose randomly from the range between start and end (inclusive) - if end < start { - return Err(Error::ErrEndPortLessThanStart); - } - - let space = end + 1 - start; - let offset = rand::random::() % space; - for i in 0..space { - let port = ((offset + i) % space) + start; - let result = self.allocate_local_addr(ip, port).await; - if result.is_ok() { - return Ok(port); - } - } - - Err(Error::ErrPortSpaceExhausted) - } - - pub(crate) async fn resolve_addr(&self, use_ipv4: bool, address: &str) -> Result { - let v: Vec<&str> = address.splitn(2, ':').collect(); - if v.len() != 2 { - return Err(Error::ErrAddrNotUdpAddr); - } - let (host, port) = (v[0], v[1]); - - // Check if host is a domain name - let ip: IpAddr = match host.parse() { - Ok(ip) => ip, - Err(_) => { - let host = host.to_lowercase(); - if host == "localhost" { - if use_ipv4 { - Ipv4Addr::new(127, 0, 0, 1).into() - } else { - Ipv6Addr::from_str("::1")?.into() - } - } else { - // host is a domain name. resolve IP address by the name - let vi = self.vi.lock().await; - if let Some(router) = &vi.router { - let r = router.lock().await; - let resolver = r.resolver.lock().await; - if let Some(ip) = resolver.lookup(host).await { - ip - } else { - return Err(Error::ErrNotFound); - } - } else { - return Err(Error::ErrNoRouterLinked); - } - } - } - }; - - let port: u16 = port.parse()?; - - let remote_addr = SocketAddr::new(ip, port); - if (use_ipv4 && remote_addr.is_ipv4()) || (!use_ipv4 && remote_addr.is_ipv6()) { - Ok(remote_addr) - } else { - Err(Error::Other(format!( - "No available {} IP address found!", - if use_ipv4 { "ipv4" } else { "ipv6" }, - ))) - } - } - - // caller must hold the mutex - pub(crate) async fn bind( - &self, - mut local_addr: SocketAddr, - ) -> Result> { - // validate address. do we have that address? - if !self.has_ipaddr(local_addr.ip()) { - return Err(Error::ErrCantAssignRequestedAddr); - } - - if local_addr.port() == 0 { - // choose randomly from the range between 5000 and 5999 - local_addr.set_port(self.assign_port(local_addr.ip(), 5000, 5999).await?); - } else { - let vi = self.vi.lock().await; - if vi.udp_conns.find(&local_addr).await.is_some() { - return Err(Error::ErrAddressAlreadyInUse); - } - } - - let v = Arc::clone(&self.vi) as Arc>; - let conn = Arc::new(UdpConn::new(local_addr, None, v)); - - { - let vi = self.vi.lock().await; - vi.udp_conns.insert(Arc::clone(&conn)).await?; - } - - Ok(conn) - } - - pub(crate) async fn dail( - &self, - use_ipv4: bool, - remote_addr: &str, - ) -> Result> { - let rem_addr = self.resolve_addr(use_ipv4, remote_addr).await?; - - // Determine source address - let src_ip = { - let vi = self.vi.lock().await; - let any_ip = if use_ipv4 { - Ipv4Addr::new(0, 0, 0, 0).into() - } else { - Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into() - }; - if let Some(src_ip) = vi.determine_source_ip(any_ip, rem_addr.ip()) { - src_ip - } else { - any_ip - } - }; - - let loc_addr = SocketAddr::new(src_ip, 0); - - let conn = self.bind(loc_addr).await?; - conn.connect(rem_addr).await?; - - Ok(conn) - } -} - -// NetConfig is a bag of configuration parameters passed to NewNet(). -#[derive(Debug, Default)] -pub struct NetConfig { - // static_ips is an array of static IP addresses to be assigned for this Net. - // If no static IP address is given, the router will automatically assign - // an IP address. - pub static_ips: Vec, - - // static_ip is deprecated. Use static_ips. - pub static_ip: String, -} - -// Net represents a local network stack equivalent to a set of layers from NIC -// up to the transport (UDP / TCP) layer. -pub enum Net { - VNet(Arc>), - Ifs(Vec), -} - -impl Net { - // NewNet creates an instance of Net. - // If config is nil, the virtual network is disabled. (uses corresponding - // net.Xxxx() operations. - // By design, it always have lo0 and eth0 interfaces. - // The lo0 has the address 127.0.0.1 assigned by default. - // IP address for eth0 will be assigned when this Net is added to a router. - pub fn new(config: Option) -> Self { - if let Some(config) = config { - let mut lo0 = Interface::new(LO0_STR.to_owned(), vec![]); - if let Ok(ipnet) = Interface::convert( - SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 0), - Some(SocketAddr::new(Ipv4Addr::new(255, 0, 0, 0).into(), 0)), - ) { - lo0.add_addr(ipnet); - } - - let eth0 = Interface::new("eth0".to_owned(), vec![]); - - let mut static_ips = vec![]; - for ip_str in &config.static_ips { - if let Ok(ip) = IpAddr::from_str(ip_str) { - static_ips.push(ip); - } - } - if !config.static_ip.is_empty() { - if let Ok(ip) = IpAddr::from_str(&config.static_ip) { - static_ips.push(ip); - } - } - - let vnet = VNet { - interfaces: vec![lo0.clone(), eth0.clone()], - static_ips, - vi: Arc::new(Mutex::new(VNetInternal { - interfaces: vec![lo0, eth0], - router: None, - udp_conns: UdpConnMap::new(), - })), - }; - - Net::VNet(Arc::new(Mutex::new(vnet))) - } else { - let interfaces = match ifaces::ifaces() { - Ok(ifs) => ifs, - Err(_) => vec![], - }; - - let mut m: HashMap> = HashMap::new(); - for iface in interfaces { - if let Some(addrs) = m.get_mut(&iface.name) { - if let Some(addr) = iface.addr { - if let Ok(inet) = Interface::convert(addr, iface.mask) { - addrs.push(inet); - } - } - } else if let Some(addr) = iface.addr { - if let Ok(inet) = Interface::convert(addr, iface.mask) { - m.insert(iface.name, vec![inet]); - } - } - } - - let mut ifs = vec![]; - for (name, addrs) in m.into_iter() { - ifs.push(Interface::new(name, addrs)); - } - - Net::Ifs(ifs) - } - } - - // Interfaces returns a list of the system's network interfaces. - pub async fn get_interfaces(&self) -> Vec { - match self { - Net::VNet(vnet) => { - let net = vnet.lock().await; - net.get_interfaces().to_vec() - } - Net::Ifs(ifs) => ifs.clone(), - } - } - - // InterfaceByName returns the interface specified by name. - pub async fn get_interface(&self, ifc_name: &str) -> Option { - match self { - Net::VNet(vnet) => { - let net = vnet.lock().await; - net.get_interface(ifc_name).await - } - Net::Ifs(ifs) => { - for ifc in ifs { - if ifc.name == ifc_name { - return Some(ifc.clone()); - } - } - None - } - } - } - - // IsVirtual tests if the virtual network is enabled. - pub fn is_virtual(&self) -> bool { - match self { - Net::VNet(_) => true, - Net::Ifs(_) => false, - } - } - - pub async fn resolve_addr(&self, use_ipv4: bool, address: &str) -> Result { - match self { - Net::VNet(vnet) => { - let net = vnet.lock().await; - net.resolve_addr(use_ipv4, address).await - } - Net::Ifs(_) => Ok(conn::lookup_host(use_ipv4, address).await?), - } - } - - pub async fn bind(&self, addr: SocketAddr) -> Result> { - match self { - Net::VNet(vnet) => { - let net = vnet.lock().await; - net.bind(addr).await - } - Net::Ifs(_) => Ok(Arc::new(UdpSocket::bind(addr).await?)), - } - } - - pub async fn dail( - &self, - use_ipv4: bool, - remote_addr: &str, - ) -> Result> { - match self { - Net::VNet(vnet) => { - let net = vnet.lock().await; - net.dail(use_ipv4, remote_addr).await - } - Net::Ifs(_) => { - let any_ip = if use_ipv4 { - Ipv4Addr::new(0, 0, 0, 0).into() - } else { - Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into() - }; - let local_addr = SocketAddr::new(any_ip, 0); - - let conn = UdpSocket::bind(local_addr).await?; - conn.connect(remote_addr).await?; - - Ok(Arc::new(conn)) - } - } - } - - pub fn get_nic(&self) -> Result>> { - match self { - Net::VNet(vnet) => Ok(Arc::clone(vnet) as Arc>), - Net::Ifs(_) => Err(Error::ErrVnetDisabled), - } - } -} diff --git a/util/src/vnet/net/net_test.rs b/util/src/vnet/net/net_test.rs deleted file mode 100644 index d24002290..000000000 --- a/util/src/vnet/net/net_test.rs +++ /dev/null @@ -1,903 +0,0 @@ -use tokio::sync::{broadcast, mpsc}; - -use super::*; -use crate::vnet::chunk::ChunkUdp; - -const DEMO_IP: &str = "1.2.3.4"; - -#[derive(Default)] -struct DummyObserver; - -#[async_trait] -impl ConnObserver for DummyObserver { - async fn write(&self, _c: Box) -> Result<()> { - Ok(()) - } - - async fn on_closed(&self, _addr: SocketAddr) {} - - fn determine_source_ip(&self, loc_ip: IpAddr, _dst_ip: IpAddr) -> Option { - Some(loc_ip) - } -} - -#[tokio::test] -async fn test_net_native_interfaces() -> Result<()> { - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - let interfaces = nw.get_interfaces().await; - log::debug!("interfaces: {:?}", interfaces); - for ifc in interfaces { - let addrs = ifc.addrs(); - for addr in addrs { - log::debug!("{}", addr) - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_net_native_resolve_addr() -> Result<()> { - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - let udp_addr = nw.resolve_addr(true, "localhost:1234").await?; - assert_eq!(udp_addr.ip().to_string(), "127.0.0.1", "should match"); - assert_eq!(udp_addr.port(), 1234, "should match"); - - let result = nw.resolve_addr(false, "127.0.0.1:1234").await; - assert!(result.is_err(), "should not match"); - - Ok(()) -} - -#[tokio::test] -async fn test_net_native_bind() -> Result<()> { - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - let conn = nw.bind(SocketAddr::from_str("127.0.0.1:0")?).await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string(), - "127.0.0.1", - "local_addr ip should match 127.0.0.1" - ); - log::debug!("laddr: {}", laddr); - - Ok(()) -} - -#[tokio::test] -async fn test_net_native_dail() -> Result<()> { - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - let conn = nw.dail(true, "127.0.0.1:1234").await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string(), - "127.0.0.1", - "local_addr should match 127.0.0.1" - ); - assert_ne!(laddr.port(), 1234, "local_addr port should match 1234"); - log::debug!("laddr: {}", laddr); - - Ok(()) -} - -#[tokio::test] -async fn test_net_native_loopback() -> Result<()> { - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - let conn = nw.bind(SocketAddr::from_str("127.0.0.1:0")?).await?; - let laddr = conn.local_addr()?; - - let msg = "PING!"; - let n = conn.send_to(msg.as_bytes(), laddr).await?; - assert_eq!(n, msg.len(), "should match msg size {}", msg.len()); - - let mut buf = vec![0u8; 1000]; - let (n, raddr) = conn.recv_from(&mut buf).await?; - assert_eq!(n, msg.len(), "should match msg size {}", msg.len()); - assert_eq!(&buf[..n], msg.as_bytes(), "should match msg content {msg}"); - assert_eq!(laddr, raddr, "should match addr {laddr}"); - - Ok(()) -} - -#[tokio::test] -async fn test_net_native_unexpected_operations() -> Result<()> { - let mut lo_name = String::new(); - let ifcs = ifaces::ifaces()?; - for ifc in &ifcs { - if let Some(addr) = ifc.addr { - if addr.ip().is_loopback() { - lo_name.clone_from(&ifc.name); - break; - } - } - } - - let nw = Net::new(None); - assert!(!nw.is_virtual(), "should be false"); - - if !lo_name.is_empty() { - if let Some(ifc) = nw.get_interface(&lo_name).await { - assert_eq!(ifc.name, lo_name, "should match ifc name"); - } else { - panic!("should succeed"); - } - } - - let result = nw.get_interface("foo0").await; - assert!(result.is_none(), "should be none"); - - //let ips = nw.get_static_ips(); - //assert!(ips.is_empty(), "should empty"); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_interfaces() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let interfaces = nw.get_interfaces().await; - assert_eq!(2, interfaces.len(), "should be one interface"); - - for ifc in interfaces { - match ifc.name.as_str() { - LO0_STR => { - let addrs = ifc.addrs(); - assert_eq!(addrs.len(), 1, "should be one address"); - } - "eth0" => { - let addrs = ifc.addrs(); - assert!(addrs.is_empty(), "should empty"); - } - _ => { - panic!("unknown interface: {}", ifc.name); - } - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_interface_by_name() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let interfaces = nw.get_interfaces().await; - assert_eq!(2, interfaces.len(), "should be one interface"); - - let nic = nw.get_nic()?; - let nic = nic.lock().await; - if let Some(ifc) = nic.get_interface(LO0_STR).await { - assert_eq!(ifc.name.as_str(), LO0_STR, "should match"); - let addrs = ifc.addrs(); - assert_eq!(addrs.len(), 1, "should be one address"); - } else { - panic!("should got ifc"); - } - - if let Some(ifc) = nic.get_interface("eth0").await { - assert_eq!(ifc.name.as_str(), "eth0", "should match"); - let addrs = ifc.addrs(); - assert!(addrs.is_empty(), "should empty"); - } else { - panic!("should got ifc"); - } - - let result = nic.get_interface("foo0").await; - assert!(result.is_none(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_has_ipaddr() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let interfaces = nw.get_interfaces().await; - assert_eq!(interfaces.len(), 2, "should be one interface"); - - { - let nic = nw.get_nic()?; - let mut nic = nic.lock().await; - let ipnet = IpNet::from_str("10.1.2.3/24")?; - nic.add_addrs_to_interface("eth0", &[ipnet]).await?; - - if let Some(ifc) = nic.get_interface("eth0").await { - let addrs = ifc.addrs(); - assert!(!addrs.is_empty(), "should not empty"); - } - } - - if let Net::VNet(vnet) = &nw { - let net = vnet.lock().await; - let ip = Ipv4Addr::from_str("127.0.0.1")?.into(); - assert!(net.has_ipaddr(ip), "the IP addr {ip} should exist"); - - let ip = Ipv4Addr::from_str("10.1.2.3")?.into(); - assert!(net.has_ipaddr(ip), "the IP addr {ip} should exist"); - - let ip = Ipv4Addr::from_str("192.168.1.1")?.into(); - assert!(!net.has_ipaddr(ip), "the IP addr {ip} should exist"); - } - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_get_all_ipaddrs() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let interfaces = nw.get_interfaces().await; - assert_eq!(interfaces.len(), 2, "should be one interface"); - - { - let nic = nw.get_nic()?; - let mut nic = nic.lock().await; - let ipnet = IpNet::from_str("10.1.2.3/24")?; - nic.add_addrs_to_interface("eth0", &[ipnet]).await?; - - if let Some(ifc) = nic.get_interface("eth0").await { - let addrs = ifc.addrs(); - assert!(!addrs.is_empty(), "should not empty"); - } - } - - if let Net::VNet(vnet) = &nw { - let net = vnet.lock().await; - let ips = net.get_all_ipaddrs(false); - assert_eq!(ips.len(), 2, "ips should match size {} == 2", ips.len()) - } - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_assign_port() -> Result<()> { - let mut nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let addr = DEMO_IP; - let start = 1000u16; - let end = 1002u16; - let space = end + 1 - start; - - let interfaces = nw.get_interfaces().await; - assert_eq!(interfaces.len(), 2, "should be one interface"); - - { - let nic = nw.get_nic()?; - let mut nic = nic.lock().await; - let ipnet = IpNet::from_str(&format!("{addr}/24"))?; - nic.add_addrs_to_interface("eth0", &[ipnet]).await?; - } - - if let Net::VNet(vnet) = &mut nw { - let vnet = vnet.lock().await; - // attempt to assign port with start > end should fail - let ip = IpAddr::from_str(addr)?; - let result = vnet.assign_port(ip, 3000, 2999).await; - assert!(result.is_err(), "assign_port should fail"); - - for i in 0..space { - let port = vnet.assign_port(ip, start, end).await?; - log::debug!("{} got port: {}", i, port); - - let obs: Arc> = - Arc::new(Mutex::new(DummyObserver)); - - let conn = Arc::new(UdpConn::new(SocketAddr::new(ip, port), None, obs)); - - let vi = vnet.vi.lock().await; - let _ = vi.udp_conns.insert(conn).await; - } - - { - let vi = vnet.vi.lock().await; - assert_eq!( - vi.udp_conns.len().await, - space as usize, - "udp_conns should match" - ); - } - - // attempt to assign again should fail - let result = vnet.assign_port(ip, start, end).await; - assert!(result.is_err(), "assign_port should fail"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_determine_source_ip() -> Result<()> { - let mut nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let interfaces = nw.get_interfaces().await; - assert_eq!(interfaces.len(), 2, "should be one interface"); - - { - let nic = nw.get_nic()?; - let mut nic = nic.lock().await; - let ipnet = IpNet::from_str(&format!("{DEMO_IP}/24"))?; - nic.add_addrs_to_interface("eth0", &[ipnet]).await?; - } - - // Any IP turned into non-loopback IP - let any_ip = IpAddr::from_str("0.0.0.0")?; - let dst_ip = IpAddr::from_str("27.1.7.135")?; - if let Net::VNet(vnet) = &mut nw { - let vnet = vnet.lock().await; - let vi = vnet.vi.lock().await; - let src_ip = vi.determine_source_ip(any_ip, dst_ip); - log::debug!("any_ip: {} => {:?}", any_ip, src_ip); - assert!(src_ip.is_some(), "shouldn't be none"); - if let Some(src_ip) = src_ip { - assert_eq!(src_ip.to_string().as_str(), DEMO_IP, "use non-loopback IP"); - } - } - - // Any IP turned into loopback IP - let any_ip = IpAddr::from_str("0.0.0.0")?; - let dst_ip = IpAddr::from_str("127.0.0.2")?; - if let Net::VNet(vnet) = &mut nw { - let vnet = vnet.lock().await; - let vi = vnet.vi.lock().await; - let src_ip = vi.determine_source_ip(any_ip, dst_ip); - log::debug!("any_ip: {} => {:?}", any_ip, src_ip); - assert!(src_ip.is_some(), "shouldn't be none"); - if let Some(src_ip) = src_ip { - assert_eq!(src_ip.to_string().as_str(), "127.0.0.1", "use loopback IP"); - } - } - - // Non any IP won't change - let any_ip = IpAddr::from_str(DEMO_IP)?; - let dst_ip = IpAddr::from_str("127.0.0.2")?; - if let Net::VNet(vnet) = &mut nw { - let vnet = vnet.lock().await; - let vi = vnet.vi.lock().await; - let src_ip = vi.determine_source_ip(any_ip, dst_ip); - log::debug!("any_ip: {} => {:?}", any_ip, src_ip); - assert!(src_ip.is_some(), "shouldn't be none"); - if let Some(src_ip) = src_ip { - assert_eq!(src_ip, any_ip, "IP change"); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_resolve_addr() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let udp_addr = nw.resolve_addr(true, "localhost:1234").await?; - assert_eq!( - udp_addr.ip().to_string().as_str(), - "127.0.0.1", - "udp addr {} should match 127.0.0.1", - udp_addr.ip(), - ); - assert_eq!( - udp_addr.port(), - 1234, - "udp addr {} should match 1234", - udp_addr.port() - ); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_loopback1() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let conn = nw.bind(SocketAddr::from_str("127.0.0.1:0")?).await?; - let laddr = conn.local_addr()?; - - let msg = "PING!"; - let n = conn.send_to(msg.as_bytes(), laddr).await?; - assert_eq!(n, msg.len(), "should match msg size {}", msg.len()); - - let mut buf = vec![0u8; 1000]; - let (n, raddr) = conn.recv_from(&mut buf).await?; - assert_eq!(n, msg.len(), "should match msg size {}", msg.len()); - assert_eq!(&buf[..n], msg.as_bytes(), "should match msg content {msg}"); - assert_eq!(laddr, raddr, "should match addr {laddr}"); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_bind_specific_port() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let conn = nw.bind(SocketAddr::from_str("127.0.0.1:50916")?).await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string().as_str(), - "127.0.0.1", - "{} should match 127.0.0.1", - laddr.ip() - ); - assert_eq!(laddr.port(), 50916, "{} should match 50916", laddr.port()); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_dail_lo0() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - assert!(nw.is_virtual(), "should be true"); - - let conn = nw.dail(true, "127.0.0.1:1234").await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string().as_str(), - "127.0.0.1", - "{} should match 127.0.0.1", - laddr.ip() - ); - assert_ne!(laddr.port(), 1234, "{} should != 1234", laddr.port()); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_dail_eth0() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let nw = Net::new(Some(NetConfig::default())); - - { - let nic = nw.get_nic()?; - - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - }; - - let conn = nw.dail(true, "27.3.4.5:1234").await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string().as_str(), - "1.2.3.1", - "{} should match 1.2.3.1", - laddr.ip() - ); - assert!(laddr.port() != 0, "{} should != 0", laddr.port()); - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_resolver() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let nw = Net::new(Some(NetConfig::default())); - - let remote_addr = nw.resolve_addr(true, "127.0.0.1:1234").await?; - assert_eq!(remote_addr.to_string(), "127.0.0.1:1234", "should match"); - - let result = nw.resolve_addr(false, "127.0.0.1:1234").await; - assert!(result.is_err(), "should not match"); - - { - let nic = nw.get_nic()?; - - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - w.add_host("test.webrtc.rs".to_owned(), "30.31.32.33".to_owned()) - .await?; - - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - tokio::spawn(async move { - let (conn, raddr) = { - let raddr = nw.resolve_addr(true, "test.webrtc.rs:1234").await?; - (nw.dail(true, "test.webrtc.rs:1234").await?, raddr) - }; - - let laddr = conn.local_addr()?; - assert_eq!( - laddr.ip().to_string().as_str(), - "1.2.3.1", - "{} should match 1.2.3.1", - laddr.ip() - ); - - assert_eq!( - raddr.to_string(), - "30.31.32.33:1234", - "{raddr} should match 30.31.32.33:1234" - ); - - drop(done_tx); - - Result::<()>::Ok(()) - }); - - let _ = done_rx.recv().await; - - Ok(()) -} - -#[tokio::test] -async fn test_net_virtual_loopback2() -> Result<()> { - let nw = Net::new(Some(NetConfig::default())); - - let conn = nw.bind(SocketAddr::from_str("127.0.0.1:50916")?).await?; - let laddr = conn.local_addr()?; - assert_eq!( - laddr.to_string().as_str(), - "127.0.0.1:50916", - "{laddr} should match 127.0.0.1:50916" - ); - - let mut c = ChunkUdp::new( - SocketAddr::from_str("127.0.0.1:4000")?, - SocketAddr::from_str("127.0.0.1:50916")?, - ); - c.user_data = b"Hello!".to_vec(); - - let (recv_ch_tx, mut recv_ch_rx) = mpsc::channel(1); - let (done_ch_tx, mut done_ch_rx) = mpsc::channel::(1); - let (close_ch_tx, mut close_ch_rx) = mpsc::channel::(1); - let conn_rx = Arc::clone(&conn); - - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - tokio::select! { - result = conn_rx.recv_from(&mut buf) => { - let (n, addr) = match result { - Ok((n, addr)) => (n, addr), - Err(err) => { - log::debug!("ReadFrom returned: {}", err); - break; - } - }; - - assert_eq!(n, 6, "{n} should match 6"); - assert_eq!(addr.to_string(), "127.0.0.1:4000", "addr should match"); - assert_eq!(&buf[..n], b"Hello!", "buf should match"); - - let _ = recv_ch_tx.send(true).await; - } - _ = close_ch_rx.recv() => { - break; - } - } - } - - drop(done_ch_tx); - }); - - if let Net::VNet(vnet) = &nw { - let vnet = vnet.lock().await; - vnet.on_inbound_chunk(Box::new(c)).await; - } else { - panic!("must be virtual net"); - } - - let _ = recv_ch_rx.recv().await; - drop(close_ch_tx); - - let _ = done_ch_rx.recv().await; - - Ok(()) -} - -async fn get_ipaddr(nic: &Arc>) -> Result { - let n = nic.lock().await; - let eth0 = n.get_interface("eth0").await.ok_or(Error::ErrNoInterface)?; - let addrs = eth0.addrs(); - if addrs.is_empty() { - Err(Error::ErrNoAddressAssigned) - } else { - Ok(addrs[0].addr()) - } -} - -//use std::io::Write; - -#[tokio::test] -async fn test_net_virtual_end2end() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let net1 = Net::new(Some(NetConfig::default())); - let ip1 = { - let nic = net1.get_nic()?; - - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - get_ipaddr(&nic).await? - }; - - let net2 = Net::new(Some(NetConfig::default())); - let ip2 = { - let nic = net2.get_nic()?; - - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - get_ipaddr(&nic).await? - }; - - let conn1 = net1.bind(SocketAddr::new(ip1, 1234)).await?; - let conn2 = net2.bind(SocketAddr::new(ip2, 5678)).await?; - - { - let mut w = wan.lock().await; - w.start().await?; - } - - let (close_ch_tx, mut close_ch_rx1) = broadcast::channel::(1); - let (done_ch_tx, mut done_ch_rx) = mpsc::channel::(1); - let (conn1_recv_ch_tx, mut conn1_recv_ch_rx) = mpsc::channel(1); - let conn1_rx = Arc::clone(&conn1); - let conn2_tr = Arc::clone(&conn2); - let mut close_ch_rx2 = close_ch_tx.subscribe(); - - // conn1 - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - log::debug!("conn1: wait for a message.."); - tokio::select! { - result = conn1_rx.recv_from(&mut buf) =>{ - let n = match result{ - Ok((n, _)) => n, - Err(err) => { - log::debug!("ReadFrom returned: {}", err); - break; - } - }; - - log::debug!("conn1 received {:?}", &buf[..n]); - let _ = conn1_recv_ch_tx.send(true).await; - } - _ = close_ch_rx1.recv() => { - log::debug!("conn1 received close_ch_rx1"); - break; - } - } - } - drop(done_ch_tx); - log::debug!("conn1 drop done_ch_tx, exit spawn"); - }); - - // conn2 - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - log::debug!("conn2: wait for a message.."); - tokio::select! { - result = conn2_tr.recv_from(&mut buf) =>{ - let (n, addr) = match result{ - Ok((n, addr)) => (n, addr), - Err(err) => { - log::debug!("ReadFrom returned: {}", err); - break; - } - }; - - log::debug!("conn2 received {:?}", &buf[..n]); - - // echo back to conn1 - let n = conn2_tr.send_to(b"Good-bye!", addr).await?; - assert_eq!( 9, n, "should match"); - } - _ = close_ch_rx2.recv() => { - log::debug!("conn1 received close_ch_rx2"); - break; - } - } - } - - log::debug!("conn2 exit spawn"); - - Result::<()>::Ok(()) - }); - - log::debug!("conn1: sending"); - let n = conn1.send_to(b"Hello!", conn2.local_addr()?).await?; - assert_eq!(n, 6, "should match"); - - let _ = conn1_recv_ch_rx.recv().await; - log::debug!("main recv conn1_recv_ch_rx"); - drop(close_ch_tx); - log::debug!("main drop close_ch_tx"); - let _ = done_ch_rx.recv().await; - log::debug!("main recv done_ch_rx"); - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_net_virtual_two_ips_on_a_nic() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let net = Net::new(Some(NetConfig { - static_ips: vec![DEMO_IP.to_owned(), "1.2.3.5".to_owned()], - ..Default::default() - })); - { - let nic = net.get_nic()?; - - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - // start the router - { - let mut w = wan.lock().await; - w.start().await?; - } - - let (conn1, conn2) = ( - net.bind(SocketAddr::new(Ipv4Addr::from_str(DEMO_IP)?.into(), 1234)) - .await?, - net.bind(SocketAddr::new(Ipv4Addr::from_str("1.2.3.5")?.into(), 1234)) - .await?, - ); - - let (close_ch_tx, mut close_ch_rx1) = broadcast::channel::(1); - let (done_ch_tx, mut done_ch_rx) = mpsc::channel::(1); - let (conn1_recv_ch_tx, mut conn1_recv_ch_rx) = mpsc::channel(1); - let conn1_rx = Arc::clone(&conn1); - let conn2_tr = Arc::clone(&conn2); - let mut close_ch_rx2 = close_ch_tx.subscribe(); - - // conn1 - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - log::debug!("conn1: wait for a message.."); - tokio::select! { - result = conn1_rx.recv_from(&mut buf) =>{ - let n = match result{ - Ok((n, _)) => n, - Err(err) => { - log::debug!("ReadFrom returned: {}", err); - break; - } - }; - - log::debug!("conn1 received {:?}", &buf[..n]); - let _ = conn1_recv_ch_tx.send(true).await; - } - _ = close_ch_rx1.recv() => { - log::debug!("conn1 received close_ch_rx1"); - break; - } - } - } - drop(done_ch_tx); - log::debug!("conn1 drop done_ch_tx, exit spawn"); - }); - - // conn2 - tokio::spawn(async move { - let mut buf = vec![0u8; 1500]; - loop { - log::debug!("conn2: wait for a message.."); - tokio::select! { - result = conn2_tr.recv_from(&mut buf) =>{ - let (n, addr) = match result{ - Ok((n, addr)) => (n, addr), - Err(err) => { - log::debug!("ReadFrom returned: {}", err); - break; - } - }; - - log::debug!("conn2 received {:?}", &buf[..n]); - - // echo back to conn1 - let n = conn2_tr.send_to(b"Good-bye!", addr).await?; - assert_eq!(n, 9, "should match"); - } - _ = close_ch_rx2.recv() => { - log::debug!("conn1 received close_ch_rx2"); - break; - } - } - } - - log::debug!("conn2 exit spawn"); - - Result::<()>::Ok(()) - }); - - log::debug!("conn1: sending"); - let n = conn1.send_to(b"Hello!", conn2.local_addr()?).await?; - assert_eq!(n, 6, "should match"); - - let _ = conn1_recv_ch_rx.recv().await; - log::debug!("main recv conn1_recv_ch_rx"); - drop(close_ch_tx); - log::debug!("main drop close_ch_tx"); - let _ = done_ch_rx.recv().await; - log::debug!("main recv done_ch_rx"); - Ok(()) -} diff --git a/util/src/vnet/resolver.rs b/util/src/vnet/resolver.rs deleted file mode 100644 index 2972c112a..000000000 --- a/util/src/vnet/resolver.rs +++ /dev/null @@ -1,68 +0,0 @@ -#[cfg(test)] -mod resolver_test; - -use std::collections::HashMap; -use std::future::Future; -use std::net::IpAddr; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::Arc; - -use tokio::sync::Mutex; - -use crate::error::*; - -#[derive(Default)] -pub(crate) struct Resolver { - parent: Option>>, - hosts: HashMap, -} - -impl Resolver { - pub(crate) fn new() -> Self { - let mut r = Resolver { - parent: None, - hosts: HashMap::new(), - }; - - if let Err(err) = r.add_host("localhost".to_owned(), "127.0.0.1".to_owned()) { - log::warn!("failed to add localhost to Resolver: {}", err); - } - r - } - - pub(crate) fn set_parent(&mut self, p: Arc>) { - self.parent = Some(p); - } - - pub(crate) fn add_host(&mut self, name: String, ip_addr: String) -> Result<()> { - if name.is_empty() { - return Err(Error::ErrHostnameEmpty); - } - let ip = IpAddr::from_str(&ip_addr)?; - self.hosts.insert(name, ip); - - Ok(()) - } - - pub(crate) fn lookup( - &self, - host_name: String, - ) -> Pin> + Send + 'static>> { - if let Some(ip) = self.hosts.get(&host_name) { - let ip2 = *ip; - return Box::pin(async move { Some(ip2) }); - } - - // mutex must be unlocked before calling into parent Resolver - if let Some(parent) = &self.parent { - let parent2 = Arc::clone(parent); - Box::pin(async move { - let p = parent2.lock().await; - p.lookup(host_name).await - }) - } else { - Box::pin(async move { None }) - } - } -} diff --git a/util/src/vnet/resolver/resolver_test.rs b/util/src/vnet/resolver/resolver_test.rs deleted file mode 100644 index 701fc3036..000000000 --- a/util/src/vnet/resolver/resolver_test.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::*; - -const DEMO_IP: &str = "1.2.3.4"; - -#[tokio::test] -async fn test_resolver_standalone() -> Result<()> { - let mut r = Resolver::new(); - - // should have localhost by default - let name = "localhost"; - let ip_addr = "127.0.0.1"; - let ip = IpAddr::from_str(ip_addr)?; - - if let Some(resolved) = r.lookup(name.to_owned()).await { - assert_eq!(resolved, ip, "should match"); - } else { - panic!("should Some, but got None"); - } - - let name = "abc.com"; - let ip_addr = DEMO_IP; - let ip = IpAddr::from_str(ip_addr)?; - log::debug!("adding {} {}", name, ip_addr); - - r.add_host(name.to_owned(), ip_addr.to_owned())?; - - if let Some(resolved) = r.lookup(name.to_owned()).await { - assert_eq!(resolved, ip, "should match"); - } else { - panic!("should Some, but got None"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_resolver_cascaded() -> Result<()> { - let mut r0 = Resolver::new(); - - let name0 = "abc.com"; - let ip_addr0 = DEMO_IP; - let ip0 = IpAddr::from_str(ip_addr0)?; - r0.add_host(name0.to_owned(), ip_addr0.to_owned())?; - - let mut r1 = Resolver::new(); - - let name1 = "myserver.local"; - let ip_addr1 = "10.1.2.5"; - let ip1 = IpAddr::from_str(ip_addr1)?; - r1.add_host(name1.to_owned(), ip_addr1.to_owned())?; - - r1.set_parent(Arc::new(Mutex::new(r0))); - - if let Some(resolved) = r1.lookup(name0.to_owned()).await { - assert_eq!(resolved, ip0, "should match"); - } else { - panic!("should Some, but got None"); - } - - if let Some(resolved) = r1.lookup(name1.to_owned()).await { - assert_eq!(resolved, ip1, "should match"); - } else { - panic!("should Some, but got None"); - } - - // should fail if the name does not exist - let result = r1.lookup("bad.com".to_owned()).await; - assert!(result.is_none(), "should fail"); - - Ok(()) -} diff --git a/util/src/vnet/router.rs b/util/src/vnet/router.rs deleted file mode 100644 index c5ad4c0f2..000000000 --- a/util/src/vnet/router.rs +++ /dev/null @@ -1,586 +0,0 @@ -#[cfg(test)] -mod router_test; - -use std::collections::HashMap; -use std::future::Future; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::ops::{Add, Sub}; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::SystemTime; - -use async_trait::async_trait; -use ipnet::*; -use portable_atomic::AtomicU64; -use tokio::sync::{mpsc, Mutex}; -use tokio::time::Duration; - -use crate::error::*; -use crate::vnet::chunk::*; -use crate::vnet::chunk_queue::*; -use crate::vnet::interface::*; -use crate::vnet::nat::*; -use crate::vnet::net::*; -use crate::vnet::resolver::*; - -const DEFAULT_ROUTER_QUEUE_SIZE: usize = 0; // unlimited - -lazy_static! { - pub static ref ROUTER_ID_CTR: AtomicU64 = AtomicU64::new(0); -} - -// Generate a unique router name -fn assign_router_name() -> String { - let n = ROUTER_ID_CTR.fetch_add(1, Ordering::SeqCst); - format!("router{n}") -} - -// RouterConfig ... -#[derive(Default)] -pub struct RouterConfig { - // name of router. If not specified, a unique name will be assigned. - pub name: String, - // cidr notation, like "192.0.2.0/24" - pub cidr: String, - // static_ips is an array of static IP addresses to be assigned for this router. - // If no static IP address is given, the router will automatically assign - // an IP address. - // This will be ignored if this router is the root. - pub static_ips: Vec, - // static_ip is deprecated. Use static_ips. - pub static_ip: String, - // Internal queue size - pub queue_size: usize, - // Effective only when this router has a parent router - pub nat_type: Option, - // Minimum Delay - pub min_delay: Duration, - // Max Jitter - pub max_jitter: Duration, -} - -// NIC is a network interface controller that interfaces Router -#[async_trait] -pub trait Nic { - async fn get_interface(&self, ifc_name: &str) -> Option; - async fn add_addrs_to_interface(&mut self, ifc_name: &str, addrs: &[IpNet]) -> Result<()>; - async fn on_inbound_chunk(&self, c: Box); - async fn get_static_ips(&self) -> Vec; - async fn set_router(&self, r: Arc>) -> Result<()>; -} - -// ChunkFilter is a handler users can add to filter chunks. -// If the filter returns false, the packet will be dropped. -pub type ChunkFilterFn = Box bool) + Send + Sync>; - -#[derive(Default)] -pub struct RouterInternal { - pub(crate) nat_type: Option, // read-only - pub(crate) ipv4net: IpNet, // read-only - pub(crate) parent: Option>>, // read-only - pub(crate) nat: NetworkAddressTranslator, // read-only - pub(crate) nics: HashMap>>, // read-only - pub(crate) chunk_filters: Vec, // requires mutex [x] - pub(crate) last_id: u8, // requires mutex [x], used to assign the last digit of IPv4 address -} - -// Router ... -#[derive(Default)] -pub struct Router { - name: String, // read-only - ipv4net: IpNet, // read-only - min_delay: Duration, // requires mutex [x] - max_jitter: Duration, // requires mutex [x] - queue: Arc, // read-only - interfaces: Vec, // read-only - static_ips: Vec, // read-only - static_local_ips: HashMap, // read-only, - children: Vec>>, // read-only - done: Option>, // requires mutex [x] - pub(crate) resolver: Arc>, // read-only - push_ch: Option>, // writer requires mutex - router_internal: Arc>, -} - -#[async_trait] -impl Nic for Router { - async fn get_interface(&self, ifc_name: &str) -> Option { - for ifc in &self.interfaces { - if ifc.name == ifc_name { - return Some(ifc.clone()); - } - } - None - } - - async fn add_addrs_to_interface(&mut self, ifc_name: &str, addrs: &[IpNet]) -> Result<()> { - for ifc in &mut self.interfaces { - if ifc.name == ifc_name { - for addr in addrs { - ifc.add_addr(*addr); - } - return Ok(()); - } - } - - Err(Error::ErrNotFound) - } - - async fn on_inbound_chunk(&self, c: Box) { - let from_parent: Box = { - let router_internal = self.router_internal.lock().await; - match router_internal.nat.translate_inbound(&*c).await { - Ok(from) => { - if let Some(from) = from { - from - } else { - return; - } - } - Err(err) => { - log::warn!("[{}] {}", self.name, err); - return; - } - } - }; - - self.push(from_parent).await; - } - - async fn get_static_ips(&self) -> Vec { - self.static_ips.clone() - } - - // caller must hold the mutex - async fn set_router(&self, parent: Arc>) -> Result<()> { - { - let mut router_internal = self.router_internal.lock().await; - router_internal.parent = Some(Arc::clone(&parent)); - } - - let parent_resolver = { - let p = parent.lock().await; - Arc::clone(&p.resolver) - }; - { - let mut resolver = self.resolver.lock().await; - resolver.set_parent(parent_resolver); - } - - let mut mapped_ips = vec![]; - let mut local_ips = vec![]; - - // when this method is called, one or more IP address has already been assigned by - // the parent router. - if let Some(ifc) = self.get_interface("eth0").await { - for ifc_addr in ifc.addrs() { - let ip = ifc_addr.addr(); - mapped_ips.push(ip); - - if let Some(loc_ip) = self.static_local_ips.get(&ip.to_string()) { - local_ips.push(*loc_ip); - } - } - } else { - return Err(Error::ErrNoIpaddrEth0); - } - - // Set up NAT here - { - let mut router_internal = self.router_internal.lock().await; - if router_internal.nat_type.is_none() { - router_internal.nat_type = Some(NatType { - mapping_behavior: EndpointDependencyType::EndpointIndependent, - filtering_behavior: EndpointDependencyType::EndpointAddrPortDependent, - hair_pining: false, - port_preservation: false, - mapping_life_time: Duration::from_secs(30), - ..Default::default() - }); - } - - router_internal.nat = NetworkAddressTranslator::new(NatConfig { - name: self.name.clone(), - nat_type: router_internal.nat_type.unwrap(), - mapped_ips, - local_ips, - })?; - } - - Ok(()) - } -} - -impl Router { - pub fn new(config: RouterConfig) -> Result { - let ipv4net: IpNet = config.cidr.parse()?; - - let queue_size = if config.queue_size > 0 { - config.queue_size - } else { - DEFAULT_ROUTER_QUEUE_SIZE - }; - - // set up network interface, lo0 - let mut lo0 = Interface::new(LO0_STR.to_owned(), vec![]); - if let Ok(ipnet) = Interface::convert( - SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 0), - Some(SocketAddr::new(Ipv4Addr::new(255, 0, 0, 0).into(), 0)), - ) { - lo0.add_addr(ipnet); - } - - // set up network interface, eth0 - let eth0 = Interface::new("eth0".to_owned(), vec![]); - - // local host name resolver - let resolver = Arc::new(Mutex::new(Resolver::new())); - - let name = if config.name.is_empty() { - assign_router_name() - } else { - config.name.clone() - }; - - let mut static_ips = vec![]; - let mut static_local_ips = HashMap::new(); - for ip_str in &config.static_ips { - let ip_pair: Vec<&str> = ip_str.split('/').collect(); - if let Ok(ip) = IpAddr::from_str(ip_pair[0]) { - if ip_pair.len() > 1 { - let loc_ip = IpAddr::from_str(ip_pair[1])?; - if !ipv4net.contains(&loc_ip) { - return Err(Error::ErrLocalIpBeyondStaticIpsSubset); - } - static_local_ips.insert(ip.to_string(), loc_ip); - } - static_ips.push(ip); - } - } - if !config.static_ip.is_empty() { - log::warn!("static_ip is deprecated. Use static_ips instead"); - if let Ok(ip) = IpAddr::from_str(&config.static_ip) { - static_ips.push(ip); - } - } - - let n_static_local = static_local_ips.len(); - if n_static_local > 0 && n_static_local != static_ips.len() { - return Err(Error::ErrLocalIpNoStaticsIpsAssociated); - } - - let router_internal = RouterInternal { - nat_type: config.nat_type, - ipv4net, - nics: HashMap::new(), - ..Default::default() - }; - - Ok(Router { - name, - ipv4net, - interfaces: vec![lo0, eth0], - static_ips, - static_local_ips, - resolver, - router_internal: Arc::new(Mutex::new(router_internal)), - queue: Arc::new(ChunkQueue::new(queue_size)), - min_delay: config.min_delay, - max_jitter: config.max_jitter, - ..Default::default() - }) - } - - // caller must hold the mutex - pub(crate) fn get_interfaces(&self) -> &[Interface] { - &self.interfaces - } - - // Start ... - pub fn start(&mut self) -> Pin>>> { - if self.done.is_some() { - return Box::pin(async move { Err(Error::ErrRouterAlreadyStarted) }); - } - - let (done_tx, mut done_rx) = mpsc::channel(1); - let (push_ch_tx, mut push_ch_rx) = mpsc::channel(1); - self.done = Some(done_tx); - self.push_ch = Some(push_ch_tx); - - let router_internal = Arc::clone(&self.router_internal); - let queue = Arc::clone(&self.queue); - let max_jitter = self.max_jitter; - let min_delay = self.min_delay; - let name = self.name.clone(); - let ipv4net = self.ipv4net; - - tokio::spawn(async move { - while let Ok(d) = Router::process_chunks( - &name, - ipv4net, - max_jitter, - min_delay, - &queue, - &router_internal, - ) - .await - { - if d == Duration::from_secs(0) { - tokio::select! { - _ = push_ch_rx.recv() =>{}, - _ = done_rx.recv() => break, - } - } else { - let t = tokio::time::sleep(d); - tokio::pin!(t); - - tokio::select! { - _ = t.as_mut() => {}, - _ = done_rx.recv() => break, - } - } - } - }); - - let children = self.children.clone(); - Box::pin(async move { Router::start_children(children).await }) - } - - // Stop ... - pub fn stop(&mut self) -> Pin>>> { - if self.done.is_none() { - return Box::pin(async move { Err(Error::ErrRouterAlreadyStopped) }); - } - self.push_ch.take(); - self.done.take(); - - let children = self.children.clone(); - Box::pin(async move { Router::stop_children(children).await }) - } - - async fn start_children(children: Vec>>) -> Result<()> { - for child in children { - let mut c = child.lock().await; - c.start().await?; - } - - Ok(()) - } - - async fn stop_children(children: Vec>>) -> Result<()> { - for child in children { - let mut c = child.lock().await; - c.stop().await?; - } - - Ok(()) - } - - // AddRouter adds a chile Router. - // after parent.add_router(child), also call child.set_router(parent) to set child's parent router - pub async fn add_router(&mut self, child: Arc>) -> Result<()> { - // Router is a NIC. Add it as a NIC so that packets are routed to this child - // router. - let nic = Arc::clone(&child) as Arc>; - self.children.push(child); - self.add_net(nic).await - } - - // AddNet ... - // after router.add_net(nic), also call nic.set_router(router) to set nic's router - pub async fn add_net(&mut self, nic: Arc>) -> Result<()> { - let mut router_internal = self.router_internal.lock().await; - router_internal.add_nic(nic).await - } - - // AddHost adds a mapping of hostname and an IP address to the local resolver. - pub async fn add_host(&mut self, host_name: String, ip_addr: String) -> Result<()> { - let mut resolver = self.resolver.lock().await; - resolver.add_host(host_name, ip_addr) - } - - // AddChunkFilter adds a filter for chunks traversing this router. - // You may add more than one filter. The filters are called in the order of this method call. - // If a chunk is dropped by a filter, subsequent filter will not receive the chunk. - pub async fn add_chunk_filter(&self, filter: ChunkFilterFn) { - let mut router_internal = self.router_internal.lock().await; - router_internal.chunk_filters.push(filter); - } - - pub(crate) async fn push(&self, mut c: Box) { - log::debug!("[{}] route {}", self.name, c); - if self.done.is_some() { - c.set_timestamp(); - - if self.queue.push(c).await { - if let Some(push_ch) = &self.push_ch { - let _ = push_ch.try_send(()); - } - } else { - log::warn!("[{}] queue was full. dropped a chunk", self.name); - } - } else { - log::warn!("router is done"); - } - } - - async fn process_chunks( - name: &str, - ipv4net: IpNet, - max_jitter: Duration, - min_delay: Duration, - queue: &Arc, - router_internal: &Arc>, - ) -> Result { - // Introduce jitter by delaying the processing of chunks. - let mj = max_jitter.as_nanos() as u64; - if mj > 0 { - let jitter = Duration::from_nanos(rand::random::() % mj); - tokio::time::sleep(jitter).await; - } - - // cut_off - // v min delay - // |<--->| - // +------------:-- - // |OOOOOOXXXXX : --> time - // +------------:-- - // |<--->| now - // due - - let entered_at = SystemTime::now(); - let cut_off = entered_at.sub(min_delay); - - // the next sleep duration - let mut d; - - loop { - d = Duration::from_secs(0); - - if let Some(c) = queue.peek().await { - // check timestamp to find if the chunk is due - if c.get_timestamp().duration_since(cut_off).is_ok() { - // There is one or more chunk in the queue but none of them are due. - // Calculate the next sleep duration here. - let next_expire = c.get_timestamp().add(min_delay); - if let Ok(diff) = next_expire.duration_since(entered_at) { - d = diff; - break; - } - } - } else { - break; // no more chunk in the queue - } - - if let Some(c) = queue.pop().await { - let ri = router_internal.lock().await; - let mut blocked = false; - for filter in &ri.chunk_filters { - if !filter(&*c) { - blocked = true; - break; - } - } - if blocked { - continue; // discard - } - - let dst_ip = c.get_destination_ip(); - - // check if the destination is in our subnet - if ipv4net.contains(&dst_ip) { - // search for the destination NIC - if let Some(nic) = ri.nics.get(&dst_ip.to_string()) { - // found the NIC, forward the chunk to the NIC. - // call to NIC must unlock mutex - let ni = nic.lock().await; - ni.on_inbound_chunk(c).await; - } else { - // NIC not found. drop it. - log::debug!("[{}] {} unreachable", name, c); - } - } else { - // the destination is outside of this subnet - // is this WAN? - if let Some(parent) = &ri.parent { - // Pass it to the parent via NAT - if let Some(to_parent) = ri.nat.translate_outbound(&*c).await? { - // call to parent router mutex unlock mutex - let p = parent.lock().await; - p.push(to_parent).await; - } - } else { - // this WAN. No route for this chunk - log::debug!("[{}] no route found for {}", name, c); - } - } - } else { - break; // no more chunk in the queue - } - } - - Ok(d) - } -} - -impl RouterInternal { - // caller must hold the mutex - pub(crate) async fn add_nic(&mut self, nic: Arc>) -> Result<()> { - let mut ips = { - let ni = nic.lock().await; - ni.get_static_ips().await - }; - - if ips.is_empty() { - // assign an IP address - let ip = self.assign_ip_address()?; - log::debug!("assign_ip_address: {}", ip); - ips.push(ip); - } - - let mut ipnets = vec![]; - for ip in &ips { - if !self.ipv4net.contains(ip) { - return Err(Error::ErrStaticIpIsBeyondSubnet); - } - self.nics.insert(ip.to_string(), Arc::clone(&nic)); - ipnets.push(IpNet::from_str(&format!( - "{}/{}", - ip, - self.ipv4net.prefix_len() - ))?); - } - - { - let mut ni = nic.lock().await; - let _ = ni.add_addrs_to_interface("eth0", &ipnets).await; - } - - Ok(()) - } - - // caller should hold the mutex - fn assign_ip_address(&mut self) -> Result { - // See: https://stackoverflow.com/questions/14915188/ip-address-ending-with-zero - - if self.last_id == 0xfe { - return Err(Error::ErrAddressSpaceExhausted); - } - - self.last_id += 1; - match self.ipv4net.addr() { - IpAddr::V4(ipv4) => { - let mut ip = ipv4.octets(); - ip[3] = self.last_id; - Ok(IpAddr::V4(Ipv4Addr::from(ip))) - } - IpAddr::V6(ipv6) => { - let mut ip = ipv6.octets(); - ip[15] += self.last_id; - Ok(IpAddr::V6(Ipv6Addr::from(ip))) - } - } - } -} diff --git a/util/src/vnet/router/router_test.rs b/util/src/vnet/router/router_test.rs deleted file mode 100644 index e5694af83..000000000 --- a/util/src/vnet/router/router_test.rs +++ /dev/null @@ -1,810 +0,0 @@ -use portable_atomic::{AtomicI32, AtomicUsize}; - -use super::*; - -const MARGIN: Duration = Duration::from_millis(18); -const DEMO_IP: &str = "1.2.3.4"; - -struct DummyNic { - net: Net, - on_inbound_chunk_handler: u16, - cbs0: AtomicI32, - done_ch_tx: Arc>>>, - delay_res: Arc>>, - npkts: i32, -} - -impl Default for DummyNic { - fn default() -> Self { - DummyNic { - net: Net::Ifs(vec![]), - on_inbound_chunk_handler: 0, - cbs0: AtomicI32::new(0), - done_ch_tx: Arc::new(Mutex::new(None)), - delay_res: Arc::new(Mutex::new(vec![])), - npkts: 0, - } - } -} - -#[async_trait] -impl Nic for DummyNic { - async fn get_interface(&self, ifc_name: &str) -> Option { - self.net.get_interface(ifc_name).await - } - - async fn add_addrs_to_interface(&mut self, ifc_name: &str, addrs: &[IpNet]) -> Result<()> { - let nic = self.net.get_nic()?; - let mut net = nic.lock().await; - net.add_addrs_to_interface(ifc_name, addrs).await - } - - async fn set_router(&self, r: Arc>) -> Result<()> { - let nic = self.net.get_nic()?; - let net = nic.lock().await; - net.set_router(r).await - } - - async fn on_inbound_chunk(&self, c: Box) { - log::debug!("received: {}", c); - match self.on_inbound_chunk_handler { - 0 => { - self.cbs0.fetch_add(1, Ordering::SeqCst); - } - 1 => { - let mut done_ch_tx = self.done_ch_tx.lock().await; - done_ch_tx.take(); - } - 2 => { - let delay = SystemTime::now() - .duration_since(c.get_timestamp()) - .unwrap_or(Duration::from_secs(0)); - { - let mut delay_res = self.delay_res.lock().await; - delay_res.push(delay); - } - - let n = self.cbs0.fetch_add(1, Ordering::SeqCst); - if n >= self.npkts - 1 { - let mut done_ch_tx = self.done_ch_tx.lock().await; - done_ch_tx.take(); - } - } - 3 => { - // echo the chunk - let mut echo = c.clone_to(); - let result = echo.set_source_addr(&c.destination_addr().to_string()); - assert!(result.is_ok(), "should succeed"); - let result = echo.set_destination_addr(&c.source_addr().to_string()); - assert!(result.is_ok(), "should succeed"); - - log::debug!("wan.push being called.."); - if let Net::VNet(vnet) = &self.net { - let net = vnet.lock().await; - let vi = net.vi.lock().await; - if let Some(r) = &vi.router { - let wan = r.lock().await; - wan.push(echo).await; - } - } - log::debug!("wan.push called!"); - } - _ => {} - }; - } - - async fn get_static_ips(&self) -> Vec { - let nic = match self.net.get_nic() { - Ok(nic) => nic, - Err(_) => return vec![], - }; - let net = nic.lock().await; - net.get_static_ips().await - } -} - -async fn get_ipaddr(nic: &Arc>) -> Result { - let n = nic.lock().await; - let eth0 = n.get_interface("eth0").await.ok_or(Error::ErrNoInterface)?; - let addrs = eth0.addrs(); - if addrs.is_empty() { - Err(Error::ErrNoAddressAssigned) - } else { - Ok(addrs[0].addr()) - } -} - -#[test] -fn test_router_standalone_cidr_parsing() -> Result<()> { - let r = Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?; - - assert_eq!(r.ipv4net.addr().to_string(), "1.2.3.0", "ip should match"); - assert_eq!( - r.ipv4net.netmask().to_string(), - "255.255.255.0", - "mask should match" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_router_standalone_assign_ip_address() -> Result<()> { - let r = Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?; - - let mut ri = r.router_internal.lock().await; - for i in 1..255 { - let ip = match ri.assign_ip_address()? { - IpAddr::V4(ip) => ip.octets().to_vec(), - IpAddr::V6(ip) => ip.octets().to_vec(), - }; - assert_eq!(ip[0], 1_u8, "should match"); - assert_eq!(ip[1], 2_u8, "should match"); - assert_eq!(ip[2], 3_u8, "should match"); - assert_eq!(ip[3], i as u8, "should match"); - } - - let result = ri.assign_ip_address(); - assert!(result.is_err(), "assign_ip_address should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_router_standalone_add_net() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let net = Net::new(Some(NetConfig::default())); - - let nic = net.get_nic()?; - - { - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - } - - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - - let eth0 = n.get_interface("eth0").await; - assert!(eth0.is_some(), "should succeed"); - if let Some(eth0) = eth0 { - let addrs = eth0.addrs(); - assert_eq!(addrs.len(), 1, "should match"); - assert_eq!(addrs[0].to_string(), "1.2.3.1/24", "should match"); - assert_eq!(addrs[0].addr().to_string(), "1.2.3.1", "should match"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_router_standalone_routing() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let (done_ch_tx, mut done_ch_rx) = mpsc::channel(1); - let mut done_ch_tx = Some(done_ch_tx); - - let mut nics = vec![]; - let mut ips = vec![]; - for i in 0..2 { - let dn = DummyNic { - net: Net::new(Some(NetConfig::default())), - on_inbound_chunk_handler: i, - ..Default::default() - }; - if i == 1 { - let mut done_ch = dn.done_ch_tx.lock().await; - *done_ch = done_ch_tx.take(); - } - let nic = Arc::new(Mutex::new(dn)); - - { - let n = Arc::clone(&nic) as Arc>; - let mut w = wan.lock().await; - w.add_net(n).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - { - // Now, eth0 must have one address assigned - let n = nic.lock().await; - if let Some(eth0) = n.get_interface("eth0").await { - let addrs = eth0.addrs(); - assert_eq!(addrs.len(), 1, "should match"); - ips.push(SocketAddr::new(addrs[0].addr(), 1111 * (i + 1))); - } - } - - nics.push(nic); - } - - { - let c = Box::new(ChunkUdp::new(ips[0], ips[1])); - - let mut r = wan.lock().await; - r.start().await?; - r.push(c).await; - } - - let _ = done_ch_rx.recv().await; - - { - let mut r = wan.lock().await; - r.stop().await?; - } - - { - let n = nics[0].lock().await; - assert_eq!(n.cbs0.load(Ordering::SeqCst), 0, "should be zero"); - } - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_router_standalone_add_chunk_filter() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - ..Default::default() - })?)); - - let mut nics = vec![]; - let mut ips = vec![]; - for i in 0..2 { - let dn = DummyNic { - net: Net::new(Some(NetConfig::default())), - on_inbound_chunk_handler: 0, - ..Default::default() - }; - let nic = Arc::new(Mutex::new(dn)); - - { - let n = Arc::clone(&nic) as Arc>; - let mut w = wan.lock().await; - w.add_net(n).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - { - // Now, eth0 must have one address assigned - let n = nic.lock().await; - if let Some(eth0) = n.get_interface("eth0").await { - let addrs = eth0.addrs(); - assert_eq!(addrs.len(), 1, "should match"); - ips.push(SocketAddr::new(addrs[0].addr(), 1111 * (i + 1))); - } - } - - nics.push(nic); - } - - // this creates a filter that block the first chunk - let make_filter_fn = |name: String| { - let n = AtomicUsize::new(0); - Box::new(move |c: &(dyn Chunk + Send + Sync)| -> bool { - let m = n.fetch_add(1, Ordering::SeqCst); - let pass = m > 0; - if pass { - log::debug!("{}: {} passed {}", m, name, c); - } else { - log::debug!("{}: {} blocked {}", m, name, c); - } - pass - }) - }; - - { - let mut r = wan.lock().await; - r.add_chunk_filter(make_filter_fn("filter1".to_owned())) - .await; - r.add_chunk_filter(make_filter_fn("filter2".to_owned())) - .await; - r.start().await?; - - // send 3 packets - for i in 0..3u8 { - let mut c = ChunkUdp::new(ips[0], ips[1]); - c.user_data = vec![i]; // 1-byte seq num - r.push(Box::new(c)).await; - } - } - - tokio::time::sleep(Duration::from_millis(50)).await; - - { - let mut r = wan.lock().await; - r.stop().await?; - } - - { - let n = nics[0].lock().await; - assert_eq!(n.cbs0.load(Ordering::SeqCst), 0, "should be zero"); - } - - { - let n = nics[1].lock().await; - assert_eq!(n.cbs0.load(Ordering::SeqCst), 1, "should be one"); - } - - Ok(()) -} - -async fn delay_sub_test(title: String, min_delay: Duration, max_jitter: Duration) -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_string(), - min_delay, - max_jitter, - ..Default::default() - })?)); - - let npkts = 1; - let (done_ch_tx, mut done_ch_rx) = mpsc::channel(1); - let mut done_ch_tx = Some(done_ch_tx); - - let mut nics = vec![]; - let mut ips = vec![]; - for i in 0..2 { - let mut dn = DummyNic { - net: Net::new(Some(NetConfig::default())), - on_inbound_chunk_handler: 0, - ..Default::default() - }; - if i == 1 { - dn.on_inbound_chunk_handler = 2; - dn.npkts = npkts; - - let mut done_ch = dn.done_ch_tx.lock().await; - *done_ch = done_ch_tx.take(); - } - let nic = Arc::new(Mutex::new(dn)); - - { - let n = Arc::clone(&nic) as Arc>; - let mut w = wan.lock().await; - w.add_net(n).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - { - // Now, eth0 must have one address assigned - let n = nic.lock().await; - if let Some(eth0) = n.get_interface("eth0").await { - let addrs = eth0.addrs(); - assert_eq!(addrs.len(), 1, "should match"); - ips.push(SocketAddr::new(addrs[0].addr(), 1111 * (i + 1))); - } - } - - nics.push(nic); - } - - { - let mut r = wan.lock().await; - r.start().await?; - - for _ in 0..npkts { - let c = Box::new(ChunkUdp::new(ips[0], ips[1])); - r.push(c).await; - tokio::time::sleep(Duration::from_millis(50)).await; - } - } - - let _ = done_ch_rx.recv().await; - - { - let mut r = wan.lock().await; - r.stop().await?; - } - - // Validate the amount of delays - { - let n = nics[1].lock().await; - let delay_res = n.delay_res.lock().await; - for d in &*delay_res { - log::info!("min delay : {:?}", min_delay); - log::info!("max jitter: {:?}", max_jitter); - log::info!("actual delay: {:?}", d); - assert!(*d >= min_delay, "{title} should delay {d:?} >= 20ms"); - assert!( - *d <= (min_delay + max_jitter + MARGIN), - "{title} should delay {d:?} <= minDelay + maxJitter", - ); - // Note: actual delay should be within 30ms but giving a 8ms - // MARGIN for possible extra delay - // (e.g. wakeup delay, debug logs, etc) - } - } - - Ok(()) -} - -//use std::io::Write; -#[cfg(target_os = "linux")] -#[tokio::test] -async fn test_router_delay() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - delay_sub_test( - "Delay only".to_owned(), - Duration::from_millis(20), - Duration::from_millis(0), - ) - .await?; - delay_sub_test( - "Jitter only".to_owned(), - Duration::from_millis(0), - Duration::from_millis(10), - ) - .await?; - delay_sub_test( - "Delay and Jitter".to_owned(), - Duration::from_millis(20), - Duration::from_millis(10), - ) - .await?; - - Ok(()) -} - -//use std::io::Write; - -#[tokio::test] -async fn test_router_one_child() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, log::LevelFilter::Trace) - .init();*/ - - let (done_ch_tx, mut done_ch_rx) = mpsc::channel(1); - let mut done_ch_tx = Some(done_ch_tx); - - let mut rs = vec![]; - let mut nics = vec![]; - let mut ips = vec![]; - for i in 0..2 { - let r = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: if i == 0 { - "1.2.3.0/24".to_owned() - } else { - "192.168.0.0/24".to_owned() - }, - ..Default::default() - })?)); - - let mut dn = DummyNic { - net: Net::new(Some(NetConfig::default())), - on_inbound_chunk_handler: i, - ..Default::default() - }; - if i == 1 { - let mut done_ch = dn.done_ch_tx.lock().await; - *done_ch = done_ch_tx.take(); - } else { - dn.on_inbound_chunk_handler = 3; - } - let nic = Arc::new(Mutex::new(dn)); - - { - let n = Arc::clone(&nic) as Arc>; - let mut w = r.lock().await; - w.add_net(n).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&r)).await?; - } - - { - let n = Arc::clone(&nic) as Arc>; - let ip = get_ipaddr(&n).await?; - ips.push(ip); - } - - nics.push(nic); - rs.push(r); - } - - { - let child = Arc::clone(&rs[1]); - let mut wan = rs[0].lock().await; - wan.add_router(child).await?; - } - { - let parent = Arc::clone(&rs[0]); - let lan = rs[1].lock().await; - lan.set_router(parent).await?; - } - - { - let mut wan = rs[0].lock().await; - wan.start().await?; - } - - { - let c = Box::new(ChunkUdp::new( - SocketAddr::new(ips[1], 1234), //lanIP - SocketAddr::new(ips[0], 5678), //wanIP - )); - log::debug!("sending {}", c); - let lan = rs[1].lock().await; - lan.push(c).await; - } - - log::debug!("waiting done_ch_rx"); - let _ = done_ch_rx.recv().await; - - { - let mut wan = rs[0].lock().await; - wan.stop().await?; - } - - Ok(()) -} - -#[test] -fn test_router_static_ips_more_than_one() -> Result<()> { - let lan = Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1".to_owned(), - "1.2.3.2".to_owned(), - "1.2.3.3".to_owned(), - ], - ..Default::default() - })?; - - assert_eq!(lan.static_ips.len(), 3, "should be 3"); - assert_eq!(lan.static_ips[0].to_string(), "1.2.3.1", "should match"); - assert_eq!(lan.static_ips[1].to_string(), "1.2.3.2", "should match"); - assert_eq!(lan.static_ips[2].to_string(), "1.2.3.3", "should match"); - - Ok(()) -} - -#[test] -fn test_router_static_ips_static_ip_local_ip_mapping() -> Result<()> { - let lan = Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1/192.168.0.1".to_owned(), - "1.2.3.2/192.168.0.2".to_owned(), - "1.2.3.3/192.168.0.3".to_owned(), - ], - ..Default::default() - })?; - - assert_eq!(lan.static_ips.len(), 3, "should be 3"); - assert_eq!(lan.static_ips[0].to_string(), "1.2.3.1", "should match"); - assert_eq!(lan.static_ips[1].to_string(), "1.2.3.2", "should match"); - assert_eq!(lan.static_ips[2].to_string(), "1.2.3.3", "should match"); - - assert_eq!(3, lan.static_local_ips.len(), "should be 3"); - let local_ips = ["192.168.0.1", "192.168.0.2", "192.168.0.3"]; - let ips = ["1.2.3.1", "1.2.3.2", "1.2.3.3"]; - for i in 0..3 { - let ext_ipstr = ips[i]; - if let Some(loc_ip) = lan.static_local_ips.get(ext_ipstr) { - assert_eq!(local_ips[i], loc_ip.to_string(), "should match"); - } else { - panic!("should have the external IP"); - } - } - - // bad local IP - let result = Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1/192.168.0.1".to_owned(), - "1.2.3.2/bad".to_owned(), // <-- invalid local IP - ], - ..Default::default() - }); - assert!(result.is_err(), "should fail"); - - // local IP out of CIDR - let result = Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1/192.168.0.1".to_owned(), - "1.2.3.2/172.16.1.2".to_owned(), // <-- out of CIDR - ], - ..Default::default() - }); - assert!(result.is_err(), "should fail"); - - // num of local IPs mismatch - let result = Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1/192.168.0.1".to_owned(), - "1.2.3.2".to_owned(), // <-- lack of local IP - ], - ..Default::default() - }); - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_router_static_ips_1to1_nat() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "0.0.0.0/0".to_owned(), - ..Default::default() - })?)); - - let lan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "1.2.3.1/192.168.0.1".to_owned(), - "1.2.3.2/192.168.0.2".to_owned(), - "1.2.3.3/192.168.0.3".to_owned(), - ], - nat_type: Some(NatType { - mode: NatMode::Nat1To1, - ..Default::default() - }), - ..Default::default() - })?)); - - { - let mut w = wan.lock().await; - w.add_router(Arc::clone(&lan)).await?; - } - { - let n = lan.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - { - let l = lan.lock().await; - let ri = l.router_internal.lock().await; - - assert_eq!(ri.nat.mapped_ips.len(), 3, "should be 3"); - assert_eq!(ri.nat.mapped_ips[0].to_string(), "1.2.3.1", "should match"); - assert_eq!(ri.nat.mapped_ips[1].to_string(), "1.2.3.2", "should match"); - assert_eq!(ri.nat.mapped_ips[2].to_string(), "1.2.3.3", "should match"); - - assert_eq!(3, ri.nat.local_ips.len(), "should be 3"); - assert_eq!( - ri.nat.local_ips[0].to_string(), - "192.168.0.1", - "should match" - ); - assert_eq!( - ri.nat.local_ips[1].to_string(), - "192.168.0.2", - "should match" - ); - assert_eq!( - ri.nat.local_ips[2].to_string(), - "192.168.0.3", - "should match" - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_router_failures_stop() -> Result<()> { - let mut r = Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?; - - let result = r.stop().await; - assert!(result.is_err(), "should fail"); - - Ok(()) -} - -#[tokio::test] -async fn test_router_failures_add_net() -> Result<()> { - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - - let net = Net::new(Some(NetConfig { - static_ips: vec![ - "5.6.7.8".to_owned(), // out of parent router'c CIDR - ], - ..Default::default() - })); - - { - let nic = net.get_nic()?; - let mut w = wan.lock().await; - let result = w.add_net(nic).await; - assert!(result.is_err(), "should fail"); - } - - Ok(()) -} - -#[tokio::test] -async fn test_router_failures_add_router() -> Result<()> { - let r1 = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - - let r2 = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "192.168.0.0/24".to_owned(), - static_ips: vec![ - "5.6.7.8".to_owned(), // out of parent router'c CIDR - ], - ..Default::default() - })?)); - - { - let mut r = r1.lock().await; - let result = r.add_router(Arc::clone(&r2)).await; - assert!(result.is_err(), "should fail"); - } - - Ok(()) -} diff --git a/webrtc/CHANGELOG.md b/webrtc/CHANGELOG.md deleted file mode 100644 index 2947f5afd..000000000 --- a/webrtc/CHANGELOG.md +++ /dev/null @@ -1,236 +0,0 @@ -# webrtc-rs changelog - -## Unreleased - -## v0.7.0 - -* Added support for insecure/deprecated signature verification algorithms, opt in via `SettingsEngine::allow_insecure_verification_algorithm` [#342](https://github.com/webrtc-rs/webrtc/pull/342). -* Make RTCRtpCodecCapability::payloader_for_codec public API [#349](https://github.com/webrtc-rs/webrtc/pull/349). -* Fixed a panic in `calculate_rtt_ms` [#350](https://github.com/webrtc-rs/webrtc/pull/350). -* Fixed `TrackRemote` missing at least the first, sometimes more, RTP packet during probing. [#387](https://github.com/webrtc-rs/webrtc/pull/387) - -### Breaking changes - -* Change `RTCPeerConnection::on_track` callback signature to `|track: Arc, receiver: Arc, transceiver: Arc|` [#355](https://github.com/webrtc-rs/webrtc/pull/355). - -* Change `RTCRtpSender::new` signature to `|receive_mtu: usize, track: Option>, transport: Arc, media_engine: Arc, interceptor: Arc, start_paused: bool,|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `API::new_rtp_sender` signature to `|&self, track: Option>, transport: Arc, interceptor: Arc,|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCRtpTransceiver::sender` signature to `|&self| -> Arc` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCRtpTransceiver::set_sender_track` signature to `|self: &Arc, sender: Arc, track: Option>,|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCRtpTransceiver::set_sender` signature to `|self: &Arc, s: Arc|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCRtpTransceiver::receiver` signature to `|&self| -> Arc` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCRtpTransceiver::set_receiver` signature to `|&self, r: Arc|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCPeerConnection::add_transceiver_from_kind` signature to `|&self, kind: RTPCodecType, init: Option,|`, `RTCRtpTransceiver::RTCRtpSender` сreated without a track [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCPeerConnection::add_transceiver_from_track` signature to `|&self, track: Arc, init: Option,|` [#377](https://github.com/webrtc-rs/webrtc/pull/377). - -* Change `RTCPeerConnection::mid` return signature to `Option` [#375](https://github.com/webrtc-rs/webrtc/pull/375). - -* Make functions non-async [#402](https://github.com/webrtc-rs/webrtc/pull/402): - - `MediaEngine`: - - `get_codecs_by_kind`; - - `get_rtp_parameters_by_kind`. - - `RTCRtpTransceiver`: - - `sender`; - - `set_sender`; - - `receiver`. - - `RTPReceiverInternal`: - - `set_transceiver_codecs`; - - `get_codecs`. - - `RTCRtpSender`: - - `set_rtp_transceiver`; - - `has_sent`. - - `TrackRemote`: - - `id`; - - `set_id`; - - `stream_id`; - - `set_stream_id`; - - `msid`; - - `codec`; - - `set_codec`; - - `params`; - - `set_params`; - - `onmute`; - - `onunmute`. - -* Change `RTPReader::read` signature to `|&self, buf: &mut [u8], attributes: &Attributes| -> Result<(rtp::packet::Packet, Attributes)>` [#450](https://github.com/webrtc-rs/webrtc/pull/450). - -* Change `RTCPReader::read` signature to `|&self, buf: &mut [u8], attributes: &Attributes| -> Result<(Vec>, Attributes)>` [#450](https://github.com/webrtc-rs/webrtc/pull/450). - -## v0.6.0 - -* Added more stats to `RemoteInboundRTPStats` and `RemoteOutboundRTPStats` [#282](https://github.com/webrtc-rs/webrtc/pull/282) by [@k0nserv](https://github.com/k0nserv). -* Don't register `video/rtx` codecs in `MediaEngine::register_default_codecs`. These weren't actually support and prevented RTX in the existing RTP stream from being used. Long term we should support RTX via this method, this is tracked in [#295](https://github.com/webrtc-rs/webrtc/issues/295). [#294 Remove video/rtx codecs](https://github.com/webrtc-rs/webrtc/pull/294) contributed by [k0nserv](https://github.com/k0nserv) -* Add IP filter to WebRTC `SettingEngine` [#306](https://github.com/webrtc-rs/webrtc/pull/306) -* Stop sequence numbers from increasing in `TrackLocalStaticSample` while the bound `RTCRtpSender` have -directions that should not send. [#316](https://github.com/webrtc-rs/webrtc/pull/316) -* Add support for a mime type "audio/telephone-event" (rfc4733) [#322](https://github.com/webrtc-rs/webrtc/pull/322) -* Fixed a panic that would sometimes happen when collecting stats. [#327](https://github.com/webrtc-rs/webrtc/pull/327) by [@k0nserv](https://github.com/k0nserv). -* Added new extension marshaller/unmarshaller for VideoOrientation, and made marshallers serializable via serde [#331](https://github.com/webrtc-rs/webrtc/pull/331) [#332](https://github.com/webrtc-rs/webrtc/pull/332) -* Updated minimum rust version to `1.60.0` -* Added a new `write_rtp_with_extensions` method to `TrackLocalStaticSample` and `TrackLocalStaticRTP`. [#336](https://github.com/webrtc-rs/webrtc/pull/336) by [@k0nserv](https://github.com/k0nserv). -* Added a new `sample_writer` helper to `TrackLocalStaticSample`. [#336](https://github.com/webrtc-rs/webrtc/pull/336) by [@k0nserv](https://github.com/k0nserv). -* Increased minimum versions for sub-dependencies: - * `webrtc-data` version to `0.6.0`. - * `webrtc-ice` version to `0.9.0`. - * `webrtc-media` version to `0.5.0`. - * `webrtc-sctp` version to `0.7.0`. - * `webrtc-util` version to `0.7.0`. - -### Breaking changes - -* Allowed one single direction for extmap matching. [#321](https://github.com/webrtc-rs/webrtc/pull/321). API change for `MediaEngine::register_header_extension`. -* Removed support for Plan-B. All major implementations of WebRTC now support unified and continuing support for plan-b is an undue maintenance burden when unified can be used. See [“Unified Plan” Transition Guide (JavaScript)](https://docs.google.com/document/d/1-ZfikoUtoJa9k-GZG1daN0BU3IjIanQ_JSscHxQesvU/) for an overview of the changes required to migrate. [#320](https://github.com/webrtc-rs/webrtc/pull/320) by [@algesten](https://github.com/algesten). -* Removed 2nd argument from `RTCCertificate::from_pem` and guard it with `pem` feature [#333] -* Renamed `RTCCertificate::pem` to `serialize_pem` and guard it with `pem` feature [#333] -* Removed `RTCCertificate::expires` [#333] -* `RTCCertificate::get_fingerprints` no longer returns `Result` [#333] -* Make functions non-async [#338](https://github.com/webrtc-rs/webrtc/pull/338): - - `RTCDataChannel`: - - `on_open`; - - `on_close`; - - `on_message`; - - `on_error`. - - `RTCDtlsTransport::on_state_change`; - - `RTCIceCandidate::to_json`; - - `RTCIceGatherer`: - - `on_local_candidate`; - - `on_state_change`; - - `on_gathering_complete`. - - `RTCIceTransport`: - - `get_selected_candidate_pair`; - - `on_selected_candidate_pair_change`; - - `on_connection_state_change`. - - `RTCPeerConnection`: - - `on_signaling_state_change`; - - `on_data_channel`; - - `on_negotiation_needed`; - - `on_ice_candidate`; - - `on_ice_gathering_state_change`; - - `on_track`; - - `on_ice_connection_state_change`; - - `on_peer_connection_state_change`. - - `RTCSctpTransport`: - - `on_error`; - - `on_data_channel`; - - `on_data_channel_opened`. - -[#333]: https://github.com/webrtc-rs/webrtc/pull/333 - -## v0.5.1 - -* Promote agent lock in ice_gather.rs create_agent() to top level of the function to avoid a race condition. [#290 Promote create_agent lock to top of function, to avoid race condition](https://github.com/webrtc-rs/webrtc/pull/290) contributed by [efer-ms](https://github.com/efer-ms) - -## v0.5.0 - -### Changes - -#### Breaking changes - -* The serialized format for `RTCIceCandidateInit` has changed to match what the specification i.e. keys are camelCase. [#153 Make RTCIceCandidateInit conform to WebRTC spec](https://github.com/webrtc-rs/webrtc/pull/153) contributed by [jmatss](https://github.com/jmatss). -* Improved robustness when proposing RTP extension IDs and handling of collisions in these. This change is only breaking if you have assumed anything about the nature of these extension IDs. [#154 Fix RTP extension id collision](https://github.com/webrtc-rs/webrtc/pull/154) contributed by [k0nserv](https://github.com/k0nserv) -* Transceivers will now not stop when either or both directions are disabled. That is, applying and SDP with `a=inactive` will not stop the transceiver, instead attached senders and receivers will pause. A transceiver can be resurrected by setting direction back to e.g. `a=sendrecv`. The desired direction can be controlled with the newly introduced public method `set_direction` on `RTCRtpTransceiver`. - * [#201 Handle inactive transceivers more correctly](https://github.com/webrtc-rs/webrtc/pull/201) contributed by [k0nserv](https://github.com/k0nserv) - * [#210 Rework transceiver direction support further](https://github.com/webrtc-rs/webrtc/pull/210) contributed by [k0nserv](https://github.com/k0nserv) - * [#214 set_direction add missing Send + Sync bound](https://github.com/webrtc-rs/webrtc/pull/214) contributed by [algesten](https://github.com/algesten) - * [#213 set_direction add missing Sync bound](https://github.com/webrtc-rs/webrtc/pull/213) contributed by [algesten](https://github.com/algesten) - * [#212 Public RTCRtpTransceiver::set_direction](https://github.com/webrtc-rs/webrtc/pull/212) contributed by [algesten](https://github.com/algesten) - * [#268 Fix current direction update when applying answer](https://github.com/webrtc-rs/webrtc/pull/268) contributed by [k0nserv](https://github.com/k0nserv) - * [#236 Pause RTP writing if direction indicates it](https://github.com/webrtc-rs/webrtc/pull/236) contributed by [algesten](https://github.com/algesten) -* Generated the `a=msid` line for `m=` line sections according to the specification. This might be break remote peers that relied on the previous, incorrect, behaviour. This also fixes a bug where an endless negotiation loop could happen. [#217 Correct msid handling for RtpSender](https://github.com/webrtc-rs/webrtc/pull/217) contributed by [k0nserv](https://github.com/k0nserv) -* Improve data channel id negotiation. We've slightly adjust the public interface for creating pre-negotiated data channels. Instead of a separate `negotiated: Option` and `id: Option` in `RTCDataChannelInit` there's now a more idiomatic `negotiated: Option`. If you have a pre-negotiated data channel simply set `negotiated: Some(id)` when creating the data channel. - * [#237 Fix datachannel id setting for 0.5.0 release](https://github.com/webrtc-rs/webrtc/pull/237) contributed by [stuqdog](https://github.com/stuqdog) - * [#229 Revert "base id updating on whether it's been negotiated, not on its …](https://github.com/webrtc-rs/webrtc/pull/229) contributed by [melekes](https://github.com/melekes) - - * [#226 base id updating on whether it's been finalized, not on its value](https://github.com/webrtc-rs/webrtc/pull/226) contributed by [stuqdog](https://github.com/stuqdog) - - -#### Other improvememnts - -We made various improvements and fixes since 0.4.0, including merging all subcrates into a single git repo. The old crate repos are archived and all development will now happen in https://github.com/webrtc-rs/webrtc/. - -* We now provide stats reporting via the standardized `RTCPeerConnection::get_stats` method. - * [#277 Implement Remote Inbound Stats](https://github.com/webrtc-rs/webrtc/pull/277) contributed by [k0nserv](https://github.com/k0nserv) - * [#220 Make stats types pub so they can be used directly](https://github.com/webrtc-rs/webrtc/pull/220) contributed by [k0nserv](https://github.com/k0nserv) - * [#225 Add RTP Stats to stats report](https://github.com/webrtc-rs/webrtc/pull/225) contributed by [k0nserv](https://github.com/k0nserv) - * [#189 Serialize stats](https://github.com/webrtc-rs/webrtc/pull/189) contributed by [sax](https://github.com/sax) - * [#180 Get stats from peer connection](https://github.com/webrtc-rs/webrtc/pull/180) contributed by [sax](https://github.com/sax) - -* [#278 Fix async-global-executor](https://github.com/webrtc-rs/webrtc/pull/278) contributed by [k0nserv](https://github.com/k0nserv) -* [#276 relax regex version requirement](https://github.com/webrtc-rs/webrtc/pull/276) contributed by [melekes](https://github.com/melekes) -* [#244 Update README.md instructions after monorepo merge](https://github.com/webrtc-rs/webrtc/pull/244) contributed by [k0nserv](https://github.com/k0nserv) -* [#241 move profile to workspace](https://github.com/webrtc-rs/webrtc/pull/241) contributed by [xnorpx](https://github.com/xnorpx) -* [#240 Increase timeout to "fix" test breaking](https://github.com/webrtc-rs/webrtc/pull/240) contributed by [algesten](https://github.com/algesten) -* [#239 One repo (again)](https://github.com/webrtc-rs/webrtc/pull/239) contributed by [algesten](https://github.com/algesten) -* [#234 Fix recent clippy lints](https://github.com/webrtc-rs/webrtc/pull/234) contributed by [k0nserv](https://github.com/k0nserv) -* [#224 update call to DataChannel::accept as per data pr #14](https://github.com/webrtc-rs/webrtc/pull/224) contributed by [melekes](https://github.com/melekes) -* [#223 dtls_transport: always set remote certificate](https://github.com/webrtc-rs/webrtc/pull/223) contributed by [melekes](https://github.com/melekes) -* [#216 Lower case mime types for comparison in fmpt lines](https://github.com/webrtc-rs/webrtc/pull/216) contributed by [k0nserv](https://github.com/k0nserv) -* [#211 Helper to trigger negotiation_needed](https://github.com/webrtc-rs/webrtc/pull/211) contributed by [algesten](https://github.com/algesten) -* [#209 MID generator feature](https://github.com/webrtc-rs/webrtc/pull/209) contributed by [algesten](https://github.com/algesten) -* [#208 update deps + loosen some requirements](https://github.com/webrtc-rs/webrtc/pull/208) contributed by [melekes](https://github.com/melekes) -* [#205 data_channel: handle stream EOF](https://github.com/webrtc-rs/webrtc/pull/205) contributed by [melekes](https://github.com/melekes) -* [#204 [peer_connection] allow persistent certificates](https://github.com/webrtc-rs/webrtc/pull/204) contributed by [melekes](https://github.com/melekes) -* [#202 bugfix-Udp connection not close (reopen #174) #195](https://github.com/webrtc-rs/webrtc/pull/202) contributed by [shiqifeng2000](https://github.com/shiqifeng2000) -* [#199 Upgrade ICE to 0.7.0](https://github.com/webrtc-rs/webrtc/pull/199) contributed by [k0nserv](https://github.com/k0nserv) -* [#194 Add AV1 MimeType and RtpCodecParameters](https://github.com/webrtc-rs/webrtc/pull/194) contributed by [billylindeman](https://github.com/billylindeman) -* [#188 Improve operations debuggability](https://github.com/webrtc-rs/webrtc/pull/188) contributed by [k0nserv](https://github.com/k0nserv) -* [#187 Fix SDP for rejected tracks to conform to RFC](https://github.com/webrtc-rs/webrtc/pull/187) contributed by [k0nserv](https://github.com/k0nserv) -* [#185 Adding some debug and display traits](https://github.com/webrtc-rs/webrtc/pull/185) contributed by [sevensidedmarble](https://github.com/sevensidedmarble) -* [#179 Fix example names in README](https://github.com/webrtc-rs/webrtc/pull/179) contributed by [ethagnawl](https://github.com/ethagnawl) -* [#176 Time overflow armv7 workaround](https://github.com/webrtc-rs/webrtc/pull/176) contributed by [frjol](https://github.com/frjol) -* [#171 close DTLS conn upon err](https://github.com/webrtc-rs/webrtc/pull/171) contributed by [melekes](https://github.com/melekes) -* [#170 always start sctp](https://github.com/webrtc-rs/webrtc/pull/170) contributed by [melekes](https://github.com/melekes) -* [#167 Add offer/answer/pranswer constructors for RTCSessionDescription](https://github.com/webrtc-rs/webrtc/pull/167) contributed by [sax](https://github.com/sax) - -#### Subcrate updates - -The various sub-crates have been updated as follows: - -* util: 0.5.3 => 0.6.0 -* sdp: 0.5.1 => 0.5.2 -* mdns: 0.4.2 => 0.5.0 -* stun: 0.4.2 => 0.4.3 -* turn: 0.5.3 => 0.6.0 -* ice: 0.6.4 => 0.8.0 -* dtls: 0.5.2 => 0.6.0 -* rtcp: 0.6.5 => 0.7.0 -* rtp: 0.6.5 => 0.6.7 -* srtp: 0.8.9 => 0.9.0 -* scpt: 0.4.3 => 0.6.1 -* data: 0.3.3 => 0.5.0 -* interceptor: 0.7.6 => 0.8.0 -* media: 0.4.5 => 0.4.7 - -Their respective change logs are found in the old, now archived, repositories and within their respective `CHANGELOG.md` files in the monorepo. - -### Contributors - -A big thanks to all the contributors that have made this release happen: - -* [morajabi](https://github.com/morajabi) -* [sax](https://github.com/sax) -* [ethagnawl](https://github.com/ethagnawl) -* [xnorpx](https://github.com/xnorpx) -* [frjol](https://github.com/frjol) -* [algesten](https://github.com/algesten) -* [shiqifeng2000](https://github.com/shiqifeng2000) -* [billylindeman](https://github.com/billylindeman) -* [sevensidedmarble](https://github.com/sevensidedmarble) -* [k0nserv](https://github.com/k0nserv) -* [stuqdog](https://github.com/stuqdog) -* [neonphog](https://github.com/neonphog) -* [melekes](https://github.com/melekes) -* [jmatss](https://github.com/jmatss) - - -## Prior to 0.5.0 - -Before 0.5.0 there was no changelog, previous changes are sometimes, but not always, available in the [GitHub Releases](https://github.com/webrtc-rs/webrtc/releases). diff --git a/webrtc/Cargo.toml b/webrtc/Cargo.toml deleted file mode 100644 index 25cb56210..000000000 --- a/webrtc/Cargo.toml +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "webrtc" -version = "0.11.0" -authors = ["Rain Liu "] -edition = "2021" -description = "A pure Rust implementation of WebRTC API" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/webrtc" -homepage = "https://webrtc.rs" -repository = "https://github.com/webrtc-rs/webrtc" -readme = "../README.md" - -[dependencies] -data = { version = "0.9.0", path = "../data", package = "webrtc-data" } -dtls = { version = "0.10.0", path = "../dtls", package = "webrtc-dtls" } -ice = { version = "0.11.0", path = "../ice", package = "webrtc-ice" } -interceptor = { version = "0.12.0", path = "../interceptor" } -mdns = { version = "0.7.0", path = "../mdns", package = "webrtc-mdns" } -media = { version = "0.8.0", path = "../media", package = "webrtc-media" } -rtcp = { version = "0.11.0", path = "../rtcp" } -rtp = { version = "0.11.0", path = "../rtp" } -sctp = { version = "0.10.0", path = "../sctp", package = "webrtc-sctp" } -sdp = { version = "0.6.2", path = "../sdp" } -srtp = { version = "0.13.0", path = "../srtp", package = "webrtc-srtp" } -stun = { version = "0.6.0", path = "../stun" } -turn = { version = "0.8.0", path = "../turn" } -util = { version = "0.9.0", path = "../util", package = "webrtc-util" } - -arc-swap = "1" -tokio = { version = "1.32.0", features = [ - "fs", - "io-util", - "io-std", - "macros", - "net", - "parking_lot", - "rt", - "rt-multi-thread", - "sync", - "time", -] } -log = "0.4" -async-trait = "0.1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -rand = "0.8" -bytes = "1" -thiserror = "1" -waitgroup = "0.1" -regex = "1.9.5" -smol_str = { version = "0.2", features = ["serde"] } -url = "2" -rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } -rcgen = { version = "0.13", features = ["pem", "x509-parser"]} -ring = "0.17" -sha2 = "0.10" -lazy_static = "1.4" -hex = "0.4" -pem = { version = "3", optional = true } -time = "0.3" -cfg-if = "1" -portable-atomic = "1.6" - -[dev-dependencies] -tokio-test = "0.4" -env_logger = "0.10" - -[features] -pem = ["dep:pem", "dtls/pem"] -openssl = ["srtp/openssl"] -vendored-openssl = ["srtp/vendored-openssl"] diff --git a/webrtc/src/api/api_test.rs b/webrtc/src/api/api_test.rs deleted file mode 100644 index c6b1aa54a..000000000 --- a/webrtc/src/api/api_test.rs +++ /dev/null @@ -1,25 +0,0 @@ -use super::*; - -#[test] -fn test_new_api() -> Result<()> { - let mut s = SettingEngine::default(); - s.detach_data_channels(); - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let api = APIBuilder::new() - .with_setting_engine(s) - .with_media_engine(m) - .build(); - - assert!( - api.setting_engine.detach.data_channels, - "Failed to set settings engine" - ); - assert!( - !api.media_engine.audio_codecs.is_empty(), - "Failed to set media engine" - ); - - Ok(()) -} diff --git a/webrtc/src/api/interceptor_registry/interceptor_registry_test.rs b/webrtc/src/api/interceptor_registry/interceptor_registry_test.rs deleted file mode 100644 index 277bce5b6..000000000 --- a/webrtc/src/api/interceptor_registry/interceptor_registry_test.rs +++ /dev/null @@ -1,278 +0,0 @@ -/*TODO: -use super::*; -use crate::api::APIBuilder; -use crate::peer_connection::configuration::RTCConfiguration; - -use bytes::Bytes; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use interceptor::mock::mock_builder::MockBuilder; -use interceptor::mock::mock_interceptor::MockInterceptor; -use interceptor::stream_info::StreamInfo; -use interceptor::{Attributes, Interceptor, RTPWriter, RTPWriterFn}; - -// E2E test of the features of Interceptors -// * Assert an extension can be set on an outbound packet -// * Assert an extension can be read on an outbound packet -// * Assert that attributes set by an interceptor are returned to the Reader -#[tokio::test] -async fn test_peer_connection_interceptor() -> Result<()> { - let create_pc = || async { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let mut ir = Registry::new(); - - let BindLocalStreamFn = |info: &StreamInfo, - writer: Arc| - -> Pin< - Box> + Send + Sync>, - > { - let writer2 = Arc::clone(&writer); - Box::pin(async move { - Arc::new(RTPWriterFn(Box::new( - move |in_pkt: &rtp::packet::Packet, - attributes: &Attributes| - -> Pin< - Box< - dyn Future> - + Send - + Sync, - >, - > { - let writer3 = Arc::clone(&writer2); - let a = attributes.clone(); - // set extension on outgoing packet - let mut out_pkt = in_pkt.clone(); - out_pkt.header.extension = true; - out_pkt.header.extension_profile = 0xBEDE; - - Box::pin(async move { - out_pkt - .header - .set_extension(2, Bytes::from_static(b"foo"))?; - //writer3.write(&out_pkt, &a).await - Ok(0) - }) - }, - ))) as Arc - }) - }; - - BindRemoteStreamFn: func(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { - return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { - if a == nil { - a = interceptor.Attributes{} - } - - a.Set("attribute", "value") - return reader.Read(b, a) - }) - }, - let mock_builder = Box::new(MockBuilder { - build: - Box::new( - |_: &str| -> std::result::Result< - Arc, - interceptor::Error, - > { - Ok(Arc::new(MockInterceptor { - ..Default::default() - })) - }, - ), - }); - let mock_builder = MockBuilder::new( - |_: &str| -> std::result::Result< - Arc, - interceptor::Error, - > { - Ok(Arc::new(MockInterceptor { - ..Default::default() - })) - }, - ); - ir.add(Box::new(mock_builder)); - - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(ir) - .build(); - api.new_peer_connection(RTCConfiguration::default()).await - }; - - let offerer = create_pc().await?; - let answerer = create_pc().await?; - - track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") - assert.NoError(t, err) - - _, err = offerer.AddTrack(track) - assert.NoError(t, err) - - seenRTP, seenRTPCancel := context.WithCancel(context.Background()) - answerer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) { - p, attributes, readErr := track.ReadRTP() - assert.NoError(t, readErr) - - assert.Equal(t, p.Extension, true) - assert.Equal(t, "foo", string(p.GetExtension(2))) - assert.Equal(t, "value", attributes.Get("attribute")) - - seenRTPCancel() - }) - - assert.NoError(t, signalPair(offerer, answerer)) - - func() { - ticker := time.NewTicker(time.Millisecond * 20) - for { - select { - case <-seenRTP.Done(): - return - case <-ticker.C: - assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) - } - } - }() - - closePairNow(t, offerer, answerer) - - Ok(()) -} - -func Test_Interceptor_BindUnbind(t *testing.T) { - lim := test.TimeOut(time.Second * 10) - defer lim.Stop() - - report := test.CheckRoutines(t) - defer report() - - m := &MediaEngine{} - assert.NoError(t, m.RegisterDefaultCodecs()) - - var ( - cntBindRTCPReader uint32 - cntBindRTCPWriter uint32 - cntBindLocalStream uint32 - cntUnbindLocalStream uint32 - cntBindRemoteStream uint32 - cntUnbindRemoteStream uint32 - cntClose uint32 - ) - mockInterceptor := &mock_interceptor.Interceptor{ - BindRTCPReaderFn: func(reader interceptor.RTCPReader) interceptor.RTCPReader { - atomic.AddUint32(&cntBindRTCPReader, 1) - return reader - }, - BindRTCPWriterFn: func(writer interceptor.RTCPWriter) interceptor.RTCPWriter { - atomic.AddUint32(&cntBindRTCPWriter, 1) - return writer - }, - BindLocalStreamFn: func(i *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { - atomic.AddUint32(&cntBindLocalStream, 1) - return writer - }, - UnbindLocalStreamFn: func(i *interceptor.StreamInfo) { - atomic.AddUint32(&cntUnbindLocalStream, 1) - }, - BindRemoteStreamFn: func(i *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { - atomic.AddUint32(&cntBindRemoteStream, 1) - return reader - }, - UnbindRemoteStreamFn: func(i *interceptor.StreamInfo) { - atomic.AddUint32(&cntUnbindRemoteStream, 1) - }, - CloseFn: func() error { - atomic.AddUint32(&cntClose, 1) - return nil - }, - } - ir := &interceptor.Registry{} - ir.Add(&mock_interceptor.Factory{ - NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { return mockInterceptor, nil }, - }) - - sender, receiver, err := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(ir)).newPair(Configuration{}) - assert.NoError(t, err) - - track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") - assert.NoError(t, err) - - _, err = sender.AddTrack(track) - assert.NoError(t, err) - - receiverReady, receiverReadyFn := context.WithCancel(context.Background()) - receiver.OnTrack(func(track *TrackRemote, _ *RTPReceiver) { - _, _, readErr := track.ReadRTP() - assert.NoError(t, readErr) - receiverReadyFn() - }) - - assert.NoError(t, signalPair(sender, receiver)) - - ticker := time.NewTicker(time.Millisecond * 20) - defer ticker.Stop() - func() { - for { - select { - case <-receiverReady.Done(): - return - case <-ticker.C: - // Send packet to make receiver track actual creates RTPReceiver. - assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) - } - } - }() - - closePairNow(t, sender, receiver) - - // Bind/UnbindLocal/RemoteStream should be called from one side. - if cnt := atomic.LoadUint32(&cntBindLocalStream); cnt != 1 { - t.Errorf("BindLocalStreamFn is expected to be called once, but called %d times", cnt) - } - if cnt := atomic.LoadUint32(&cntUnbindLocalStream); cnt != 1 { - t.Errorf("UnbindLocalStreamFn is expected to be called once, but called %d times", cnt) - } - if cnt := atomic.LoadUint32(&cntBindRemoteStream); cnt != 1 { - t.Errorf("BindRemoteStreamFn is expected to be called once, but called %d times", cnt) - } - if cnt := atomic.LoadUint32(&cntUnbindRemoteStream); cnt != 1 { - t.Errorf("UnbindRemoteStreamFn is expected to be called once, but called %d times", cnt) - } - - // BindRTCPWriter/Reader and Close should be called from both side. - if cnt := atomic.LoadUint32(&cntBindRTCPWriter); cnt != 2 { - t.Errorf("BindRTCPWriterFn is expected to be called twice, but called %d times", cnt) - } - if cnt := atomic.LoadUint32(&cntBindRTCPReader); cnt != 2 { - t.Errorf("BindRTCPReaderFn is expected to be called twice, but called %d times", cnt) - } - if cnt := atomic.LoadUint32(&cntClose); cnt != 2 { - t.Errorf("CloseFn is expected to be called twice, but called %d times", cnt) - } -} - -func Test_InterceptorRegistry_Build(t *testing.T) { - registryBuildCount := 0 - - ir := &interceptor.Registry{} - ir.Add(&mock_interceptor.Factory{ - NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { - registryBuildCount++ - return &interceptor.NoOp{}, nil - }, - }) - - peerConnectionA, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{}) - assert.NoError(t, err) - - peerConnectionB, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{}) - assert.NoError(t, err) - - assert.Equal(t, 2, registryBuildCount) - closePairNow(t, peerConnectionA, peerConnectionB) -} -*/ diff --git a/webrtc/src/api/interceptor_registry/mod.rs b/webrtc/src/api/interceptor_registry/mod.rs deleted file mode 100644 index ca4f54904..000000000 --- a/webrtc/src/api/interceptor_registry/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -#[cfg(test)] -mod interceptor_registry_test; - -use interceptor::nack::generator::Generator; -use interceptor::nack::responder::Responder; -use interceptor::registry::Registry; -use interceptor::report::receiver::ReceiverReport; -use interceptor::report::sender::SenderReport; -use interceptor::twcc::receiver::Receiver; -use interceptor::twcc::sender::Sender; - -use crate::api::media_engine::MediaEngine; -use crate::error::Result; -use crate::rtp_transceiver::rtp_codec::{RTCRtpHeaderExtensionCapability, RTPCodecType}; -use crate::rtp_transceiver::{RTCPFeedback, TYPE_RTCP_FB_TRANSPORT_CC}; - -/// register_default_interceptors will register some useful interceptors. -/// If you want to customize which interceptors are loaded, you should copy the -/// code from this method and remove unwanted interceptors. -pub fn register_default_interceptors( - mut registry: Registry, - media_engine: &mut MediaEngine, -) -> Result { - registry = configure_nack(registry, media_engine); - - registry = configure_rtcp_reports(registry); - - registry = configure_twcc_receiver_only(registry, media_engine)?; - - Ok(registry) -} - -/// configure_rtcp_reports will setup everything necessary for generating Sender and Receiver Reports -pub fn configure_rtcp_reports(mut registry: Registry) -> Registry { - let receiver = Box::new(ReceiverReport::builder()); - let sender = Box::new(SenderReport::builder()); - registry.add(receiver); - registry.add(sender); - registry -} - -/// configure_nack will setup everything necessary for handling generating/responding to nack messages. -pub fn configure_nack(mut registry: Registry, media_engine: &mut MediaEngine) -> Registry { - media_engine.register_feedback( - RTCPFeedback { - typ: "nack".to_owned(), - parameter: "".to_owned(), - }, - RTPCodecType::Video, - ); - media_engine.register_feedback( - RTCPFeedback { - typ: "nack".to_owned(), - parameter: "pli".to_owned(), - }, - RTPCodecType::Video, - ); - - let generator = Box::new(Generator::builder()); - let responder = Box::new(Responder::builder()); - registry.add(responder); - registry.add(generator); - registry -} - -/// configure_twcc will setup everything necessary for adding -/// a TWCC header extension to outgoing RTP packets and generating TWCC reports. -pub fn configure_twcc(mut registry: Registry, media_engine: &mut MediaEngine) -> Result { - media_engine.register_feedback( - RTCPFeedback { - typ: TYPE_RTCP_FB_TRANSPORT_CC.to_owned(), - ..Default::default() - }, - RTPCodecType::Video, - ); - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - - media_engine.register_feedback( - RTCPFeedback { - typ: TYPE_RTCP_FB_TRANSPORT_CC.to_owned(), - ..Default::default() - }, - RTPCodecType::Audio, - ); - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - let sender = Box::new(Sender::builder()); - let receiver = Box::new(Receiver::builder()); - registry.add(sender); - registry.add(receiver); - Ok(registry) -} - -/// configure_twcc_sender will setup everything necessary for adding -/// a TWCC header extension to outgoing RTP packets. This will allow the remote peer to generate TWCC reports. -pub fn configure_twcc_sender_only( - mut registry: Registry, - media_engine: &mut MediaEngine, -) -> Result { - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - let sender = Box::new(Sender::builder()); - registry.add(sender); - Ok(registry) -} - -/// configure_twcc_receiver will setup everything necessary for generating TWCC reports. -pub fn configure_twcc_receiver_only( - mut registry: Registry, - media_engine: &mut MediaEngine, -) -> Result { - media_engine.register_feedback( - RTCPFeedback { - typ: TYPE_RTCP_FB_TRANSPORT_CC.to_owned(), - ..Default::default() - }, - RTPCodecType::Video, - ); - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - - media_engine.register_feedback( - RTCPFeedback { - typ: TYPE_RTCP_FB_TRANSPORT_CC.to_owned(), - ..Default::default() - }, - RTPCodecType::Audio, - ); - media_engine.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::TRANSPORT_CC_URI.to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - let receiver = Box::new(Receiver::builder()); - registry.add(receiver); - Ok(registry) -} diff --git a/webrtc/src/api/media_engine/media_engine_test.rs b/webrtc/src/api/media_engine/media_engine_test.rs deleted file mode 100644 index cbf3b503b..000000000 --- a/webrtc/src/api/media_engine/media_engine_test.rs +++ /dev/null @@ -1,780 +0,0 @@ -use std::io::Cursor; - -use regex::Regex; - -use super::*; -use crate::api::media_engine::MIME_TYPE_OPUS; -use crate::api::APIBuilder; -use crate::peer_connection::configuration::RTCConfiguration; - -#[tokio::test] -async fn test_opus_case() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let pc = api.new_peer_connection(RTCConfiguration::default()).await?; - pc.add_transceiver_from_kind(RTPCodecType::Audio, None) - .await?; - - let offer = pc.create_offer(None).await?; - - let re = Regex::new(r"(?m)^a=rtpmap:\d+ opus/48000/2").unwrap(); - assert!(re.is_match(offer.sdp.as_str())); - - pc.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_video_case() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let pc = api.new_peer_connection(RTCConfiguration::default()).await?; - pc.add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let offer = pc.create_offer(None).await?; - - let re = Regex::new(r"(?m)^a=rtpmap:\d+ H264/90000").unwrap(); - assert!(re.is_match(offer.sdp.as_str())); - let re = Regex::new(r"(?m)^a=rtpmap:\d+ VP8/90000").unwrap(); - assert!(re.is_match(offer.sdp.as_str())); - let re = Regex::new(r"(?m)^a=rtpmap:\d+ VP9/90000").unwrap(); - assert!(re.is_match(offer.sdp.as_str())); - - pc.close().await?; - - Ok(()) -} - -#[tokio::test] -async fn test_media_engine_remote_description() -> Result<()> { - let must_parse = |raw: &str| -> Result { - let mut reader = Cursor::new(raw.as_bytes()); - Ok(SessionDescription::unmarshal(&mut reader)?) - }; - - //"No Media" - { - const NO_MEDIA: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -"; - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.update_from_remote_description(&must_parse(NO_MEDIA)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(!m.negotiated_audio.load(Ordering::SeqCst)); - } - - //"Enable Opus" - { - const OPUS_SAME_PAYLOAD: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 111 -a=rtpmap:111 opus/48000/2 -a=fmtp:111 minptime=10; useinbandfec=1 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.update_from_remote_description(&must_parse(OPUS_SAME_PAYLOAD)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let (opus_codec, _) = m.get_codec_by_payload(111).await?; - assert_eq!(opus_codec.capability.mime_type, MIME_TYPE_OPUS); - } - - //"Change Payload Type" - { - const OPUS_SAME_PAYLOAD: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 112 -a=rtpmap:112 opus/48000/2 -a=fmtp:112 minptime=10; useinbandfec=1 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.update_from_remote_description(&must_parse(OPUS_SAME_PAYLOAD)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let result = m.get_codec_by_payload(111).await; - assert!(result.is_err()); - - let (opus_codec, _) = m.get_codec_by_payload(112).await?; - assert_eq!(opus_codec.capability.mime_type, MIME_TYPE_OPUS); - } - - //"Case Insensitive" - { - const OPUS_UPCASE: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 111 -a=rtpmap:111 OPUS/48000/2 -a=fmtp:111 minptime=10; useinbandfec=1 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.update_from_remote_description(&must_parse(OPUS_UPCASE)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let (opus_codec, _) = m.get_codec_by_payload(111).await?; - assert_eq!(opus_codec.capability.mime_type, "audio/OPUS"); - } - - //"Handle different fmtp" - { - const OPUS_NO_FMTP: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 111 -a=rtpmap:111 opus/48000/2 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.update_from_remote_description(&must_parse(OPUS_NO_FMTP)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let (opus_codec, _) = m.get_codec_by_payload(111).await?; - assert_eq!(opus_codec.capability.mime_type, MIME_TYPE_OPUS); - } - - //"Header Extensions" - { - const HEADER_EXTENSIONS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 111 -a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid -a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id -a=rtpmap:111 opus/48000/2 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - for extension in [ - "urn:ietf:params:rtp-hdrext:sdes:mid", - "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", - ] { - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: extension.to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - } - - m.update_from_remote_description(&must_parse(HEADER_EXTENSIONS)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let (abs_id, abs_audio_enabled, abs_video_enabled) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::ABS_SEND_TIME_URI.to_owned(), - }) - .await; - assert_eq!(abs_id, 0); - assert!(!abs_audio_enabled); - assert!(!abs_video_enabled); - - let (mid_id, mid_audio_enabled, mid_video_enabled) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::SDES_MID_URI.to_owned(), - }) - .await; - assert_eq!(mid_id, 7); - assert!(mid_audio_enabled); - assert!(!mid_video_enabled); - } - - //"Prefers exact codec matches" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 96 98 -a=rtpmap:96 H264/90000 -a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f -a=rtpmap:98 H264/90000 -a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f" - .to_string(), - rtcp_feedback: vec![], - }, - payload_type: 127, - ..Default::default() - }, - RTPCodecType::Video, - )?; - m.update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await?; - - assert!(m.negotiated_video.load(Ordering::SeqCst)); - assert!(!m.negotiated_audio.load(Ordering::SeqCst)); - - let (supported_h264, _) = m.get_codec_by_payload(98).await?; - assert_eq!(supported_h264.capability.mime_type, MIME_TYPE_H264); - - assert!(m.get_codec_by_payload(96).await.is_err()); - } - - //"Does not match when fmtpline is set and does not match" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 96 98 -a=rtpmap:96 H264/90000 -a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f" - .to_string(), - rtcp_feedback: vec![], - }, - payload_type: 127, - ..Default::default() - }, - RTPCodecType::Video, - )?; - assert!(m - .update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await - .is_err()); - - assert!(m.get_codec_by_payload(96).await.is_err()); - } - - //"Matches when fmtpline is not set in offer, but exists in mediaengine" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 96 -a=rtpmap:96 VP9/90000 -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=0".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 98, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await?; - - assert!(m.negotiated_video.load(Ordering::SeqCst)); - - m.get_codec_by_payload(96).await?; - } - - //"Matches when fmtpline exists in neither" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 96 -a=rtpmap:96 VP8/90000 -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await?; - - assert!(m.negotiated_video.load(Ordering::SeqCst)); - - m.get_codec_by_payload(96).await?; - } - - //"Matches when rtx apt for exact match codec" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 94 96 97 -a=rtpmap:94 VP8/90000 -a=rtpmap:96 VP9/90000 -a=fmtp:96 profile-id=2 -a=rtpmap:97 rtx/90000 -a=fmtp:97 apt=96 -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 94, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=2".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: "video/rtx".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "apt=96".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 97, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await?; - - assert!(m.negotiated_video.load(Ordering::SeqCst)); - - m.get_codec_by_payload(97).await?; - } - - //"Matches when rtx apt for partial match codec" - { - const PROFILE_LEVELS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=video 60323 UDP/TLS/RTP/SAVPF 94 96 97 -a=rtpmap:94 VP8/90000 -a=rtpmap:96 VP9/90000 -a=fmtp:96 profile-id=2 -a=rtpmap:97 rtx/90000 -a=fmtp:97 apt=96 -"; - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 94, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=1".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: "video/rtx".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "apt=96".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 97, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - m.update_from_remote_description(&must_parse(PROFILE_LEVELS)?) - .await?; - - assert!(m.negotiated_video.load(Ordering::SeqCst)); - - if let Err(err) = m.get_codec_by_payload(97).await { - assert_eq!(err, Error::ErrCodecNotFound); - } else { - panic!(); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_media_engine_header_extension_direction() -> Result<()> { - let register_codec = |m: &mut MediaEngine| -> Result<()> { - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - ) - }; - - //"No Direction" - { - let mut m = MediaEngine::default(); - register_codec(&mut m)?; - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: "webrtc-header-test".to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - let params = - m.get_rtp_parameters_by_kind(RTPCodecType::Audio, RTCRtpTransceiverDirection::Recvonly); - - assert_eq!(params.header_extensions.len(), 1); - } - - //"Same Direction" - { - let mut m = MediaEngine::default(); - register_codec(&mut m)?; - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: "webrtc-header-test".to_owned(), - }, - RTPCodecType::Audio, - Some(RTCRtpTransceiverDirection::Recvonly), - )?; - - let params = - m.get_rtp_parameters_by_kind(RTPCodecType::Audio, RTCRtpTransceiverDirection::Recvonly); - - assert_eq!(params.header_extensions.len(), 1); - } - - //"Different Direction" - { - let mut m = MediaEngine::default(); - register_codec(&mut m)?; - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: "webrtc-header-test".to_owned(), - }, - RTPCodecType::Audio, - Some(RTCRtpTransceiverDirection::Sendonly), - )?; - - let params = - m.get_rtp_parameters_by_kind(RTPCodecType::Audio, RTCRtpTransceiverDirection::Recvonly); - - assert_eq!(params.header_extensions.len(), 0); - } - - //"No direction and inactive" - { - let mut m = MediaEngine::default(); - register_codec(&mut m)?; - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: "webrtc-header-test".to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - let params = - m.get_rtp_parameters_by_kind(RTPCodecType::Audio, RTCRtpTransceiverDirection::Inactive); - - assert_eq!(params.header_extensions.len(), 1); - } - - Ok(()) -} - -/// If a user attempts to register a codec twice we should just discard duplicate calls -#[tokio::test] -async fn test_media_engine_double_register() -> Result<()> { - let mut m = MediaEngine::default(); - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - assert_eq!(m.audio_codecs.len(), 1); - Ok(()) -} - -async fn validate(m: &MediaEngine) -> Result<()> { - m.update_header_extension(2, "test-extension", RTPCodecType::Audio) - .await?; - - let (id, audio_negotiated, video_negotiated) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: "test-extension".to_owned(), - }) - .await; - assert_eq!(id, 2); - assert!(audio_negotiated); - assert!(!video_negotiated); - - Ok(()) -} - -/// The cloned MediaEngine instance should be able to update negotiated header extensions. -#[tokio::test] -async fn test_update_header_extension_to_cloned_media_engine() -> Result<()> { - let mut m = MediaEngine::default(); - - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTPCodecType::Audio, - )?; - - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: "test-extension".to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - - validate(&m).await?; - validate(&m.clone_to()).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_extension_id_collision() -> Result<()> { - let must_parse = |raw: &str| -> Result { - let mut reader = Cursor::new(raw.as_bytes()); - Ok(SessionDescription::unmarshal(&mut reader)?) - }; - - const HEADER_EXTENSIONS: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=audio 9 UDP/TLS/RTP/SAVPF 111 -a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level -a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id -a=rtpmap:111 opus/48000/2 -"; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - { - let extension = "urn:3gpp:video-orientation"; - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: extension.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - } - for extension in [ - "urn:ietf:params:rtp-hdrext:ssrc-audio-level", - "urn:ietf:params:rtp-hdrext:sdes:mid", - "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", - ] { - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: extension.to_owned(), - }, - RTPCodecType::Audio, - None, - )?; - } - - m.update_from_remote_description(&must_parse(HEADER_EXTENSIONS)?) - .await?; - - assert!(!m.negotiated_video.load(Ordering::SeqCst)); - assert!(m.negotiated_audio.load(Ordering::SeqCst)); - - let (abs_id, abs_audio_enabled, abs_video_enabled) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::ABS_SEND_TIME_URI.to_owned(), - }) - .await; - assert_eq!(abs_id, 0); - assert!(!abs_audio_enabled); - assert!(!abs_video_enabled); - - let (mid_id, mid_audio_enabled, mid_video_enabled) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::SDES_MID_URI.to_owned(), - }) - .await; - assert_eq!(mid_id, 7); - assert!(mid_audio_enabled); - assert!(!mid_video_enabled); - - let (mid_id, mid_audio_enabled, mid_video_enabled) = m - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: sdp::extmap::AUDIO_LEVEL_URI.to_owned(), - }) - .await; - assert_eq!(mid_id, 1); - assert!(mid_audio_enabled); - assert!(!mid_video_enabled); - - let params = - m.get_rtp_parameters_by_kind(RTPCodecType::Video, RTCRtpTransceiverDirection::Sendonly); - //dbg!(¶ms); - - let orientation = params - .header_extensions - .iter() - .find(|ext| ext.uri == "urn:3gpp:video-orientation") - .unwrap(); - assert_ne!(orientation.id, 1); - assert_ne!(orientation.id, 7); - assert_ne!(orientation.id, 5); - - Ok(()) -} diff --git a/webrtc/src/api/media_engine/mod.rs b/webrtc/src/api/media_engine/mod.rs deleted file mode 100644 index 82955534e..000000000 --- a/webrtc/src/api/media_engine/mod.rs +++ /dev/null @@ -1,819 +0,0 @@ -#[cfg(test)] -mod media_engine_test; - -use std::collections::HashMap; -use std::ops::Range; -use std::sync::atomic::Ordering; -use std::time::{SystemTime, UNIX_EPOCH}; - -use portable_atomic::AtomicBool; -use sdp::description::session::SessionDescription; -use util::sync::Mutex as SyncMutex; - -use crate::error::{Error, Result}; -use crate::peer_connection::sdp::{ - codecs_from_media_description, rtp_extensions_from_media_description, -}; -use crate::rtp_transceiver::rtp_codec::{ - codec_parameters_fuzzy_search, CodecMatch, RTCRtpCodecCapability, RTCRtpCodecParameters, - RTCRtpHeaderExtensionCapability, RTCRtpHeaderExtensionParameters, RTCRtpParameters, - RTPCodecType, -}; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::rtp_transceiver::{fmtp, PayloadType, RTCPFeedback}; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::CodecStats; -use crate::stats::StatsReportType::Codec; - -/// MIME_TYPE_H264 H264 MIME type. -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_H264: &str = "video/H264"; -/// MIME_TYPE_HEVC HEVC MIME type. -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_HEVC: &str = "video/HEVC"; -/// MIME_TYPE_OPUS Opus MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_OPUS: &str = "audio/opus"; -/// MIME_TYPE_VP8 VP8 MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_VP8: &str = "video/VP8"; -/// MIME_TYPE_VP9 VP9 MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_VP9: &str = "video/VP9"; -/// MIME_TYPE_AV1 AV1 MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_AV1: &str = "video/AV1"; -/// MIME_TYPE_G722 G722 MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_G722: &str = "audio/G722"; -/// MIME_TYPE_PCMU PCMU MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_PCMU: &str = "audio/PCMU"; -/// MIME_TYPE_PCMA PCMA MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_PCMA: &str = "audio/PCMA"; -/// MIME_TYPE_TELEPHONE_EVENT telephone-event MIME type -/// Note: Matching should be case insensitive. -pub const MIME_TYPE_TELEPHONE_EVENT: &str = "audio/telephone-event"; - -const VALID_EXT_IDS: Range = 1..15; - -#[derive(Default, Clone)] -pub(crate) struct MediaEngineHeaderExtension { - pub(crate) uri: String, - pub(crate) is_audio: bool, - pub(crate) is_video: bool, - pub(crate) allowed_direction: Option, -} - -impl MediaEngineHeaderExtension { - pub fn is_matching_direction(&self, dir: RTCRtpTransceiverDirection) -> bool { - if let Some(allowed_direction) = self.allowed_direction { - use RTCRtpTransceiverDirection::*; - allowed_direction == Inactive && dir == Inactive - || allowed_direction.has_send() && dir.has_send() - || allowed_direction.has_recv() && dir.has_recv() - } else { - // None means all directions matches. - true - } - } -} - -/// A MediaEngine defines the codecs supported by a PeerConnection, and the -/// configuration of those codecs. A MediaEngine must not be shared between -/// PeerConnections. -#[derive(Default)] -pub struct MediaEngine { - // If we have attempted to negotiate a codec type yet. - pub(crate) negotiated_video: AtomicBool, - pub(crate) negotiated_audio: AtomicBool, - - pub(crate) video_codecs: Vec, - pub(crate) audio_codecs: Vec, - pub(crate) negotiated_video_codecs: SyncMutex>, - pub(crate) negotiated_audio_codecs: SyncMutex>, - - header_extensions: Vec, - proposed_header_extensions: SyncMutex>, - pub(crate) negotiated_header_extensions: SyncMutex>, -} - -impl MediaEngine { - /// register_default_codecs registers the default codecs supported by Pion WebRTC. - /// register_default_codecs is not safe for concurrent use. - pub fn register_default_codecs(&mut self) -> Result<()> { - // Default Audio Codecs - for codec in vec![ - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "minptime=10;useinbandfec=1".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_G722.to_owned(), - clock_rate: 8000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 9, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_PCMU.to_owned(), - clock_rate: 8000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 0, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_PCMA.to_owned(), - clock_rate: 8000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 8, - ..Default::default() - }, - ] { - self.register_codec(codec, RTPCodecType::Audio)?; - } - - let video_rtcp_feedback = vec![ - RTCPFeedback { - typ: "goog-remb".to_owned(), - parameter: "".to_owned(), - }, - RTCPFeedback { - typ: "ccm".to_owned(), - parameter: "fir".to_owned(), - }, - RTCPFeedback { - typ: "nack".to_owned(), - parameter: "".to_owned(), - }, - RTCPFeedback { - typ: "nack".to_owned(), - parameter: "pli".to_owned(), - }, - ]; - for codec in vec![ - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 96, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=0".to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 98, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=1".to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 100, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 102, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 127, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 125, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 108, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 127, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032" - .to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 123, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_AV1.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=0".to_owned(), - rtcp_feedback: video_rtcp_feedback.clone(), - }, - payload_type: 41, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_HEVC.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: video_rtcp_feedback, - }, - payload_type: 126, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: "video/ulpfec".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 116, - ..Default::default() - }, - ] { - self.register_codec(codec, RTPCodecType::Video)?; - } - - Ok(()) - } - - /// add_codec will append codec if it not exists - fn add_codec(codecs: &mut Vec, codec: RTCRtpCodecParameters) { - for c in codecs.iter() { - if c.capability.mime_type == codec.capability.mime_type - && c.payload_type == codec.payload_type - { - return; - } - } - codecs.push(codec); - } - - /// register_codec adds codec to the MediaEngine - /// These are the list of codecs supported by this PeerConnection. - /// register_codec is not safe for concurrent use. - pub fn register_codec( - &mut self, - mut codec: RTCRtpCodecParameters, - typ: RTPCodecType, - ) -> Result<()> { - codec.stats_id = format!( - "RTPCodec-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - match typ { - RTPCodecType::Audio => { - MediaEngine::add_codec(&mut self.audio_codecs, codec); - Ok(()) - } - RTPCodecType::Video => { - MediaEngine::add_codec(&mut self.video_codecs, codec); - Ok(()) - } - _ => Err(Error::ErrUnknownType), - } - } - - /// Adds a header extension to the MediaEngine - /// To determine the negotiated value use [`MediaEngine::get_header_extension_id`] after signaling is complete. - /// - /// The `allowed_direction` controls for which transceiver directions the extension matches. If - /// set to `None` it matches all directions. The `SendRecv` direction would match all transceiver - /// directions apart from `Inactive`. Inactive only matches inactive. - pub fn register_header_extension( - &mut self, - extension: RTCRtpHeaderExtensionCapability, - typ: RTPCodecType, - allowed_direction: Option, - ) -> Result<()> { - let ext = { - match self - .header_extensions - .iter_mut() - .find(|ext| ext.uri == extension.uri) - { - Some(ext) => ext, - None => { - // We have registered too many extensions - if self.header_extensions.len() > VALID_EXT_IDS.end as usize { - return Err(Error::ErrRegisterHeaderExtensionNoFreeID); - } - self.header_extensions.push(MediaEngineHeaderExtension { - allowed_direction, - ..Default::default() - }); - - // Unwrap is fine because we just pushed - self.header_extensions.last_mut().unwrap() - } - } - }; - - if typ == RTPCodecType::Audio { - ext.is_audio = true; - } else if typ == RTPCodecType::Video { - ext.is_video = true; - } - - ext.uri = extension.uri; - - if ext.allowed_direction != allowed_direction { - return Err(Error::ErrRegisterHeaderExtensionInvalidDirection); - } - - Ok(()) - } - - /// register_feedback adds feedback mechanism to already registered codecs. - pub fn register_feedback(&mut self, feedback: RTCPFeedback, typ: RTPCodecType) { - match typ { - RTPCodecType::Video => { - for v in &mut self.video_codecs { - v.capability.rtcp_feedback.push(feedback.clone()); - } - } - RTPCodecType::Audio => { - for a in &mut self.audio_codecs { - a.capability.rtcp_feedback.push(feedback.clone()); - } - } - _ => {} - } - } - - /// get_header_extension_id returns the negotiated ID for a header extension. - /// If the Header Extension isn't enabled ok will be false - pub async fn get_header_extension_id( - &self, - extension: RTCRtpHeaderExtensionCapability, - ) -> (isize, bool, bool) { - let negotiated_header_extensions = self.negotiated_header_extensions.lock(); - if negotiated_header_extensions.is_empty() { - return (0, false, false); - } - - for (id, h) in &*negotiated_header_extensions { - if extension.uri == h.uri { - return (*id, h.is_audio, h.is_video); - } - } - - (0, false, false) - } - - /// clone_to copies any user modifiable state of the MediaEngine - /// all internal state is reset - pub(crate) fn clone_to(&self) -> Self { - MediaEngine { - video_codecs: self.video_codecs.clone(), - audio_codecs: self.audio_codecs.clone(), - header_extensions: self.header_extensions.clone(), - ..Default::default() - } - } - - pub(crate) async fn get_codec_by_payload( - &self, - payload_type: PayloadType, - ) -> Result<(RTCRtpCodecParameters, RTPCodecType)> { - { - let negotiated_video_codecs = self.negotiated_video_codecs.lock(); - for codec in &*negotiated_video_codecs { - if codec.payload_type == payload_type { - return Ok((codec.clone(), RTPCodecType::Video)); - } - } - } - { - let negotiated_audio_codecs = self.negotiated_audio_codecs.lock(); - for codec in &*negotiated_audio_codecs { - if codec.payload_type == payload_type { - return Ok((codec.clone(), RTPCodecType::Audio)); - } - } - } - - Err(Error::ErrCodecNotFound) - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - let mut reports = HashMap::new(); - - for codec in &self.video_codecs { - reports.insert(codec.stats_id.clone(), Codec(CodecStats::from(codec))); - } - - for codec in &self.audio_codecs { - reports.insert(codec.stats_id.clone(), Codec(CodecStats::from(codec))); - } - - collector.merge(reports); - } - - /// Look up a codec and enable if it exists - pub(crate) fn match_remote_codec( - &self, - remote_codec: &RTCRtpCodecParameters, - typ: RTPCodecType, - exact_matches: &[RTCRtpCodecParameters], - partial_matches: &[RTCRtpCodecParameters], - ) -> Result { - let codecs = if typ == RTPCodecType::Audio { - &self.audio_codecs - } else { - &self.video_codecs - }; - - let remote_fmtp = fmtp::parse( - &remote_codec.capability.mime_type, - remote_codec.capability.sdp_fmtp_line.as_str(), - ); - if let Some(apt) = remote_fmtp.parameter("apt") { - let payload_type = apt.parse::()?; - - let mut apt_match = CodecMatch::None; - for codec in exact_matches { - if codec.payload_type == payload_type { - apt_match = CodecMatch::Exact; - break; - } - } - - if apt_match == CodecMatch::None { - for codec in partial_matches { - if codec.payload_type == payload_type { - apt_match = CodecMatch::Partial; - break; - } - } - } - - if apt_match == CodecMatch::None { - return Ok(CodecMatch::None); // not an error, we just ignore this codec we don't support - } - - // if apt's media codec is partial match, then apt codec must be partial match too - let (_, mut match_type) = codec_parameters_fuzzy_search(remote_codec, codecs); - if match_type == CodecMatch::Exact && apt_match == CodecMatch::Partial { - match_type = CodecMatch::Partial; - } - return Ok(match_type); - } - - let (_, match_type) = codec_parameters_fuzzy_search(remote_codec, codecs); - Ok(match_type) - } - - /// Look up a header extension and enable if it exists - pub(crate) async fn update_header_extension( - &self, - id: isize, - extension: &str, - typ: RTPCodecType, - ) -> Result<()> { - let mut negotiated_header_extensions = self.negotiated_header_extensions.lock(); - let mut proposed_header_extensions = self.proposed_header_extensions.lock(); - - for local_extension in &self.header_extensions { - if local_extension.uri != extension { - continue; - } - - let negotiated_ext = negotiated_header_extensions - .iter_mut() - .find(|(_, ext)| ext.uri == extension); - - if let Some(n_ext) = negotiated_ext { - if *n_ext.0 == id { - n_ext.1.is_video |= typ == RTPCodecType::Video; - n_ext.1.is_audio |= typ == RTPCodecType::Audio; - } else { - let nid = n_ext.0; - log::warn!("Invalid ext id mapping in update_header_extension. {} was negotiated as {}, but was {} in call", extension, nid, id); - } - } else { - // We either only have a proposal or we have neither proposal nor a negotiated id - // Accept whatevers the peer suggests - - if let Some(prev_ext) = negotiated_header_extensions.get(&id) { - let prev_uri = &prev_ext.uri; - log::warn!("Assigning {} to {} would override previous assignment to {}, no action taken", id, extension, prev_uri); - } else { - let h = MediaEngineHeaderExtension { - uri: extension.to_owned(), - is_audio: local_extension.is_audio && typ == RTPCodecType::Audio, - is_video: local_extension.is_video && typ == RTPCodecType::Video, - allowed_direction: local_extension.allowed_direction, - }; - negotiated_header_extensions.insert(id, h); - } - } - - // Clear any proposals we had for this id - proposed_header_extensions.remove(&id); - } - Ok(()) - } - - pub(crate) async fn push_codecs(&self, codecs: Vec, typ: RTPCodecType) { - for codec in codecs { - if typ == RTPCodecType::Audio { - let mut negotiated_audio_codecs = self.negotiated_audio_codecs.lock(); - MediaEngine::add_codec(&mut negotiated_audio_codecs, codec); - } else if typ == RTPCodecType::Video { - let mut negotiated_video_codecs = self.negotiated_video_codecs.lock(); - MediaEngine::add_codec(&mut negotiated_video_codecs, codec); - } - } - } - - /// Update the MediaEngine from a remote description - pub(crate) async fn update_from_remote_description( - &self, - desc: &SessionDescription, - ) -> Result<()> { - for media in &desc.media_descriptions { - let typ = if !self.negotiated_audio.load(Ordering::SeqCst) - && media.media_name.media.to_lowercase() == "audio" - { - self.negotiated_audio.store(true, Ordering::SeqCst); - RTPCodecType::Audio - } else if !self.negotiated_video.load(Ordering::SeqCst) - && media.media_name.media.to_lowercase() == "video" - { - self.negotiated_video.store(true, Ordering::SeqCst); - RTPCodecType::Video - } else { - continue; - }; - - let codecs = codecs_from_media_description(media)?; - - let mut exact_matches = vec![]; //make([]RTPCodecParameters, 0, len(codecs)) - let mut partial_matches = vec![]; //make([]RTPCodecParameters, 0, len(codecs)) - - for codec in codecs { - let match_type = - self.match_remote_codec(&codec, typ, &exact_matches, &partial_matches)?; - - if match_type == CodecMatch::Exact { - exact_matches.push(codec); - } else if match_type == CodecMatch::Partial { - partial_matches.push(codec); - } - } - - // use exact matches when they exist, otherwise fall back to partial - if !exact_matches.is_empty() { - self.push_codecs(exact_matches, typ).await; - } else if !partial_matches.is_empty() { - self.push_codecs(partial_matches, typ).await; - } else { - // no match, not negotiated - continue; - } - - let extensions = rtp_extensions_from_media_description(media)?; - - for (extension, id) in extensions { - self.update_header_extension(id, &extension, typ).await?; - } - } - - Ok(()) - } - - pub(crate) fn get_codecs_by_kind(&self, typ: RTPCodecType) -> Vec { - if typ == RTPCodecType::Video { - if self.negotiated_video.load(Ordering::SeqCst) { - let negotiated_video_codecs = self.negotiated_video_codecs.lock(); - negotiated_video_codecs.clone() - } else { - self.video_codecs.clone() - } - } else if typ == RTPCodecType::Audio { - if self.negotiated_audio.load(Ordering::SeqCst) { - let negotiated_audio_codecs = self.negotiated_audio_codecs.lock(); - negotiated_audio_codecs.clone() - } else { - self.audio_codecs.clone() - } - } else { - vec![] - } - } - - pub(crate) fn get_rtp_parameters_by_kind( - &self, - typ: RTPCodecType, - direction: RTCRtpTransceiverDirection, - ) -> RTCRtpParameters { - let mut header_extensions = vec![]; - - if self.negotiated_video.load(Ordering::SeqCst) && typ == RTPCodecType::Video - || self.negotiated_audio.load(Ordering::SeqCst) && typ == RTPCodecType::Audio - { - let negotiated_header_extensions = self.negotiated_header_extensions.lock(); - for (id, e) in &*negotiated_header_extensions { - if e.is_matching_direction(direction) - && (e.is_audio && typ == RTPCodecType::Audio - || e.is_video && typ == RTPCodecType::Video) - { - header_extensions.push(RTCRtpHeaderExtensionParameters { - id: *id, - uri: e.uri.clone(), - }); - } - } - } else { - let mut proposed_header_extensions = self.proposed_header_extensions.lock(); - let mut negotiated_header_extensions = self.negotiated_header_extensions.lock(); - - for local_extension in &self.header_extensions { - let relevant = local_extension.is_matching_direction(direction) - && (local_extension.is_audio && typ == RTPCodecType::Audio - || local_extension.is_video && typ == RTPCodecType::Video); - - if !relevant { - continue; - } - - if let Some((id, negotiated_extension)) = negotiated_header_extensions - .iter_mut() - .find(|(_, e)| e.uri == local_extension.uri) - { - // We have previously negotiated this extension, make sure to record it as - // active for the current type - negotiated_extension.is_audio |= typ == RTPCodecType::Audio; - negotiated_extension.is_video |= typ == RTPCodecType::Video; - - header_extensions.push(RTCRtpHeaderExtensionParameters { - id: *id, - uri: negotiated_extension.uri.clone(), - }); - - continue; - } - - if let Some((id, negotiated_extension)) = proposed_header_extensions - .iter_mut() - .find(|(_, e)| e.uri == local_extension.uri) - { - // We have previously proposed this extension, re-use it - header_extensions.push(RTCRtpHeaderExtensionParameters { - id: *id, - uri: negotiated_extension.uri.clone(), - }); - - continue; - } - - // Figure out which (unused id) to propose. - let id = VALID_EXT_IDS.clone().find(|id| { - !negotiated_header_extensions.keys().any(|nid| nid == id) - && !proposed_header_extensions.keys().any(|pid| pid == id) - }); - - if let Some(id) = id { - proposed_header_extensions.insert( - id, - MediaEngineHeaderExtension { - uri: local_extension.uri.clone(), - is_audio: local_extension.is_audio, - is_video: local_extension.is_video, - allowed_direction: local_extension.allowed_direction, - }, - ); - - header_extensions.push(RTCRtpHeaderExtensionParameters { - id, - uri: local_extension.uri.clone(), - }); - } else { - log::warn!("No available RTP extension ID for {}", local_extension.uri); - } - } - } - - RTCRtpParameters { - header_extensions, - codecs: self.get_codecs_by_kind(typ), - } - } - - pub(crate) async fn get_rtp_parameters_by_payload_type( - &self, - payload_type: PayloadType, - ) -> Result { - let (codec, typ) = self.get_codec_by_payload(payload_type).await?; - - let mut header_extensions = vec![]; - { - let negotiated_header_extensions = self.negotiated_header_extensions.lock(); - for (id, e) in &*negotiated_header_extensions { - if e.is_audio && typ == RTPCodecType::Audio - || e.is_video && typ == RTPCodecType::Video - { - header_extensions.push(RTCRtpHeaderExtensionParameters { - uri: e.uri.clone(), - id: *id, - }); - } - } - } - - Ok(RTCRtpParameters { - header_extensions, - codecs: vec![codec], - }) - } -} diff --git a/webrtc/src/api/mod.rs b/webrtc/src/api/mod.rs deleted file mode 100644 index 2252dfbb2..000000000 --- a/webrtc/src/api/mod.rs +++ /dev/null @@ -1,238 +0,0 @@ -#[cfg(test)] -mod api_test; - -pub mod interceptor_registry; -pub mod media_engine; -pub mod setting_engine; - -use std::sync::Arc; -use std::time::SystemTime; - -use interceptor::registry::Registry; -use interceptor::Interceptor; -use media_engine::*; -use rcgen::KeyPair; -use setting_engine::*; - -use crate::data_channel::data_channel_parameters::DataChannelParameters; -use crate::data_channel::RTCDataChannel; -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::{Error, Result}; -use crate::ice_transport::ice_gatherer::{RTCIceGatherOptions, RTCIceGatherer}; -use crate::ice_transport::RTCIceTransport; -use crate::peer_connection::certificate::RTCCertificate; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::RTCPeerConnection; -use crate::rtp_transceiver::rtp_codec::RTPCodecType; -use crate::rtp_transceiver::rtp_receiver::RTCRtpReceiver; -use crate::rtp_transceiver::rtp_sender::RTCRtpSender; -use crate::sctp_transport::RTCSctpTransport; -use crate::track::track_local::TrackLocal; - -/// API bundles the global functions of the WebRTC and ORTC API. -/// Some of these functions are also exported globally using the -/// defaultAPI object. Note that the global version of the API -/// may be phased out in the future. -pub struct API { - pub(crate) setting_engine: Arc, - pub(crate) media_engine: Arc, - pub(crate) interceptor_registry: Registry, -} - -impl API { - /// new_peer_connection creates a new PeerConnection with the provided configuration against the received API object - pub async fn new_peer_connection( - &self, - configuration: RTCConfiguration, - ) -> Result { - RTCPeerConnection::new(self, configuration).await - } - - /// new_ice_gatherer creates a new ice gatherer. - /// This constructor is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - pub fn new_ice_gatherer(&self, opts: RTCIceGatherOptions) -> Result { - let mut validated_servers = vec![]; - if !opts.ice_servers.is_empty() { - for server in &opts.ice_servers { - let url = server.urls()?; - validated_servers.extend(url); - } - } - - Ok(RTCIceGatherer::new( - validated_servers, - opts.ice_gather_policy, - Arc::clone(&self.setting_engine), - )) - } - - /// new_ice_transport creates a new ice transport. - /// This constructor is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - pub fn new_ice_transport(&self, gatherer: Arc) -> RTCIceTransport { - RTCIceTransport::new(gatherer) - } - - /// new_dtls_transport creates a new dtls_transport transport. - /// This constructor is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - pub fn new_dtls_transport( - &self, - ice_transport: Arc, - mut certificates: Vec, - ) -> Result { - if !certificates.is_empty() { - let now = SystemTime::now(); - for cert in &certificates { - cert.expires - .duration_since(now) - .map_err(|_| Error::ErrCertificateExpired)?; - } - } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - certificates = vec![cert]; - }; - - Ok(RTCDtlsTransport::new( - ice_transport, - certificates, - Arc::clone(&self.setting_engine), - )) - } - - /// new_sctp_transport creates a new SCTPTransport. - /// This constructor is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - pub fn new_sctp_transport( - &self, - dtls_transport: Arc, - ) -> Result { - Ok(RTCSctpTransport::new( - dtls_transport, - Arc::clone(&self.setting_engine), - )) - } - - /// new_data_channel creates a new DataChannel. - /// This constructor is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - pub async fn new_data_channel( - &self, - sctp_transport: Arc, - params: DataChannelParameters, - ) -> Result { - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #5) - if params.label.len() > 65535 { - return Err(Error::ErrStringSizeLimit); - } - - let d = RTCDataChannel::new(params, Arc::clone(&self.setting_engine)); - d.open(sctp_transport).await?; - - Ok(d) - } - - /// new_rtp_receiver constructs a new RTPReceiver - pub fn new_rtp_receiver( - &self, - kind: RTPCodecType, - transport: Arc, - interceptor: Arc, - ) -> RTCRtpReceiver { - RTCRtpReceiver::new( - self.setting_engine.get_receive_mtu(), - kind, - transport, - Arc::clone(&self.media_engine), - interceptor, - ) - } - - /// new_rtp_sender constructs a new RTPSender - pub async fn new_rtp_sender( - &self, - track: Option>, - transport: Arc, - interceptor: Arc, - ) -> RTCRtpSender { - let kind = track.as_ref().map(|t| t.kind()).unwrap_or_default(); - RTCRtpSender::new( - self.setting_engine.get_receive_mtu(), - track, - kind, - transport, - Arc::clone(&self.media_engine), - interceptor, - false, - ) - .await - } - - /// Returns the internal [`SettingEngine`]. - pub fn setting_engine(&self) -> Arc { - Arc::clone(&self.setting_engine) - } - - /// Returns the internal [`MediaEngine`]. - pub fn media_engine(&self) -> Arc { - Arc::clone(&self.media_engine) - } -} - -#[derive(Default)] -pub struct APIBuilder { - setting_engine: Option>, - media_engine: Option>, - interceptor_registry: Option, -} - -impl APIBuilder { - pub fn new() -> Self { - APIBuilder::default() - } - - pub fn build(mut self) -> API { - API { - setting_engine: if let Some(setting_engine) = self.setting_engine.take() { - setting_engine - } else { - Arc::new(SettingEngine::default()) - }, - media_engine: if let Some(media_engine) = self.media_engine.take() { - media_engine - } else { - Arc::new(MediaEngine::default()) - }, - interceptor_registry: if let Some(interceptor_registry) = - self.interceptor_registry.take() - { - interceptor_registry - } else { - Registry::new() - }, - } - } - - /// WithSettingEngine allows providing a SettingEngine to the API. - /// Settings should not be changed after passing the engine to an API. - pub fn with_setting_engine(mut self, setting_engine: SettingEngine) -> Self { - self.setting_engine = Some(Arc::new(setting_engine)); - self - } - - /// WithMediaEngine allows providing a MediaEngine to the API. - /// Settings can be changed after passing the engine to an API. - pub fn with_media_engine(mut self, media_engine: MediaEngine) -> Self { - self.media_engine = Some(Arc::new(media_engine)); - self - } - - /// with_interceptor_registry allows providing Interceptors to the API. - /// Settings should not be changed after passing the registry to an API. - pub fn with_interceptor_registry(mut self, interceptor_registry: Registry) -> Self { - self.interceptor_registry = Some(interceptor_registry); - self - } -} diff --git a/webrtc/src/api/setting_engine/mod.rs b/webrtc/src/api/setting_engine/mod.rs deleted file mode 100644 index 6d909d3ae..000000000 --- a/webrtc/src/api/setting_engine/mod.rs +++ /dev/null @@ -1,327 +0,0 @@ -#[cfg(test)] -mod setting_engine_test; - -use std::sync::Arc; - -use dtls::extension::extension_use_srtp::SrtpProtectionProfile; -use ice::agent::agent_config::{InterfaceFilterFn, IpFilterFn}; -use ice::mdns::MulticastDnsMode; -use ice::network_type::NetworkType; -use ice::udp_network::UDPNetwork; -use tokio::time::Duration; -use util::vnet::net::*; - -use crate::dtls_transport::dtls_role::DTLSRole; -use crate::error::{Error, Result}; -use crate::ice_transport::ice_candidate_type::RTCIceCandidateType; -use crate::RECEIVE_MTU; - -#[derive(Default, Clone)] -pub struct Detach { - pub data_channels: bool, -} - -#[derive(Default, Clone)] -pub struct Timeout { - pub ice_disconnected_timeout: Option, - pub ice_failed_timeout: Option, - pub ice_keepalive_interval: Option, - pub ice_host_acceptance_min_wait: Option, - pub ice_srflx_acceptance_min_wait: Option, - pub ice_prflx_acceptance_min_wait: Option, - pub ice_relay_acceptance_min_wait: Option, -} - -#[derive(Default, Clone)] -pub struct Candidates { - pub ice_lite: bool, - pub ice_network_types: Vec, - pub interface_filter: Arc>, - pub ip_filter: Arc>, - pub nat_1to1_ips: Vec, - pub nat_1to1_ip_candidate_type: RTCIceCandidateType, - pub multicast_dns_mode: MulticastDnsMode, - pub multicast_dns_host_name: String, - pub username_fragment: String, - pub password: String, -} - -#[derive(Default, Clone)] -pub struct ReplayProtection { - pub dtls: usize, - pub srtp: usize, - pub srtcp: usize, -} - -/// SettingEngine allows influencing behavior in ways that are not -/// supported by the WebRTC API. This allows us to support additional -/// use-cases without deviating from the WebRTC API elsewhere. -#[derive(Default, Clone)] -pub struct SettingEngine { - pub(crate) detach: Detach, - pub(crate) timeout: Timeout, - pub(crate) candidates: Candidates, - pub(crate) replay_protection: ReplayProtection, - pub(crate) sdp_media_level_fingerprints: bool, - pub(crate) answering_dtls_role: DTLSRole, - pub(crate) disable_certificate_fingerprint_verification: bool, - pub(crate) allow_insecure_verification_algorithm: bool, - pub(crate) disable_srtp_replay_protection: bool, - pub(crate) disable_srtcp_replay_protection: bool, - pub(crate) vnet: Option>, - //BufferFactory :func(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser, - //iceTCPMux :ice.TCPMux,? - //iceProxyDialer :proxy.Dialer,? - pub(crate) udp_network: UDPNetwork, - pub(crate) disable_media_engine_copy: bool, - pub(crate) srtp_protection_profiles: Vec, - pub(crate) receive_mtu: usize, - pub(crate) mid_generator: Option String + Send + Sync>>, -} - -impl SettingEngine { - /// get_receive_mtu returns the configured MTU. If SettingEngine's MTU is configured to 0 it returns the default - pub(crate) fn get_receive_mtu(&self) -> usize { - if self.receive_mtu != 0 { - self.receive_mtu - } else { - RECEIVE_MTU - } - } - /// detach_data_channels enables detaching data channels. When enabled - /// data channels have to be detached in the OnOpen callback using the - /// DataChannel.Detach method. - pub fn detach_data_channels(&mut self) { - self.detach.data_channels = true; - } - - /// set_srtp_protection_profiles allows the user to override the default srtp Protection Profiles - /// The default srtp protection profiles are provided by the function `defaultSrtpProtectionProfiles` - pub fn set_srtp_protection_profiles(&mut self, profiles: Vec) { - self.srtp_protection_profiles = profiles - } - - /// set_ice_timeouts sets the behavior around ICE Timeouts - /// * disconnected_timeout is the duration without network activity before a Agent is considered disconnected. Default is 5 Seconds - /// * failed_timeout is the duration without network activity before a Agent is considered failed after disconnected. Default is 25 Seconds - /// * keep_alive_interval is how often the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent. Default is 2 seconds - pub fn set_ice_timeouts( - &mut self, - disconnected_timeout: Option, - failed_timeout: Option, - keep_alive_interval: Option, - ) { - self.timeout.ice_disconnected_timeout = disconnected_timeout; - self.timeout.ice_failed_timeout = failed_timeout; - self.timeout.ice_keepalive_interval = keep_alive_interval; - } - - /// set_host_acceptance_min_wait sets the icehost_acceptance_min_wait - pub fn set_host_acceptance_min_wait(&mut self, t: Option) { - self.timeout.ice_host_acceptance_min_wait = t; - } - - /// set_srflx_acceptance_min_wait sets the icesrflx_acceptance_min_wait - pub fn set_srflx_acceptance_min_wait(&mut self, t: Option) { - self.timeout.ice_srflx_acceptance_min_wait = t; - } - - /// set_prflx_acceptance_min_wait sets the iceprflx_acceptance_min_wait - pub fn set_prflx_acceptance_min_wait(&mut self, t: Option) { - self.timeout.ice_prflx_acceptance_min_wait = t; - } - - /// set_relay_acceptance_min_wait sets the icerelay_acceptance_min_wait - pub fn set_relay_acceptance_min_wait(&mut self, t: Option) { - self.timeout.ice_relay_acceptance_min_wait = t; - } - - /// set_udp_network allows ICE traffic to come through Ephemeral or UDPMux. - /// UDPMux drastically simplifying deployments where ports will need to be opened/forwarded. - /// UDPMux should be started prior to creating PeerConnections. - pub fn set_udp_network(&mut self, udp_network: UDPNetwork) { - self.udp_network = udp_network; - } - - /// set_lite configures whether or not the ice agent should be a lite agent - pub fn set_lite(&mut self, lite: bool) { - self.candidates.ice_lite = lite; - } - - /// set_network_types configures what types of candidate networks are supported - /// during local and server reflexive gathering. - pub fn set_network_types(&mut self, candidate_types: Vec) { - self.candidates.ice_network_types = candidate_types; - } - - /// set_interface_filter sets the filtering functions when gathering ICE candidates - /// This can be used to exclude certain network interfaces from ICE. Which may be - /// useful if you know a certain interface will never succeed, or if you wish to reduce - /// the amount of information you wish to expose to the remote peer - pub fn set_interface_filter(&mut self, filter: InterfaceFilterFn) { - self.candidates.interface_filter = Arc::new(Some(filter)); - } - - /// set_ip_filter sets the filtering functions when gathering ICE candidates - /// This can be used to exclude certain ip from ICE. Which may be - /// useful if you know a certain ip will never succeed, or if you wish to reduce - /// the amount of information you wish to expose to the remote peer - pub fn set_ip_filter(&mut self, filter: IpFilterFn) { - self.candidates.ip_filter = Arc::new(Some(filter)); - } - - /// set_nat_1to1_ips sets a list of external IP addresses of 1:1 (D)NAT - /// and a candidate type for which the external IP address is used. - /// This is useful when you are host a server using Pion on an AWS EC2 instance - /// which has a private address, behind a 1:1 DNAT with a public IP (e.g. - /// Elastic IP). In this case, you can give the public IP address so that - /// Pion will use the public IP address in its candidate instead of the private - /// IP address. The second argument, candidate_type, is used to tell Pion which - /// type of candidate should use the given public IP address. - /// Two types of candidates are supported: - /// - /// ICECandidateTypeHost: - /// The public IP address will be used for the host candidate in the SDP. - /// ICECandidateTypeSrflx: - /// A server reflexive candidate with the given public IP address will be added - /// to the SDP. - /// - /// Please note that if you choose ICECandidateTypeHost, then the private IP address - /// won't be advertised with the peer. Also, this option cannot be used along with mDNS. - /// - /// If you choose ICECandidateTypeSrflx, it simply adds a server reflexive candidate - /// with the public IP. The host candidate is still available along with mDNS - /// capabilities unaffected. Also, you cannot give STUN server URL at the same time. - /// It will result in an error otherwise. - pub fn set_nat_1to1_ips(&mut self, ips: Vec, candidate_type: RTCIceCandidateType) { - self.candidates.nat_1to1_ips = ips; - self.candidates.nat_1to1_ip_candidate_type = candidate_type; - } - - /// set_answering_dtls_role sets the dtls_transport role that is selected when offering - /// The dtls_transport role controls if the WebRTC Client as a client or server. This - /// may be useful when interacting with non-compliant clients or debugging issues. - /// - /// DTLSRoleActive: - /// Act as dtls_transport Client, send the ClientHello and starts the handshake - /// DTLSRolePassive: - /// Act as dtls_transport Server, wait for ClientHello - pub fn set_answering_dtls_role(&mut self, role: DTLSRole) -> Result<()> { - if role != DTLSRole::Client && role != DTLSRole::Server { - return Err(Error::ErrSettingEngineSetAnsweringDTLSRole); - } - - self.answering_dtls_role = role; - Ok(()) - } - - /// set_vnet sets the VNet instance that is passed to ice - /// VNet is a virtual network layer, allowing users to simulate - /// different topologies, latency, loss and jitter. This can be useful for - /// learning WebRTC concepts or testing your application in a lab environment - pub fn set_vnet(&mut self, vnet: Option>) { - self.vnet = vnet; - } - - /// set_ice_multicast_dns_mode controls if ice queries and generates mDNS ICE Candidates - pub fn set_ice_multicast_dns_mode(&mut self, multicast_dns_mode: ice::mdns::MulticastDnsMode) { - self.candidates.multicast_dns_mode = multicast_dns_mode - } - - /// set_multicast_dns_host_name sets a static HostName to be used by ice instead of generating one on startup - /// This should only be used for a single PeerConnection. Having multiple PeerConnections with the same HostName will cause - /// undefined behavior - pub fn set_multicast_dns_host_name(&mut self, host_name: String) { - self.candidates.multicast_dns_host_name = host_name; - } - - /// set_ice_credentials sets a staic uFrag/uPwd to be used by ice - /// This is useful if you want to do signalless WebRTC session, or having a reproducible environment with static credentials - pub fn set_ice_credentials(&mut self, username_fragment: String, password: String) { - self.candidates.username_fragment = username_fragment; - self.candidates.password = password; - } - - /// disable_certificate_fingerprint_verification disables fingerprint verification after dtls_transport Handshake has finished - pub fn disable_certificate_fingerprint_verification(&mut self, is_disabled: bool) { - self.disable_certificate_fingerprint_verification = is_disabled; - } - - /// allow_insecure_verification_algorithm allows the usage of certain signature verification - /// algorithm that are known to be vulnerable or deprecated. - pub fn allow_insecure_verification_algorithm(&mut self, is_allowed: bool) { - self.allow_insecure_verification_algorithm = is_allowed; - } - /// set_dtls_replay_protection_window sets a replay attack protection window size of dtls_transport connection. - pub fn set_dtls_replay_protection_window(&mut self, n: usize) { - self.replay_protection.dtls = n; - } - - /// set_srtp_replay_protection_window sets a replay attack protection window size of srtp session. - pub fn set_srtp_replay_protection_window(&mut self, n: usize) { - self.disable_srtp_replay_protection = false; - self.replay_protection.srtp = n; - } - - /// set_srtcp_replay_protection_window sets a replay attack protection window size of srtcp session. - pub fn set_srtcp_replay_protection_window(&mut self, n: usize) { - self.disable_srtcp_replay_protection = false; - self.replay_protection.srtcp = n; - } - - /// disable_srtp_replay_protection disables srtp replay protection. - pub fn disable_srtp_replay_protection(&mut self, is_disabled: bool) { - self.disable_srtp_replay_protection = is_disabled; - } - - /// disable_srtcp_replay_protection disables srtcp replay protection. - pub fn disable_srtcp_replay_protection(&mut self, is_disabled: bool) { - self.disable_srtcp_replay_protection = is_disabled; - } - - /// set_sdp_media_level_fingerprints configures the logic for dtls_transport Fingerprint insertion - /// If true, fingerprints will be inserted in the sdp at the fingerprint - /// level, instead of the session level. This helps with compatibility with - /// some webrtc implementations. - pub fn set_sdp_media_level_fingerprints(&mut self, sdp_media_level_fingerprints: bool) { - self.sdp_media_level_fingerprints = sdp_media_level_fingerprints; - } - - // SetICETCPMux enables ICE-TCP when set to a non-nil value. Make sure that - // NetworkTypeTCP4 or NetworkTypeTCP6 is enabled as well. - //pub fn SetICETCPMux(&mut self, tcpMux ice.TCPMux) { - // self.iceTCPMux = tcpMux - //} - - // SetICEProxyDialer sets the proxy dialer interface based on golang.org/x/net/proxy. - //pub fn SetICEProxyDialer(&mut self, d proxy.Dialer) { - // self.iceProxyDialer = d - //} - - /// disable_media_engine_copy stops the MediaEngine from being copied. This allows a user to modify - /// the MediaEngine after the PeerConnection has been constructed. This is useful if you wish to - /// modify codecs after signaling. Make sure not to share MediaEngines between PeerConnections. - pub fn disable_media_engine_copy(&mut self, is_disabled: bool) { - self.disable_media_engine_copy = is_disabled; - } - - /// set_receive_mtu sets the size of read buffer that copies incoming packets. This is optional. - /// Leave this 0 for the default receive_mtu - pub fn set_receive_mtu(&mut self, receive_mtu: usize) { - self.receive_mtu = receive_mtu; - } - - /// Sets a callback used to generate mid for transceivers created by this side of the RTCPeerconnection. - /// By having separate "naming schemes" for mids generated by either side of a connection, it's - /// possible to reduce complexity when handling SDP offers/answers clashing. - /// - /// The `isize` argument is currently greatest seen _numeric_ mid. Since mids don't need to be numeric - /// this doesn't necessarily indicating anything. - /// - /// Note that the spec says: All MID values MUST be generated in a fashion that does not leak user - /// information, e.g., randomly or using a per-PeerConnection counter, and SHOULD be 3 bytes or less, - /// to allow them to efficiently fit into the RTP header extension - pub fn set_mid_generator(&mut self, f: impl Fn(isize) -> String + Send + Sync + 'static) { - self.mid_generator = Some(Arc::new(f)); - } -} diff --git a/webrtc/src/api/setting_engine/setting_engine_test.rs b/webrtc/src/api/setting_engine/setting_engine_test.rs deleted file mode 100644 index cb5433f58..000000000 --- a/webrtc/src/api/setting_engine/setting_engine_test.rs +++ /dev/null @@ -1,271 +0,0 @@ -use std::sync::atomic::Ordering; - -use super::*; -use crate::api::media_engine::MediaEngine; -use crate::api::APIBuilder; -use crate::peer_connection::peer_connection_test::*; -use crate::rtp_transceiver::rtp_codec::RTPCodecType; - -#[test] -fn test_set_connection_timeout() -> Result<()> { - let mut s = SettingEngine::default(); - - assert_eq!(s.timeout.ice_disconnected_timeout, None); - assert_eq!(s.timeout.ice_failed_timeout, None); - assert_eq!(s.timeout.ice_keepalive_interval, None); - - s.set_ice_timeouts( - Some(Duration::from_secs(1)), - Some(Duration::from_secs(2)), - Some(Duration::from_secs(3)), - ); - assert_eq!( - s.timeout.ice_disconnected_timeout, - Some(Duration::from_secs(1)) - ); - assert_eq!(s.timeout.ice_failed_timeout, Some(Duration::from_secs(2))); - assert_eq!( - s.timeout.ice_keepalive_interval, - Some(Duration::from_secs(3)) - ); - - Ok(()) -} - -#[test] -fn test_detach_data_channels() -> Result<()> { - let mut s = SettingEngine::default(); - - assert!( - !s.detach.data_channels, - "SettingEngine defaults aren't as expected." - ); - - s.detach_data_channels(); - - assert!( - s.detach.data_channels, - "Failed to enable detached data channels." - ); - - Ok(()) -} - -#[test] -fn test_set_nat_1to1_ips() -> Result<()> { - let mut s = SettingEngine::default(); - - assert!( - s.candidates.nat_1to1_ips.is_empty(), - "Invalid default value" - ); - assert!( - s.candidates.nat_1to1_ip_candidate_type == RTCIceCandidateType::Unspecified, - "Invalid default value" - ); - - let ips = vec!["1.2.3.4".to_owned()]; - let typ = RTCIceCandidateType::Host; - s.set_nat_1to1_ips(ips, typ); - assert!( - !(s.candidates.nat_1to1_ips.len() != 1 || s.candidates.nat_1to1_ips[0] != "1.2.3.4"), - "Failed to set NAT1To1IPs" - ); - assert!( - s.candidates.nat_1to1_ip_candidate_type == typ, - "Failed to set NAT1To1IPCandidateType" - ); - - Ok(()) -} - -#[test] -fn test_set_answering_dtls_role() -> Result<()> { - let mut s = SettingEngine::default(); - assert!( - s.set_answering_dtls_role(DTLSRole::Auto).is_err(), - "SetAnsweringDTLSRole can only be called with DTLSRoleClient or DTLSRoleServer" - ); - assert!( - s.set_answering_dtls_role(DTLSRole::Unspecified).is_err(), - "SetAnsweringDTLSRole can only be called with DTLSRoleClient or DTLSRoleServer" - ); - - Ok(()) -} - -#[test] -fn test_set_replay_protection() -> Result<()> { - let mut s = SettingEngine::default(); - - assert!( - !(s.replay_protection.dtls != 0 - || s.replay_protection.srtp != 0 - || s.replay_protection.srtcp != 0), - "SettingEngine defaults aren't as expected." - ); - - s.set_dtls_replay_protection_window(128); - s.set_srtp_replay_protection_window(64); - s.set_srtcp_replay_protection_window(32); - - assert!( - !(s.replay_protection.dtls == 0 || s.replay_protection.dtls != 128), - "Failed to set DTLS replay protection window" - ); - assert!( - !(s.replay_protection.srtp == 0 || s.replay_protection.srtp != 64), - "Failed to set SRTP replay protection window" - ); - assert!( - !(s.replay_protection.srtcp == 0 || s.replay_protection.srtcp != 32), - "Failed to set SRTCP replay protection window" - ); - - Ok(()) -} - -/*TODO:#[test] fn test_setting_engine_set_ice_tcp_mux() ->Result<()> { - - listener, err := net.ListenTCP("tcp", &net.TCPAddr{}) - if err != nil { - panic(err) - } - - defer func() { - _ = listener.Close() - }() - - tcpMux := NewICETCPMux(nil, listener, 8) - - defer func() { - _ = tcpMux.Close() - }() - - let mut s = SettingEngine::default(); - settingEngine.SetICETCPMux(tcpMux) - - assert.Equal(t, tcpMux, settingEngine.iceTCPMux) - - Ok(()) -} -*/ - -#[tokio::test] -async fn test_setting_engine_set_disable_media_engine_copy() -> Result<()> { - //"Copy" - { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offerer, mut answerer) = new_pair(&api).await?; - - offerer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - signal_pair(&mut offerer, &mut answerer).await?; - - // Assert that the MediaEngine the user created isn't modified - assert!(!api.media_engine.negotiated_video.load(Ordering::SeqCst)); - { - let negotiated_video_codecs = api.media_engine.negotiated_video_codecs.lock(); - assert!(negotiated_video_codecs.is_empty()); - } - - // Assert that the internal MediaEngine is modified - assert!(offerer - .internal - .media_engine - .negotiated_video - .load(Ordering::SeqCst)); - { - let negotiated_video_codecs = - offerer.internal.media_engine.negotiated_video_codecs.lock(); - assert!(!negotiated_video_codecs.is_empty()); - } - - close_pair_now(&offerer, &answerer).await; - - let (new_offerer, new_answerer) = new_pair(&api).await?; - - // Assert that the first internal MediaEngine hasn't been cleared - assert!(offerer - .internal - .media_engine - .negotiated_video - .load(Ordering::SeqCst)); - { - let negotiated_video_codecs = - offerer.internal.media_engine.negotiated_video_codecs.lock(); - assert!(!negotiated_video_codecs.is_empty()); - } - - // Assert that the new internal MediaEngine isn't modified - assert!(!new_offerer - .internal - .media_engine - .negotiated_video - .load(Ordering::SeqCst)); - { - let negotiated_video_codecs = new_offerer - .internal - .media_engine - .negotiated_video_codecs - .lock(); - assert!(negotiated_video_codecs.is_empty()); - } - - close_pair_now(&new_offerer, &new_answerer).await; - } - - //"No Copy" - { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let mut s = SettingEngine::default(); - s.disable_media_engine_copy(true); - - let api = APIBuilder::new() - .with_media_engine(m) - .with_setting_engine(s) - .build(); - - let (mut offerer, mut answerer) = new_pair(&api).await?; - - offerer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - signal_pair(&mut offerer, &mut answerer).await?; - - // Assert that the user MediaEngine was modified, so no copy happened - assert!(api.media_engine.negotiated_video.load(Ordering::SeqCst)); - { - let negotiated_video_codecs = api.media_engine.negotiated_video_codecs.lock(); - assert!(!negotiated_video_codecs.is_empty()); - } - - close_pair_now(&offerer, &answerer).await; - - let (offerer, answerer) = new_pair(&api).await?; - - // Assert that the new internal MediaEngine was modified, so no copy happened - assert!(offerer - .internal - .media_engine - .negotiated_video - .load(Ordering::SeqCst)); - { - let negotiated_video_codecs = - offerer.internal.media_engine.negotiated_video_codecs.lock(); - assert!(!negotiated_video_codecs.is_empty()); - } - - close_pair_now(&offerer, &answerer).await; - } - - Ok(()) -} diff --git a/webrtc/src/data_channel/data_channel_init.rs b/webrtc/src/data_channel/data_channel_init.rs deleted file mode 100644 index 5adbdb721..000000000 --- a/webrtc/src/data_channel/data_channel_init.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// DataChannelConfig can be used to configure properties of the underlying -/// channel such as data reliability. -#[derive(Default, Debug, Clone)] -pub struct RTCDataChannelInit { - /// ordered indicates if data is allowed to be delivered out of order. The - /// default value of true, guarantees that data will be delivered in order. - pub ordered: Option, - - /// max_packet_life_time limits the time (in milliseconds) during which the - /// channel will transmit or retransmit data if not acknowledged. This value - /// may be clamped if it exceeds the maximum value supported. - pub max_packet_life_time: Option, - - /// max_retransmits limits the number of times a channel will retransmit data - /// if not successfully delivered. This value may be clamped if it exceeds - /// the maximum value supported. - pub max_retransmits: Option, - - /// protocol describes the subprotocol name used for this channel. - pub protocol: Option, - - /// negotiated describes if the data channel is created by the local peer or - /// the remote peer. The default value of None tells the user agent to - /// announce the channel in-band and instruct the other peer to dispatch a - /// corresponding DataChannel. If set to Some(id), it is up to the application - /// to negotiate the channel and create an DataChannel with the same id - /// at the other peer. - pub negotiated: Option, -} diff --git a/webrtc/src/data_channel/data_channel_message.rs b/webrtc/src/data_channel/data_channel_message.rs deleted file mode 100644 index a781ec431..000000000 --- a/webrtc/src/data_channel/data_channel_message.rs +++ /dev/null @@ -1,11 +0,0 @@ -use bytes::Bytes; - -/// DataChannelMessage represents a message received from the -/// data channel. IsString will be set to true if the incoming -/// message is of the string type. Otherwise the message is of -/// a binary type. -#[derive(Default, Debug, Clone)] -pub struct DataChannelMessage { - pub is_string: bool, - pub data: Bytes, -} diff --git a/webrtc/src/data_channel/data_channel_parameters.rs b/webrtc/src/data_channel/data_channel_parameters.rs deleted file mode 100644 index 88f116ed3..000000000 --- a/webrtc/src/data_channel/data_channel_parameters.rs +++ /dev/null @@ -1,12 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// DataChannelParameters describes the configuration of the DataChannel. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct DataChannelParameters { - pub label: String, - pub protocol: String, - pub ordered: bool, - pub max_packet_life_time: u16, - pub max_retransmits: u16, - pub negotiated: Option, -} diff --git a/webrtc/src/data_channel/data_channel_state.rs b/webrtc/src/data_channel/data_channel_state.rs deleted file mode 100644 index 38ea04cb0..000000000 --- a/webrtc/src/data_channel/data_channel_state.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// DataChannelState indicates the state of a data channel. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RTCDataChannelState { - #[serde(rename = "unspecified")] - #[default] - Unspecified = 0, - - /// DataChannelStateConnecting indicates that the data channel is being - /// established. This is the initial state of DataChannel, whether created - /// with create_data_channel, or dispatched as a part of an DataChannelEvent. - #[serde(rename = "connecting")] - Connecting, - - /// DataChannelStateOpen indicates that the underlying data transport is - /// established and communication is possible. - #[serde(rename = "open")] - Open, - - /// DataChannelStateClosing indicates that the procedure to close down the - /// underlying data transport has started. - #[serde(rename = "closing")] - Closing, - - /// DataChannelStateClosed indicates that the underlying data transport - /// has been closed or could not be established. - #[serde(rename = "closed")] - Closed, -} - -const DATA_CHANNEL_STATE_CONNECTING_STR: &str = "connecting"; -const DATA_CHANNEL_STATE_OPEN_STR: &str = "open"; -const DATA_CHANNEL_STATE_CLOSING_STR: &str = "closing"; -const DATA_CHANNEL_STATE_CLOSED_STR: &str = "closed"; - -impl From for RTCDataChannelState { - fn from(v: u8) -> Self { - match v { - 1 => RTCDataChannelState::Connecting, - 2 => RTCDataChannelState::Open, - 3 => RTCDataChannelState::Closing, - 4 => RTCDataChannelState::Closed, - _ => RTCDataChannelState::Unspecified, - } - } -} - -impl From<&str> for RTCDataChannelState { - fn from(raw: &str) -> Self { - match raw { - DATA_CHANNEL_STATE_CONNECTING_STR => RTCDataChannelState::Connecting, - DATA_CHANNEL_STATE_OPEN_STR => RTCDataChannelState::Open, - DATA_CHANNEL_STATE_CLOSING_STR => RTCDataChannelState::Closing, - DATA_CHANNEL_STATE_CLOSED_STR => RTCDataChannelState::Closed, - _ => RTCDataChannelState::Unspecified, - } - } -} - -impl fmt::Display for RTCDataChannelState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCDataChannelState::Connecting => DATA_CHANNEL_STATE_CONNECTING_STR, - RTCDataChannelState::Open => DATA_CHANNEL_STATE_OPEN_STR, - RTCDataChannelState::Closing => DATA_CHANNEL_STATE_CLOSING_STR, - RTCDataChannelState::Closed => DATA_CHANNEL_STATE_CLOSED_STR, - RTCDataChannelState::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_data_channel_state() { - let tests = vec![ - (crate::UNSPECIFIED_STR, RTCDataChannelState::Unspecified), - ("connecting", RTCDataChannelState::Connecting), - ("open", RTCDataChannelState::Open), - ("closing", RTCDataChannelState::Closing), - ("closed", RTCDataChannelState::Closed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!( - RTCDataChannelState::from(state_string), - expected_state, - "testCase: {expected_state}", - ); - } - } - - #[test] - fn test_data_channel_state_string() { - let tests = vec![ - (RTCDataChannelState::Unspecified, crate::UNSPECIFIED_STR), - (RTCDataChannelState::Connecting, "connecting"), - (RTCDataChannelState::Open, "open"), - (RTCDataChannelState::Closing, "closing"), - (RTCDataChannelState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string) - } - } -} diff --git a/webrtc/src/data_channel/data_channel_test.rs b/webrtc/src/data_channel/data_channel_test.rs deleted file mode 100644 index 65e8eb9f1..000000000 --- a/webrtc/src/data_channel/data_channel_test.rs +++ /dev/null @@ -1,1504 +0,0 @@ -// Silence warning on `for i in 0..vec.len() { … }`: -#![allow(clippy::needless_range_loop)] - -use regex::Regex; -use tokio::sync::mpsc; -use tokio::time::Duration; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::media_engine::MediaEngine; -use crate::api::{APIBuilder, API}; -use crate::data_channel::data_channel_init::RTCDataChannelInit; -//use log::LevelFilter; -//use std::io::Write; -use crate::dtls_transport::dtls_parameters::DTLSParameters; -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::flatten_errs; -use crate::ice_transport::ice_candidate::RTCIceCandidate; -use crate::ice_transport::ice_connection_state::RTCIceConnectionState; -use crate::ice_transport::ice_gatherer::{RTCIceGatherOptions, RTCIceGatherer}; -use crate::ice_transport::ice_parameters::RTCIceParameters; -use crate::ice_transport::ice_role::RTCIceRole; -use crate::ice_transport::RTCIceTransport; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::peer_connection_test::*; -use crate::peer_connection::RTCPeerConnection; -use crate::sctp_transport::sctp_transport_capabilities::SCTPTransportCapabilities; - -// EXPECTED_LABEL represents the label of the data channel we are trying to test. -// Some other channels may have been created during initialization (in the Wasm -// bindings this is a requirement). -const EXPECTED_LABEL: &str = "data"; - -async fn set_up_data_channel_parameters_test( - api: &API, - options: Option, -) -> Result<( - RTCPeerConnection, - RTCPeerConnection, - Arc, - mpsc::Sender<()>, - mpsc::Receiver<()>, -)> { - let (offer_pc, answer_pc) = new_pair(api).await?; - let (done_tx, done_rx) = mpsc::channel(1); - - let dc = offer_pc - .create_data_channel(EXPECTED_LABEL, options) - .await?; - Ok((offer_pc, answer_pc, dc, done_tx, done_rx)) -} - -async fn close_reliability_param_test( - pc1: &mut RTCPeerConnection, - pc2: &mut RTCPeerConnection, - done_rx: mpsc::Receiver<()>, -) -> Result<()> { - signal_pair(pc1, pc2).await?; - - close_pair(pc1, pc2, done_rx).await; - - Ok(()) -} - -/* -TODO: #[tokio::test] async fnBenchmarkDataChannelSend2(b *testing.B) { benchmarkDataChannelSend(b, 2) } -#[tokio::test] async fnBenchmarkDataChannelSend4(b *testing.B) { benchmarkDataChannelSend(b, 4) } -#[tokio::test] async fnBenchmarkDataChannelSend8(b *testing.B) { benchmarkDataChannelSend(b, 8) } -#[tokio::test] async fnBenchmarkDataChannelSend16(b *testing.B) { benchmarkDataChannelSend(b, 16) } -#[tokio::test] async fnBenchmarkDataChannelSend32(b *testing.B) { benchmarkDataChannelSend(b, 32) } - -// See https://github.com/pion/webrtc/issues/1516 -#[tokio::test] async fnbenchmarkDataChannelSend(b *testing.B, numChannels int) { - offerPC, answerPC, err := newPair() - if err != nil { - b.Fatalf("Failed to create a PC pair for testing") - } - - open := make(map[string]chan bool) - answerPC.OnDataChannel(func(d *DataChannel) { - if _, ok := open[d.Label()]; !ok { - // Ignore anything unknown channel label. - return - } - d.OnOpen(func() { open[d.Label()] <- true }) - }) - - var wg sync.WaitGroup - for i := 0; i < numChannels; i++ { - label := fmt.Sprintf("dc-%d", i) - open[label] = make(chan bool) - wg.Add(1) - dc, err := offerPC.CreateDataChannel(label, nil) - assert.NoError(b, err) - - dc.OnOpen(func() { - <-open[label] - for n := 0; n < b.N/numChannels; n++ { - if err := dc.SendText("Ping"); err != nil { - b.Fatalf("Unexpected error sending data (label=%q): %v", label, err) - } - } - wg.Done() - }) - } - - assert.NoError(b, signalPair(offerPC, answerPC)) - wg.Wait() - close_pair_now(b, offerPC, answerPC) -} -*/ - -#[tokio::test] -async fn test_data_channel_open() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - //"handler should be called once" - { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offer_pc, mut answer_pc) = new_pair(&api).await?; - - let (done_tx, done_rx) = mpsc::channel(1); - let (open_calls_tx, mut open_calls_rx) = mpsc::channel(2); - - let open_calls_tx = Arc::new(open_calls_tx); - let done_tx = Arc::new(done_tx); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - if d.label() == EXPECTED_LABEL { - let open_calls_tx2 = Arc::clone(&open_calls_tx); - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - d.on_open(Box::new(move || { - Box::pin(async move { - let _ = open_calls_tx2.send(()).await; - }) - })); - d.on_message(Box::new(move |_: DataChannelMessage| { - let done_tx3 = Arc::clone(&done_tx2); - tokio::spawn(async move { - // Wait a little bit to ensure all messages are processed. - tokio::time::sleep(Duration::from_millis(100)).await; - let _ = done_tx3.send(()).await; - }); - Box::pin(async {}) - })); - }) - } else { - Box::pin(async {}) - } - })); - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - let dc2 = Arc::clone(&dc); - dc.on_open(Box::new(move || { - Box::pin(async move { - let result = dc2.send_text("Ping".to_owned()).await; - assert!(result.is_ok(), "Failed to send string on data channel"); - }) - })); - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - close_pair(&offer_pc, &answer_pc, done_rx).await; - - let _ = open_calls_rx.recv().await; - } - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_send_before_signaling() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - //"before signaling" - - let (mut offer_pc, mut answer_pc) = new_pair(&api).await?; - - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - Box::pin(async move { - let d2 = Arc::clone(&d); - d.on_message(Box::new(move |_: DataChannelMessage| { - let d3 = Arc::clone(&d2); - Box::pin(async move { - let result = d3.send(&Bytes::from(b"Pong".to_vec())).await; - assert!(result.is_ok(), "Failed to send string on data channel"); - }) - })); - assert!(d.ordered(), "Ordered should be set to true"); - }) - })); - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - assert!(dc.ordered(), "Ordered should be set to true"); - - let dc2 = Arc::clone(&dc); - dc.on_open(Box::new(move || { - let dc3 = Arc::clone(&dc2); - Box::pin(async move { - let result = dc3.send_text("Ping".to_owned()).await; - assert!(result.is_ok(), "Failed to send string on data channel"); - }) - })); - - let (done_tx, done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - dc.on_message(Box::new(move |_: DataChannelMessage| { - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - close_pair(&offer_pc, &answer_pc, done_rx).await; - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_send_after_connected() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offer_pc, mut answer_pc) = new_pair(&api).await?; - - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - Box::pin(async move { - let d2 = Arc::clone(&d); - d.on_message(Box::new(move |_: DataChannelMessage| { - let d3 = Arc::clone(&d2); - - Box::pin(async move { - let result = d3.send(&Bytes::from(b"Pong".to_vec())).await; - assert!(result.is_ok(), "Failed to send string on data channel"); - }) - })); - assert!(d.ordered(), "Ordered should be set to true"); - }) - })); - - let dc = offer_pc - .create_data_channel(EXPECTED_LABEL, None) - .await - .expect("Failed to create a PC pair for testing"); - - let (done_tx, done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - - //once := &sync.Once{} - offer_pc.on_ice_connection_state_change(Box::new(move |state: RTCIceConnectionState| { - let done_tx1 = Arc::clone(&done_tx); - let dc1 = Arc::clone(&dc); - Box::pin(async move { - if state == RTCIceConnectionState::Connected - || state == RTCIceConnectionState::Completed - { - // wasm fires completed state multiple times - /*once.Do(func()*/ - { - assert!(dc1.ordered(), "Ordered should be set to true"); - - dc1.on_message(Box::new(move |_: DataChannelMessage| { - let done_tx2 = Arc::clone(&done_tx1); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - if dc1.send_text("Ping".to_owned()).await.is_err() { - // wasm binding doesn't fire OnOpen (we probably already missed it) - let dc2 = Arc::clone(&dc1); - dc1.on_open(Box::new(move || { - let dc3 = Arc::clone(&dc2); - Box::pin(async move { - let result = dc3.send_text("Ping".to_owned()).await; - assert!(result.is_ok(), "Failed to send string on data channel"); - }) - })); - } - } - } - }) - })); - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - close_pair(&offer_pc, &answer_pc, done_rx).await; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_close() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - // "Close after PeerConnection Closed" - { - let (offer_pc, answer_pc) = new_pair(&api).await?; - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - close_pair_now(&offer_pc, &answer_pc).await; - dc.close().await?; - } - - // "Close before connected" - { - let (offer_pc, answer_pc) = new_pair(&api).await?; - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - dc.close().await?; - close_pair_now(&offer_pc, &answer_pc).await; - } - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_parameters_max_packet_life_time_exchange() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let ordered = true; - let max_packet_life_time = 3u16; - let options = RTCDataChannelInit { - ordered: Some(ordered), - max_packet_life_time: Some(max_packet_life_time), - ..Default::default() - }; - - let (mut offer_pc, mut answer_pc, dc, done_tx, done_rx) = - set_up_data_channel_parameters_test(&api, Some(options)).await?; - - // Check if parameters are correctly set - assert_eq!( - dc.ordered(), - ordered, - "Ordered should be same value as set in DataChannelInit" - ); - assert_eq!( - dc.max_packet_lifetime(), - max_packet_life_time, - "should match" - ); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - // Check if parameters are correctly set - assert_eq!( - d.ordered(), - ordered, - "Ordered should be same value as set in DataChannelInit" - ); - assert_eq!( - d.max_packet_lifetime(), - max_packet_life_time, - "should match" - ); - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - close_reliability_param_test(&mut offer_pc, &mut answer_pc, done_rx).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_parameters_max_retransmits_exchange() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let ordered = false; - let max_retransmits = 3000u16; - let options = RTCDataChannelInit { - ordered: Some(ordered), - max_retransmits: Some(max_retransmits), - ..Default::default() - }; - - let (mut offer_pc, mut answer_pc, dc, done_tx, done_rx) = - set_up_data_channel_parameters_test(&api, Some(options)).await?; - - // Check if parameters are correctly set - assert!(!dc.ordered(), "Ordered should be set to false"); - assert_eq!(dc.max_retransmits(), max_retransmits, "should match"); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - - // Check if parameters are correctly set - assert!(!d.ordered(), "Ordered should be set to false"); - assert_eq!(max_retransmits, d.max_retransmits(), "should match"); - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - close_reliability_param_test(&mut offer_pc, &mut answer_pc, done_rx).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_parameters_protocol_exchange() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let protocol = "json".to_owned(); - let options = RTCDataChannelInit { - protocol: Some(protocol.clone()), - ..Default::default() - }; - - let (mut offer_pc, mut answer_pc, dc, done_tx, done_rx) = - set_up_data_channel_parameters_test(&api, Some(options)).await?; - - // Check if parameters are correctly set - assert_eq!( - protocol, - dc.protocol(), - "Protocol should match DataChannelConfig" - ); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - // Check if parameters are correctly set - assert_eq!( - protocol, - d.protocol(), - "Protocol should match what channel creator declared" - ); - - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - close_reliability_param_test(&mut offer_pc, &mut answer_pc, done_rx).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_parameters_negotiated_exchange() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - const EXPECTED_MESSAGE: &str = "Hello World"; - - let id = 500u16; - let options = RTCDataChannelInit { - negotiated: Some(id), - ..Default::default() - }; - - let (mut offer_pc, mut answer_pc, offer_datachannel, done_tx, done_rx) = - set_up_data_channel_parameters_test(&api, Some(options.clone())).await?; - - let answer_datachannel = answer_pc - .create_data_channel(EXPECTED_LABEL, Some(options)) - .await?; - - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Ignore our default channel, exists to force ICE candidates. See signalPair for more info - if d.label() == "initial_data_channel" { - return Box::pin(async {}); - } - panic!("OnDataChannel must not be fired when negotiated == true"); - })); - - offer_pc.on_data_channel(Box::new(move |_d: Arc| { - panic!("OnDataChannel must not be fired when negotiated == true"); - })); - - let seen_answer_message = Arc::new(AtomicBool::new(false)); - let seen_offer_message = Arc::new(AtomicBool::new(false)); - - let seen_answer_message2 = Arc::clone(&seen_answer_message); - answer_datachannel.on_message(Box::new(move |msg: DataChannelMessage| { - if msg.is_string && msg.data == EXPECTED_MESSAGE { - seen_answer_message2.store(true, Ordering::SeqCst); - } - - Box::pin(async {}) - })); - - let seen_offer_message2 = Arc::clone(&seen_offer_message); - offer_datachannel.on_message(Box::new(move |msg: DataChannelMessage| { - if msg.is_string && msg.data == EXPECTED_MESSAGE { - seen_offer_message2.store(true, Ordering::SeqCst); - } - Box::pin(async {}) - })); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - tokio::spawn(async move { - loop { - if seen_answer_message.load(Ordering::SeqCst) - && seen_offer_message.load(Ordering::SeqCst) - { - break; - } - - if offer_datachannel.ready_state() == RTCDataChannelState::Open { - offer_datachannel - .send_text(EXPECTED_MESSAGE.to_owned()) - .await?; - } - if answer_datachannel.ready_state() == RTCDataChannelState::Open { - answer_datachannel - .send_text(EXPECTED_MESSAGE.to_owned()) - .await?; - } - - tokio::time::sleep(Duration::from_millis(50)).await; - } - - let mut done = done_tx.lock().await; - done.take(); - - Result::<()>::Ok(()) - }); - - close_reliability_param_test(&mut offer_pc, &mut answer_pc, done_rx).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_event_handlers() -> Result<()> { - let api = APIBuilder::new().build(); - - let dc = RTCDataChannel { - setting_engine: Arc::clone(&api.setting_engine), - ..Default::default() - }; - - let (on_open_called_tx, mut on_open_called_rx) = mpsc::channel::<()>(1); - let (on_message_called_tx, mut on_message_called_rx) = mpsc::channel::<()>(1); - - // Verify that the noop case works - dc.do_open(); - - let on_open_called_tx = Arc::new(Mutex::new(Some(on_open_called_tx))); - dc.on_open(Box::new(move || { - let on_open_called_tx2 = Arc::clone(&on_open_called_tx); - Box::pin(async move { - let mut done = on_open_called_tx2.lock().await; - done.take(); - }) - })); - - let on_message_called_tx = Arc::new(Mutex::new(Some(on_message_called_tx))); - dc.on_message(Box::new(move |_: DataChannelMessage| { - let on_message_called_tx2 = Arc::clone(&on_message_called_tx); - Box::pin(async move { - let mut done = on_message_called_tx2.lock().await; - done.take(); - }) - })); - - // Verify that the set handlers are called - dc.do_open(); - dc.do_message(DataChannelMessage { - is_string: false, - data: Bytes::from_static(b"o hai"), - }) - .await; - - // Wait for all handlers to be called - let _ = on_open_called_rx.recv().await; - let _ = on_message_called_rx.recv().await; - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_messages_are_ordered() -> Result<()> { - let api = APIBuilder::new().build(); - - let dc = RTCDataChannel { - setting_engine: Arc::clone(&api.setting_engine), - ..Default::default() - }; - - let m = 16u64; - let (out_tx, mut out_rx) = mpsc::channel::(m as usize); - - let out_tx = Arc::new(out_tx); - - let out_tx1 = Arc::clone(&out_tx); - dc.on_message(Box::new(move |msg: DataChannelMessage| { - let out_tx2 = Arc::clone(&out_tx1); - - Box::pin(async move { - // randomly sleep - let r = rand::random::() % m; - tokio::time::sleep(Duration::from_millis(r)).await; - - let mut buf = [0u8; 8]; - for i in 0..8 { - buf[i] = msg.data[i]; - } - let s = u64::from_be_bytes(buf); - - let _ = out_tx2.send(s).await; - }) - })); - - tokio::spawn(async move { - for j in 1..=m { - let buf = j.to_be_bytes().to_vec(); - - dc.do_message(DataChannelMessage { - is_string: false, - data: Bytes::from(buf), - }) - .await; - // Change the registered handler a couple of times to make sure - // that everything continues to work, we don't lose messages, etc. - if j % 2 == 0 { - let out_tx1 = Arc::clone(&out_tx); - dc.on_message(Box::new(move |msg: DataChannelMessage| { - let out_tx2 = Arc::clone(&out_tx1); - - Box::pin(async move { - // randomly sleep - let r = rand::random::() % m; - tokio::time::sleep(Duration::from_millis(r)).await; - - let mut buf = [0u8; 8]; - for i in 0..8 { - buf[i] = msg.data[i]; - } - let s = u64::from_be_bytes(buf); - - let _ = out_tx2.send(s).await; - }) - })); - } - } - }); - - let mut values = vec![]; - for _ in 1..=m { - if let Some(v) = out_rx.recv().await { - values.push(v); - } else { - break; - } - } - - let mut expected = vec![0u64; m as usize]; - for i in 1..=m as usize { - expected[i - 1] = i as u64; - } - assert_eq!(values, expected); - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_parameters_go() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - //"MaxPacketLifeTime exchange" - { - let ordered = true; - let max_packet_life_time = 3u16; - let options = RTCDataChannelInit { - ordered: Some(ordered), - max_packet_life_time: Some(max_packet_life_time), - ..Default::default() - }; - - let (mut offer_pc, mut answer_pc, dc, done_tx, done_rx) = - set_up_data_channel_parameters_test(&api, Some(options)).await?; - - // Check if parameters are correctly set - assert!(dc.ordered(), "Ordered should be set to true"); - assert_eq!( - max_packet_life_time, - dc.max_packet_lifetime(), - "should match" - ); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - - // Check if parameters are correctly set - assert!(d.ordered, "Ordered should be set to true"); - assert_eq!( - max_packet_life_time, - d.max_packet_lifetime(), - "should match" - ); - - let done_tx2 = Arc::clone(&done_tx); - Box::pin(async move { - let mut done = done_tx2.lock().await; - done.take(); - }) - })); - - close_reliability_param_test(&mut offer_pc, &mut answer_pc, done_rx).await?; - } - - //"All other property methods" - { - let id = 123u16; - let dc = RTCDataChannel { - id: AtomicU16::new(id), - label: "mylabel".to_owned(), - protocol: "myprotocol".to_owned(), - negotiated: true, - ..Default::default() - }; - - assert_eq!(dc.id.load(Ordering::SeqCst), dc.id(), "should match"); - assert_eq!(dc.label, dc.label(), "should match"); - assert_eq!(dc.protocol, dc.protocol(), "should match"); - assert_eq!(dc.negotiated, dc.negotiated(), "should match"); - assert_eq!(0, dc.buffered_amount().await, "should match"); - dc.set_buffered_amount_low_threshold(1500).await; - assert_eq!( - 1500, - dc.buffered_amount_low_threshold().await, - "should match" - ); - } - - Ok(()) -} - -//use log::LevelFilter; -//use std::io::Write; - -#[tokio::test] -async fn test_data_channel_buffered_amount_set_before_open() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let n_cbs = Arc::new(AtomicU16::new(0)); - let buf = Bytes::from_static(&[0u8; 1000]); - - let (mut offer_pc, mut answer_pc) = new_pair(&api).await?; - - let (done_tx, done_rx) = mpsc::channel::<()>(1); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - let n_packets_received = Arc::new(AtomicU16::new(0)); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - - let done_tx2 = Arc::clone(&done_tx); - let n_packets_received2 = Arc::clone(&n_packets_received); - Box::pin(async move { - d.on_message(Box::new(move |_msg: DataChannelMessage| { - let n = n_packets_received2.fetch_add(1, Ordering::SeqCst); - if n == 9 { - let done_tx3 = Arc::clone(&done_tx2); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(10)).await; - let mut done = done_tx3.lock().await; - done.take(); - }); - } - - Box::pin(async {}) - })); - - assert!(d.ordered(), "Ordered should be set to true"); - }) - })); - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - assert!(dc.ordered(), "Ordered should be set to true"); - - let dc2 = Arc::clone(&dc); - dc.on_open(Box::new(move || { - let dc3 = Arc::clone(&dc2); - Box::pin(async move { - for _ in 0..10 { - assert!( - dc3.send(&buf).await.is_ok(), - "Failed to send string on data channel" - ); - assert_eq!( - 1500, - dc3.buffered_amount_low_threshold().await, - "value mismatch" - ); - } - }) - })); - - dc.on_message(Box::new(|_msg: DataChannelMessage| Box::pin(async {}))); - - // The value is temporarily stored in the dc object - // until the dc gets opened - dc.set_buffered_amount_low_threshold(1500).await; - // The callback function is temporarily stored in the dc object - // until the dc gets opened - let n_cbs2 = Arc::clone(&n_cbs); - dc.on_buffered_amount_low(Box::new(move || { - n_cbs2.fetch_add(1, Ordering::SeqCst); - Box::pin(async {}) - })) - .await; - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - close_pair(&offer_pc, &answer_pc, done_rx).await; - - assert!( - n_cbs.load(Ordering::SeqCst) > 0, - "callback should be made at least once" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_data_channel_buffered_amount_set_after_open() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let n_cbs = Arc::new(AtomicU16::new(0)); - let buf = Bytes::from_static(&[0u8; 1000]); - - let (mut offer_pc, mut answer_pc) = new_pair(&api).await?; - - let (done_tx, done_rx) = mpsc::channel::<()>(1); - - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - let n_packets_received = Arc::new(AtomicU16::new(0)); - answer_pc.on_data_channel(Box::new(move |d: Arc| { - // Make sure this is the data channel we were looking for. (Not the one - // created in signalPair). - if d.label() != EXPECTED_LABEL { - return Box::pin(async {}); - } - - let done_tx2 = Arc::clone(&done_tx); - let n_packets_received2 = Arc::clone(&n_packets_received); - Box::pin(async move { - d.on_message(Box::new(move |_msg: DataChannelMessage| { - let n = n_packets_received2.fetch_add(1, Ordering::SeqCst); - if n == 9 { - let done_tx3 = Arc::clone(&done_tx2); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(10)).await; - let mut done = done_tx3.lock().await; - done.take(); - }); - } - - Box::pin(async {}) - })); - - assert!(d.ordered(), "Ordered should be set to true"); - }) - })); - - let dc = offer_pc.create_data_channel(EXPECTED_LABEL, None).await?; - - assert!(dc.ordered(), "Ordered should be set to true"); - - let dc2 = Arc::clone(&dc); - let n_cbs2 = Arc::clone(&n_cbs); - dc.on_open(Box::new(move || { - let dc3 = Arc::clone(&dc2); - Box::pin(async move { - // The value should directly be passed to sctp - dc3.set_buffered_amount_low_threshold(1500).await; - // The callback function should directly be passed to sctp - dc3.on_buffered_amount_low(Box::new(move || { - n_cbs2.fetch_add(1, Ordering::SeqCst); - Box::pin(async {}) - })) - .await; - - for _ in 0..10 { - assert!( - dc3.send(&buf).await.is_ok(), - "Failed to send string on data channel" - ); - assert_eq!( - 1500, - dc3.buffered_amount_low_threshold().await, - "value mismatch" - ); - } - }) - })); - - dc.on_message(Box::new(|_msg: DataChannelMessage| Box::pin(async {}))); - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - close_pair(&offer_pc, &answer_pc, done_rx).await; - - assert!( - n_cbs.load(Ordering::SeqCst) > 0, - "callback should be made at least once" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_eof_detach() -> Result<()> { - let label: &str = "test-channel"; - let test_data: &'static str = "this is some test data"; - - // Use Detach data channels mode - let mut s = SettingEngine::default(); - s.detach_data_channels(); - let api = APIBuilder::new().with_setting_engine(s).build(); - - // Set up two peer connections. - let mut pca = api.new_peer_connection(RTCConfiguration::default()).await?; - let mut pcb = api.new_peer_connection(RTCConfiguration::default()).await?; - - let wg = WaitGroup::new(); - - let (dc_chan_tx, mut dc_chan_rx) = mpsc::channel(1); - let dc_chan_tx = Arc::new(dc_chan_tx); - pcb.on_data_channel(Box::new(move |dc: Arc| { - if dc.label() != label { - return Box::pin(async {}); - } - log::debug!("OnDataChannel was called"); - let dc_chan_tx2 = Arc::clone(&dc_chan_tx); - let dc2 = Arc::clone(&dc); - Box::pin(async move { - let dc3 = Arc::clone(&dc2); - dc2.on_open(Box::new(move || { - let dc_chan_tx3 = Arc::clone(&dc_chan_tx2); - let dc4 = Arc::clone(&dc3); - Box::pin(async move { - let detached = match dc4.detach().await { - Ok(detached) => detached, - Err(err) => { - log::debug!("Detach failed: {}", err); - panic!(); - } - }; - - let _ = dc_chan_tx3.send(detached).await; - }) - })); - }) - })); - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - - log::debug!("Waiting for OnDataChannel"); - let dc = dc_chan_rx.recv().await.unwrap(); - log::debug!("data channel opened"); - - log::debug!("Waiting for ping..."); - let mut msg = vec![0u8; 256]; - let n = dc.read(&mut msg).await?; - log::debug!("Received ping! {:?}\n", &msg[..n]); - - assert_eq!(test_data.as_bytes(), &msg[..n]); - log::debug!("Received ping successfully!"); - - dc.close().await?; - - Result::<()>::Ok(()) - }); - - signal_pair(&mut pca, &mut pcb).await?; - - let attached = pca.create_data_channel(label, None).await?; - - log::debug!("Waiting for data channel to open"); - let (open_tx, mut open_rx) = mpsc::channel::<()>(1); - let open_tx = Arc::new(open_tx); - attached.on_open(Box::new(move || { - let open_tx2 = Arc::clone(&open_tx); - Box::pin(async move { - let _ = open_tx2.send(()).await; - }) - })); - - let _ = open_rx.recv().await; - log::debug!("data channel opened"); - - let dc = attached.detach().await?; - - let w = wg.worker(); - tokio::spawn(async move { - let _d = w; - log::debug!("Sending ping..."); - dc.write(&Bytes::from_static(test_data.as_bytes())).await?; - log::debug!("Sent ping"); - - dc.close().await?; - - log::debug!("Waiting for EOF"); - let mut buf = vec![0u8; 256]; - let n = dc.read(&mut buf).await?; - assert_eq!(0, n, "should be empty"); - - Result::<()>::Ok(()) - }); - - wg.wait().await; - - close_pair_now(&pca, &pcb).await; - - Ok(()) -} - -#[tokio::test] -async fn test_eof_no_detach() -> Result<()> { - let label: &str = "test-channel"; - let test_data: &'static [u8] = b"this is some test data"; - - let api = APIBuilder::new().build(); - - // Set up two peer connections. - let mut pca = api.new_peer_connection(RTCConfiguration::default()).await?; - let mut pcb = api.new_peer_connection(RTCConfiguration::default()).await?; - - let (dca_closed_ch_tx, mut dca_closed_ch_rx) = mpsc::channel::<()>(1); - let (dcb_closed_ch_tx, mut dcb_closed_ch_rx) = mpsc::channel::<()>(1); - - let dcb_closed_ch_tx = Arc::new(dcb_closed_ch_tx); - pcb.on_data_channel(Box::new(move |dc: Arc| { - if dc.label() != label { - return Box::pin(async {}); - } - - log::debug!("pcb: new datachannel: {}", dc.label()); - - let dcb_closed_ch_tx2 = Arc::clone(&dcb_closed_ch_tx); - Box::pin(async move { - // Register channel opening handling - dc.on_open(Box::new(move || { - log::debug!("pcb: datachannel opened"); - Box::pin(async {}) - })); - - dc.on_close(Box::new(move || { - // (2) - log::debug!("pcb: data channel closed"); - let dcb_closed_ch_tx3 = Arc::clone(&dcb_closed_ch_tx2); - Box::pin(async move { - let _ = dcb_closed_ch_tx3.send(()).await; - }) - })); - - // Register the OnMessage to handle incoming messages - log::debug!("pcb: registering onMessage callback"); - dc.on_message(Box::new(|dc_msg: DataChannelMessage| { - let test_data: &'static [u8] = b"this is some test data"; - log::debug!("pcb: received ping: {:?}", dc_msg.data); - assert_eq!(&dc_msg.data[..], test_data, "data mismatch"); - Box::pin(async {}) - })); - }) - })); - - let dca = pca.create_data_channel(label, None).await?; - let dca2 = Arc::clone(&dca); - dca.on_open(Box::new(move || { - log::debug!("pca: data channel opened"); - log::debug!("pca: sending {:?}", test_data); - let dca3 = Arc::clone(&dca2); - Box::pin(async move { - let _ = dca3.send(&Bytes::from_static(test_data)).await; - log::debug!("pca: sent ping"); - assert!(dca3.close().await.is_ok(), "should succeed"); // <-- dca closes - }) - })); - - let dca_closed_ch_tx = Arc::new(dca_closed_ch_tx); - dca.on_close(Box::new(move || { - // (1) - log::debug!("pca: data channel closed"); - let dca_closed_ch_tx2 = Arc::clone(&dca_closed_ch_tx); - Box::pin(async move { - let _ = dca_closed_ch_tx2.send(()).await; - }) - })); - - // Register the OnMessage to handle incoming messages - log::debug!("pca: registering onMessage callback"); - dca.on_message(Box::new(move |dc_msg: DataChannelMessage| { - log::debug!("pca: received pong: {:?}", &dc_msg.data[..]); - assert_eq!(&dc_msg.data[..], test_data, "data mismatch"); - Box::pin(async {}) - })); - - signal_pair(&mut pca, &mut pcb).await?; - - // When dca closes the channel, - // (1) dca.Onclose() will fire immediately, then - // (2) dcb.OnClose will also fire - let _ = dca_closed_ch_rx.recv().await; // (1) - let _ = dcb_closed_ch_rx.recv().await; // (2) - - close_pair_now(&pca, &pcb).await; - - Ok(()) -} - -// Assert that a Session Description that doesn't follow -// draft-ietf-mmusic-sctp-sdp is still accepted -#[tokio::test] -async fn test_data_channel_non_standard_session_description() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (offer_pc, answer_pc) = new_pair(&api).await?; - - let _ = offer_pc.create_data_channel("foo", None).await?; - - let (on_data_channel_called_tx, mut on_data_channel_called_rx) = mpsc::channel::<()>(1); - let on_data_channel_called_tx = Arc::new(on_data_channel_called_tx); - answer_pc.on_data_channel(Box::new(move |_: Arc| { - let on_data_channel_called_tx2 = Arc::clone(&on_data_channel_called_tx); - Box::pin(async move { - let _ = on_data_channel_called_tx2.send(()).await; - }) - })); - - let offer = offer_pc.create_offer(None).await?; - - let mut offer_gathering_complete = offer_pc.gathering_complete_promise().await; - offer_pc.set_local_description(offer).await?; - let _ = offer_gathering_complete.recv().await; - - let mut offer = offer_pc.local_description().await.unwrap(); - - // Replace with old values - const OLD_APPLICATION: &str = "m=application 63743 DTLS/SCTP 5000\r"; - const OLD_ATTRIBUTE: &str = "a=sctpmap:5000 webrtc-datachannel 256\r"; - - let re = Regex::new(r"m=application (.*?)\r").unwrap(); - offer.sdp = re - .replace_all(offer.sdp.as_str(), OLD_APPLICATION) - .to_string(); - let re = Regex::new(r"a=sctp-port(.*?)\r").unwrap(); - offer.sdp = re - .replace_all(offer.sdp.as_str(), OLD_ATTRIBUTE) - .to_string(); - - // Assert that replace worked - assert!(offer.sdp.contains(OLD_APPLICATION)); - assert!(offer.sdp.contains(OLD_ATTRIBUTE)); - - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - - let mut answer_gathering_complete = answer_pc.gathering_complete_promise().await; - answer_pc.set_local_description(answer).await?; - let _ = answer_gathering_complete.recv().await; - - let answer = answer_pc.local_description().await.unwrap(); - offer_pc.set_remote_description(answer).await?; - - let _ = on_data_channel_called_rx.recv().await; - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} - -struct TestOrtcStack { - //api *API - gatherer: Arc, - ice: Arc, - dtls: Arc, - sctp: Arc, -} - -struct TestOrtcSignal { - ice_candidates: Vec, //`json:"iceCandidates"` - ice_parameters: RTCIceParameters, //`json:"iceParameters"` - dtls_parameters: DTLSParameters, //`json:"dtlsParameters"` - sctp_capabilities: SCTPTransportCapabilities, //`json:"sctpCapabilities"` -} - -impl TestOrtcStack { - async fn new(api: &API) -> Result { - // Create the ICE gatherer - let gatherer = Arc::new(api.new_ice_gatherer(RTCIceGatherOptions::default())?); - - // Construct the ICE transport - let ice = Arc::new(api.new_ice_transport(Arc::clone(&gatherer))); - - // Construct the DTLS transport - let dtls = Arc::new(api.new_dtls_transport(Arc::clone(&ice), vec![])?); - - // Construct the SCTP transport - let sctp = Arc::new(api.new_sctp_transport(Arc::clone(&dtls))?); - - Ok(TestOrtcStack { - gatherer, - ice, - dtls, - sctp, - }) - } - - async fn set_signal(&self, sig: &TestOrtcSignal, is_offer: bool) -> Result<()> { - let ice_role = if is_offer { - RTCIceRole::Controlling - } else { - RTCIceRole::Controlled - }; - - self.ice.set_remote_candidates(&sig.ice_candidates).await?; - - // Start the ICE transport - self.ice.start(&sig.ice_parameters, Some(ice_role)).await?; - - // Start the DTLS transport - self.dtls.start(sig.dtls_parameters.clone()).await?; - - // Start the SCTP transport - self.sctp.start(sig.sctp_capabilities).await?; - - Ok(()) - } - - async fn get_signal(&self) -> Result { - let (gather_finished_tx, mut gather_finished_rx) = mpsc::channel::<()>(1); - let gather_finished_tx = Arc::new(gather_finished_tx); - self.gatherer - .on_local_candidate(Box::new(move |i: Option| { - let gather_finished_tx2 = Arc::clone(&gather_finished_tx); - Box::pin(async move { - if i.is_none() { - let _ = gather_finished_tx2.send(()).await; - } - }) - })); - - self.gatherer.gather().await?; - - let _ = gather_finished_rx.recv().await; - - let ice_candidates = self.gatherer.get_local_candidates().await?; - - let ice_parameters = self.gatherer.get_local_parameters().await?; - - let dtls_parameters = self.dtls.get_local_parameters()?; - - let sctp_capabilities = self.sctp.get_capabilities(); - - Ok(TestOrtcSignal { - ice_candidates, - ice_parameters, - dtls_parameters, - sctp_capabilities, - }) - } - - async fn close(&self) -> Result<()> { - let mut close_errs = vec![]; - - if let Err(err) = self.sctp.stop().await { - close_errs.push(err); - } - - if let Err(err) = self.ice.stop().await { - close_errs.push(err); - } - - flatten_errs(close_errs) - } -} - -async fn new_ortc_pair(api: &API) -> Result<(Arc, Arc)> { - let sa = Arc::new(TestOrtcStack::new(api).await?); - let sb = Arc::new(TestOrtcStack::new(api).await?); - Ok((sa, sb)) -} - -async fn signal_ortc_pair(stack_a: Arc, stack_b: Arc) -> Result<()> { - let sig_a = stack_a.get_signal().await?; - let sig_b = stack_b.get_signal().await?; - - let (a_tx, mut a_rx) = mpsc::channel(1); - let (b_tx, mut b_rx) = mpsc::channel(1); - - tokio::spawn(async move { - let _ = a_tx.send(stack_b.set_signal(&sig_a, false).await).await; - }); - - tokio::spawn(async move { - let _ = b_tx.send(stack_a.set_signal(&sig_b, true).await).await; - }); - - let err_a = a_rx.recv().await.unwrap(); - let err_b = b_rx.recv().await.unwrap(); - - let mut close_errs = vec![]; - if let Err(err) = err_a { - close_errs.push(err); - } - if let Err(err) = err_b { - close_errs.push(err); - } - - flatten_errs(close_errs) -} - -#[tokio::test] -async fn test_data_channel_ortc_e2e() -> Result<()> { - let api = APIBuilder::new().build(); - - let (stack_a, stack_b) = new_ortc_pair(&api).await?; - - let (await_setup_tx, mut await_setup_rx) = mpsc::channel::<()>(1); - let (await_string_tx, mut await_string_rx) = mpsc::channel::<()>(1); - let (await_binary_tx, mut await_binary_rx) = mpsc::channel::<()>(1); - - let await_setup_tx = Arc::new(await_setup_tx); - let await_string_tx = Arc::new(await_string_tx); - let await_binary_tx = Arc::new(await_binary_tx); - stack_b - .sctp - .on_data_channel(Box::new(move |d: Arc| { - let await_setup_tx2 = Arc::clone(&await_setup_tx); - let await_string_tx2 = Arc::clone(&await_string_tx); - let await_binary_tx2 = Arc::clone(&await_binary_tx); - Box::pin(async move { - let _ = await_setup_tx2.send(()).await; - - d.on_message(Box::new(move |msg: DataChannelMessage| { - let await_string_tx3 = Arc::clone(&await_string_tx2); - let await_binary_tx3 = Arc::clone(&await_binary_tx2); - Box::pin(async move { - if msg.is_string { - let _ = await_string_tx3.send(()).await; - } else { - let _ = await_binary_tx3.send(()).await; - } - }) - })); - }) - })); - - signal_ortc_pair(Arc::clone(&stack_a), Arc::clone(&stack_b)).await?; - - let dc_params = DataChannelParameters { - label: "Foo".to_owned(), - negotiated: None, - ..Default::default() - }; - - let channel_a = api - .new_data_channel(Arc::clone(&stack_a.sctp), dc_params) - .await?; - - let _ = await_setup_rx.recv().await; - - channel_a.send_text("ABC".to_owned()).await?; - channel_a.send(&Bytes::from_static(b"ABC")).await?; - - let _ = await_string_rx.recv().await; - let _ = await_binary_rx.recv().await; - - stack_a.close().await?; - stack_b.close().await?; - - // attempt to send when channel is closed - let result = channel_a.send(&Bytes::from_static(b"ABC")).await; - if let Err(err) = result { - assert_eq!( - Error::ErrClosedPipe, - err, - "expected ErrClosedPipe, but got {err}" - ); - } else { - panic!(); - } - - let result = channel_a.send_text("test".to_owned()).await; - if let Err(err) = result { - assert_eq!( - Error::ErrClosedPipe, - err, - "expected ErrClosedPipe, but got {err}" - ); - } else { - panic!(); - } - - let result = channel_a.ensure_open(); - if let Err(err) = result { - assert_eq!( - Error::ErrClosedPipe, - err, - "expected ErrClosedPipe, but got {err}" - ); - } else { - panic!(); - } - - Ok(()) -} diff --git a/webrtc/src/data_channel/mod.rs b/webrtc/src/data_channel/mod.rs deleted file mode 100644 index bdf05af2f..000000000 --- a/webrtc/src/data_channel/mod.rs +++ /dev/null @@ -1,556 +0,0 @@ -#[cfg(test)] -mod data_channel_test; - -pub mod data_channel_init; -pub mod data_channel_message; -pub mod data_channel_parameters; -pub mod data_channel_state; - -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; -use std::time::SystemTime; - -use arc_swap::ArcSwapOption; -use bytes::Bytes; -use data::message::message_channel_open::ChannelType; -use data_channel_message::*; -use data_channel_parameters::*; -use data_channel_state::RTCDataChannelState; -use portable_atomic::{AtomicBool, AtomicU16, AtomicU8, AtomicUsize}; -use sctp::stream::OnBufferedAmountLowFn; -use tokio::sync::{Mutex, Notify}; -use util::sync::Mutex as SyncMutex; - -use crate::api::setting_engine::SettingEngine; -use crate::error::{Error, OnErrorHdlrFn, Result}; -use crate::sctp_transport::RTCSctpTransport; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::{DataChannelStats, StatsReportType}; - -/// message size limit for Chromium -const DATA_CHANNEL_BUFFER_SIZE: u16 = u16::MAX; - -pub type OnMessageHdlrFn = Box< - dyn (FnMut(DataChannelMessage) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnOpenHdlrFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -pub type OnCloseHdlrFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -/// DataChannel represents a WebRTC DataChannel -/// The DataChannel interface represents a network channel -/// which can be used for bidirectional peer-to-peer transfers of arbitrary data -#[derive(Default)] -pub struct RTCDataChannel { - pub(crate) stats_id: String, - pub(crate) label: String, - pub(crate) ordered: bool, - pub(crate) max_packet_lifetime: u16, - pub(crate) max_retransmits: u16, - pub(crate) protocol: String, - pub(crate) negotiated: bool, - pub(crate) id: AtomicU16, - pub(crate) ready_state: Arc, // DataChannelState - pub(crate) buffered_amount_low_threshold: AtomicUsize, - pub(crate) detach_called: Arc, - - // The binaryType represents attribute MUST, on getting, return the value to - // which it was last set. On setting, if the new value is either the string - // "blob" or the string "arraybuffer", then set the IDL attribute to this - // new value. Otherwise, throw a SyntaxError. When an DataChannel object - // is created, the binaryType attribute MUST be initialized to the string - // "blob". This attribute controls how binary data is exposed to scripts. - // binaryType string - pub(crate) on_message_handler: Arc>>, - pub(crate) on_open_handler: SyncMutex>, - pub(crate) on_close_handler: Arc>>, - pub(crate) on_error_handler: Arc>>, - - pub(crate) on_buffered_amount_low: Mutex>, - - pub(crate) sctp_transport: Mutex>>, - pub(crate) data_channel: Mutex>>, - - pub(crate) notify_tx: Arc, - - // A reference to the associated api object used by this datachannel - pub(crate) setting_engine: Arc, -} - -impl RTCDataChannel { - // create the DataChannel object before the networking is set up. - pub(crate) fn new(params: DataChannelParameters, setting_engine: Arc) -> Self { - // the id value if non-negotiated doesn't matter, since it will be overwritten - // on opening - let id = params.negotiated.unwrap_or(0); - RTCDataChannel { - stats_id: format!( - "DataChannel-{}", - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map_or(0, |d| d.as_nanos()) - ), - label: params.label, - protocol: params.protocol, - negotiated: params.negotiated.is_some(), - id: AtomicU16::new(id), - ordered: params.ordered, - max_packet_lifetime: params.max_packet_life_time, - max_retransmits: params.max_retransmits, - ready_state: Arc::new(AtomicU8::new(RTCDataChannelState::Connecting as u8)), - detach_called: Arc::new(AtomicBool::new(false)), - - notify_tx: Arc::new(Notify::new()), - - setting_engine, - ..Default::default() - } - } - - /// open opens the datachannel over the sctp transport - pub(crate) async fn open(&self, sctp_transport: Arc) -> Result<()> { - if let Some(association) = sctp_transport.association().await { - { - let mut st = self.sctp_transport.lock().await; - if st.is_none() { - *st = Some(Arc::downgrade(&sctp_transport)); - } else { - return Ok(()); - } - } - - let channel_type; - let reliability_parameter; - - if self.max_packet_lifetime == 0 && self.max_retransmits == 0 { - reliability_parameter = 0u32; - if self.ordered { - channel_type = ChannelType::Reliable; - } else { - channel_type = ChannelType::ReliableUnordered; - } - } else if self.max_retransmits != 0 { - reliability_parameter = self.max_retransmits as u32; - if self.ordered { - channel_type = ChannelType::PartialReliableRexmit; - } else { - channel_type = ChannelType::PartialReliableRexmitUnordered; - } - } else { - reliability_parameter = self.max_packet_lifetime as u32; - if self.ordered { - channel_type = ChannelType::PartialReliableTimed; - } else { - channel_type = ChannelType::PartialReliableTimedUnordered; - } - } - - let cfg = data::data_channel::Config { - channel_type, - priority: data::message::message_channel_open::CHANNEL_PRIORITY_NORMAL, - reliability_parameter, - label: self.label.clone(), - protocol: self.protocol.clone(), - negotiated: self.negotiated, - }; - - if !self.negotiated { - self.id.store( - sctp_transport - .generate_and_set_data_channel_id( - sctp_transport.dtls_transport.role().await, - ) - .await?, - Ordering::SeqCst, - ); - } - - let dc = data::data_channel::DataChannel::dial(&association, self.id(), cfg).await?; - - // buffered_amount_low_threshold and on_buffered_amount_low might be set earlier - dc.set_buffered_amount_low_threshold( - self.buffered_amount_low_threshold.load(Ordering::SeqCst), - ); - { - let mut on_buffered_amount_low = self.on_buffered_amount_low.lock().await; - if let Some(f) = on_buffered_amount_low.take() { - dc.on_buffered_amount_low(f); - } - } - - self.handle_open(Arc::new(dc)).await; - - Ok(()) - } else { - Err(Error::ErrSCTPNotEstablished) - } - } - - /// transport returns the SCTPTransport instance the DataChannel is sending over. - pub async fn transport(&self) -> Option> { - let sctp_transport = self.sctp_transport.lock().await; - sctp_transport.clone() - } - - /// on_open sets an event handler which is invoked when - /// the underlying data transport has been established (or re-established). - pub fn on_open(&self, f: OnOpenHdlrFn) { - let _ = self.on_open_handler.lock().replace(f); - - if self.ready_state() == RTCDataChannelState::Open { - self.do_open(); - } - } - - fn do_open(&self) { - let on_open_handler = self.on_open_handler.lock().take(); - if on_open_handler.is_none() { - return; - } - - let detach_data_channels = self.setting_engine.detach.data_channels; - let detach_called = Arc::clone(&self.detach_called); - tokio::spawn(async move { - if let Some(f) = on_open_handler { - f().await; - - // self.check_detach_after_open(); - // After onOpen is complete check that the user called detach - // and provide an error message if the call was missed - if detach_data_channels && !detach_called.load(Ordering::SeqCst) { - log::warn!( - "webrtc.DetachDataChannels() enabled but didn't Detach, call Detach from OnOpen" - ); - } - } - }); - } - - /// on_close sets an event handler which is invoked when - /// the underlying data transport has been closed. - pub fn on_close(&self, f: OnCloseHdlrFn) { - self.on_close_handler.store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_message sets an event handler which is invoked on a binary - /// message arrival over the sctp transport from a remote peer. - /// OnMessage can currently receive messages up to 16384 bytes - /// in size. Check out the detach API if you want to use larger - /// message sizes. Note that browser support for larger messages - /// is also limited. - pub fn on_message(&self, f: OnMessageHdlrFn) { - self.on_message_handler.store(Some(Arc::new(Mutex::new(f)))); - } - - async fn do_message(&self, msg: DataChannelMessage) { - if let Some(handler) = &*self.on_message_handler.load() { - let mut f = handler.lock().await; - f(msg).await; - } - } - - pub(crate) async fn handle_open(&self, dc: Arc) { - { - let mut data_channel = self.data_channel.lock().await; - *data_channel = Some(Arc::clone(&dc)); - } - self.set_ready_state(RTCDataChannelState::Open); - - self.do_open(); - - if !self.setting_engine.detach.data_channels { - let ready_state = Arc::clone(&self.ready_state); - let on_message_handler = Arc::clone(&self.on_message_handler); - let on_close_handler = Arc::clone(&self.on_close_handler); - let on_error_handler = Arc::clone(&self.on_error_handler); - let notify_rx = self.notify_tx.clone(); - tokio::spawn(async move { - RTCDataChannel::read_loop( - notify_rx, - dc, - ready_state, - on_message_handler, - on_close_handler, - on_error_handler, - ) - .await; - }); - } - } - - /// on_error sets an event handler which is invoked when - /// the underlying data transport cannot be read. - pub fn on_error(&self, f: OnErrorHdlrFn) { - self.on_error_handler.store(Some(Arc::new(Mutex::new(f)))); - } - - async fn read_loop( - notify_rx: Arc, - data_channel: Arc, - ready_state: Arc, - on_message_handler: Arc>>, - on_close_handler: Arc>>, - on_error_handler: Arc>>, - ) { - let mut buffer = vec![0u8; DATA_CHANNEL_BUFFER_SIZE as usize]; - loop { - let (n, is_string) = tokio::select! { - _ = notify_rx.notified() => break, - result = data_channel.read_data_channel(&mut buffer) => { - match result{ - // EOF (`data_channel` was either closed or the underlying stream got - // reset by the remote) => close and run `on_close` handler. - Ok((0, _)) => - { - ready_state.store(RTCDataChannelState::Closed as u8, Ordering::SeqCst); - - let on_close_handler2 = Arc::clone(&on_close_handler); - tokio::spawn(async move { - if let Some(handler) = &*on_close_handler2.load() { - let mut f = handler.lock().await; - f().await; - } - }); - - break; - } - Ok((n, is_string)) => (n, is_string), - Err(err) => { - ready_state.store(RTCDataChannelState::Closed as u8, Ordering::SeqCst); - - let on_error_handler2 = Arc::clone(&on_error_handler); - tokio::spawn(async move { - if let Some(handler) = &*on_error_handler2.load() { - let mut f = handler.lock().await; - f(err.into()).await; - } - }); - - let on_close_handler2 = Arc::clone(&on_close_handler); - tokio::spawn(async move { - if let Some(handler) = &*on_close_handler2.load() { - let mut f = handler.lock().await; - f().await; - } - }); - - break; - } - } - } - }; - - if let Some(handler) = &*on_message_handler.load() { - let mut f = handler.lock().await; - f(DataChannelMessage { - is_string, - data: Bytes::from(buffer[..n].to_vec()), - }) - .await; - } - } - } - - /// send sends the binary message to the DataChannel peer - pub async fn send(&self, data: &Bytes) -> Result { - self.ensure_open()?; - - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - Ok(dc.write_data_channel(data, false).await?) - } else { - Err(Error::ErrClosedPipe) - } - } - - /// send_text sends the text message to the DataChannel peer - pub async fn send_text(&self, s: impl Into) -> Result { - self.ensure_open()?; - - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - Ok(dc.write_data_channel(&Bytes::from(s.into()), true).await?) - } else { - Err(Error::ErrClosedPipe) - } - } - - fn ensure_open(&self) -> Result<()> { - if self.ready_state() != RTCDataChannelState::Open { - Err(Error::ErrClosedPipe) - } else { - Ok(()) - } - } - - /// detach allows you to detach the underlying datachannel. This provides - /// an idiomatic API to work with, however it disables the OnMessage callback. - /// Before calling Detach you have to enable this behavior by calling - /// webrtc.DetachDataChannels(). Combining detached and normal data channels - /// is not supported. - /// Please refer to the data-channels-detach example and the - /// pion/datachannel documentation for the correct way to handle the - /// resulting DataChannel object. - pub async fn detach(&self) -> Result> { - if !self.setting_engine.detach.data_channels { - return Err(Error::ErrDetachNotEnabled); - } - - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - self.detach_called.store(true, Ordering::SeqCst); - - Ok(Arc::clone(dc)) - } else { - Err(Error::ErrDetachBeforeOpened) - } - } - - /// Close Closes the DataChannel. It may be called regardless of whether - /// the DataChannel object was created by this peer or the remote peer. - pub async fn close(&self) -> Result<()> { - if self.ready_state() == RTCDataChannelState::Closed { - return Ok(()); - } - - self.set_ready_state(RTCDataChannelState::Closing); - self.notify_tx.notify_waiters(); - - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - Ok(dc.close().await?) - } else { - Ok(()) - } - } - - /// label represents a label that can be used to distinguish this - /// DataChannel object from other DataChannel objects. Scripts are - /// allowed to create multiple DataChannel objects with the same label. - pub fn label(&self) -> &str { - self.label.as_str() - } - - /// Ordered returns true if the DataChannel is ordered, and false if - /// out-of-order delivery is allowed. - pub fn ordered(&self) -> bool { - self.ordered - } - - /// max_packet_lifetime represents the length of the time window (msec) during - /// which transmissions and retransmissions may occur in unreliable mode. - pub fn max_packet_lifetime(&self) -> u16 { - self.max_packet_lifetime - } - - /// max_retransmits represents the maximum number of retransmissions that are - /// attempted in unreliable mode. - pub fn max_retransmits(&self) -> u16 { - self.max_retransmits - } - - /// protocol represents the name of the sub-protocol used with this - /// DataChannel. - pub fn protocol(&self) -> &str { - self.protocol.as_str() - } - - /// negotiated represents whether this DataChannel was negotiated by the - /// application (true), or not (false). - pub fn negotiated(&self) -> bool { - self.negotiated - } - - /// ID represents the ID for this DataChannel. The value is initially - /// null, which is what will be returned if the ID was not provided at - /// channel creation time, and the DTLS role of the SCTP transport has not - /// yet been negotiated. Otherwise, it will return the ID that was either - /// selected by the script or generated. After the ID is set to a non-null - /// value, it will not change. - pub fn id(&self) -> u16 { - self.id.load(Ordering::SeqCst) - } - - /// ready_state represents the state of the DataChannel object. - pub fn ready_state(&self) -> RTCDataChannelState { - self.ready_state.load(Ordering::SeqCst).into() - } - - /// buffered_amount represents the number of bytes of application data - /// (UTF-8 text and binary data) that have been queued using send(). Even - /// though the data transmission can occur in parallel, the returned value - /// MUST NOT be decreased before the current task yielded back to the event - /// loop to prevent race conditions. The value does not include framing - /// overhead incurred by the protocol, or buffering done by the operating - /// system or network hardware. The value of buffered_amount slot will only - /// increase with each call to the send() method as long as the ready_state is - /// open; however, buffered_amount does not reset to zero once the channel - /// closes. - pub async fn buffered_amount(&self) -> usize { - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - dc.buffered_amount() - } else { - 0 - } - } - - /// buffered_amount_low_threshold represents the threshold at which the - /// bufferedAmount is considered to be low. When the bufferedAmount decreases - /// from above this threshold to equal or below it, the bufferedamountlow - /// event fires. buffered_amount_low_threshold is initially zero on each new - /// DataChannel, but the application may change its value at any time. - /// The threshold is set to 0 by default. - pub async fn buffered_amount_low_threshold(&self) -> usize { - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - dc.buffered_amount_low_threshold() - } else { - self.buffered_amount_low_threshold.load(Ordering::SeqCst) - } - } - - /// set_buffered_amount_low_threshold is used to update the threshold. - /// See buffered_amount_low_threshold(). - pub async fn set_buffered_amount_low_threshold(&self, th: usize) { - self.buffered_amount_low_threshold - .store(th, Ordering::SeqCst); - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - dc.set_buffered_amount_low_threshold(th); - } - } - - /// on_buffered_amount_low sets an event handler which is invoked when - /// the number of bytes of outgoing data becomes lower than the - /// buffered_amount_low_threshold. - pub async fn on_buffered_amount_low(&self, f: OnBufferedAmountLowFn) { - let data_channel = self.data_channel.lock().await; - if let Some(dc) = &*data_channel { - dc.on_buffered_amount_low(f); - } else { - let mut on_buffered_amount_low = self.on_buffered_amount_low.lock().await; - *on_buffered_amount_low = Some(f); - } - } - - pub(crate) fn get_stats_id(&self) -> &str { - self.stats_id.as_str() - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - let stats = DataChannelStats::from(self).await; - collector.insert(self.stats_id.clone(), StatsReportType::DataChannel(stats)); - } - - pub(crate) fn set_ready_state(&self, r: RTCDataChannelState) { - self.ready_state.store(r as u8, Ordering::SeqCst); - } -} diff --git a/webrtc/src/dtls_transport/dtls_fingerprint.rs b/webrtc/src/dtls_transport/dtls_fingerprint.rs deleted file mode 100644 index eaea36763..000000000 --- a/webrtc/src/dtls_transport/dtls_fingerprint.rs +++ /dev/null @@ -1,15 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// DTLSFingerprint specifies the hash function algorithm and certificate -/// fingerprint as described in . -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RTCDtlsFingerprint { - /// Algorithm specifies one of the the hash function algorithms defined in - /// the 'Hash function Textual Names' registry. - pub algorithm: String, - - /// Value specifies the value of the certificate fingerprint in lowercase - /// hex string as expressed utilizing the syntax of 'fingerprint' in - /// . - pub value: String, -} diff --git a/webrtc/src/dtls_transport/dtls_parameters.rs b/webrtc/src/dtls_transport/dtls_parameters.rs deleted file mode 100644 index 6aa14477f..000000000 --- a/webrtc/src/dtls_transport/dtls_parameters.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::dtls_fingerprint::*; -use super::dtls_role::*; - -/// DTLSParameters holds information relating to DTLS configuration. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct DTLSParameters { - pub role: DTLSRole, - pub fingerprints: Vec, -} diff --git a/webrtc/src/dtls_transport/dtls_role.rs b/webrtc/src/dtls_transport/dtls_role.rs deleted file mode 100644 index 88730c631..000000000 --- a/webrtc/src/dtls_transport/dtls_role.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::fmt; - -use sdp::description::session::SessionDescription; -use sdp::util::ConnectionRole; -use serde::{Deserialize, Serialize}; - -/// DtlsRole indicates the role of the DTLS transport. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum DTLSRole { - #[default] - Unspecified = 0, - - /// DTLSRoleAuto defines the DTLS role is determined based on - /// the resolved ICE role: the ICE controlled role acts as the DTLS - /// client and the ICE controlling role acts as the DTLS server. - #[serde(rename = "auto")] - Auto = 1, - - /// DTLSRoleClient defines the DTLS client role. - #[serde(rename = "client")] - Client = 2, - - /// DTLSRoleServer defines the DTLS server role. - #[serde(rename = "server")] - Server = 3, -} - -/// -/// The answerer MUST use either a -/// setup attribute value of setup:active or setup:passive. Note that -/// if the answerer uses setup:passive, then the DTLS handshake will -/// not begin until the answerer is received, which adds additional -/// latency. setup:active allows the answer and the DTLS handshake to -/// occur in parallel. Thus, setup:active is RECOMMENDED. -pub(crate) const DEFAULT_DTLS_ROLE_ANSWER: DTLSRole = DTLSRole::Client; - -/// The endpoint that is the offerer MUST use the setup attribute -/// value of setup:actpass and be prepared to receive a client_hello -/// before it receives the answer. -pub(crate) const DEFAULT_DTLS_ROLE_OFFER: DTLSRole = DTLSRole::Auto; - -impl fmt::Display for DTLSRole { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - DTLSRole::Auto => write!(f, "auto"), - DTLSRole::Client => write!(f, "client"), - DTLSRole::Server => write!(f, "server"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -/// Iterate a SessionDescription from a remote to determine if an explicit -/// role can been determined from it. The decision is made from the first role we we parse. -/// If no role can be found we return DTLSRoleAuto -impl From<&SessionDescription> for DTLSRole { - fn from(session_description: &SessionDescription) -> Self { - for media_section in &session_description.media_descriptions { - for attribute in &media_section.attributes { - if attribute.key == "setup" { - if let Some(value) = &attribute.value { - match value.as_str() { - "active" => return DTLSRole::Client, - "passive" => return DTLSRole::Server, - _ => return DTLSRole::Auto, - }; - } else { - return DTLSRole::Auto; - } - } - } - } - - DTLSRole::Auto - } -} - -impl DTLSRole { - pub(crate) fn to_connection_role(self) -> ConnectionRole { - match self { - DTLSRole::Client => ConnectionRole::Active, - DTLSRole::Server => ConnectionRole::Passive, - DTLSRole::Auto => ConnectionRole::Actpass, - _ => ConnectionRole::Unspecified, - } - } -} - -#[cfg(test)] -mod test { - use std::io::Cursor; - - use super::*; - use crate::error::Result; - - #[test] - fn test_dtls_role_string() { - let tests = vec![ - (DTLSRole::Unspecified, "Unspecified"), - (DTLSRole::Auto, "auto"), - (DTLSRole::Client, "client"), - (DTLSRole::Server, "server"), - ]; - - for (role, expected_string) in tests { - assert_eq!(role.to_string(), expected_string) - } - } - - #[test] - fn test_dtls_role_from_remote_sdp() -> Result<()> { - const NO_MEDIA: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -"; - - const MEDIA_NO_SETUP: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=application 47299 DTLS/SCTP 5000 -c=IN IP4 192.168.20.129 -"; - - const MEDIA_SETUP_DECLARED: &str = "v=0 -o=- 4596489990601351948 2 IN IP4 127.0.0.1 -s=- -t=0 0 -m=application 47299 DTLS/SCTP 5000 -c=IN IP4 192.168.20.129 -a=setup:"; - - let tests = vec![ - ("No MediaDescriptions", NO_MEDIA.to_owned(), DTLSRole::Auto), - ( - "MediaDescription, no setup", - MEDIA_NO_SETUP.to_owned(), - DTLSRole::Auto, - ), - ( - "MediaDescription, setup:actpass", - format!("{}{}\n", MEDIA_SETUP_DECLARED, "actpass"), - DTLSRole::Auto, - ), - ( - "MediaDescription, setup:passive", - format!("{}{}\n", MEDIA_SETUP_DECLARED, "passive"), - DTLSRole::Server, - ), - ( - "MediaDescription, setup:active", - format!("{}{}\n", MEDIA_SETUP_DECLARED, "active"), - DTLSRole::Client, - ), - ]; - - for (name, session_description_str, expected_role) in tests { - let mut reader = Cursor::new(session_description_str.as_bytes()); - let session_description = SessionDescription::unmarshal(&mut reader)?; - assert_eq!( - DTLSRole::from(&session_description), - expected_role, - "{name} failed" - ); - } - - Ok(()) - } -} diff --git a/webrtc/src/dtls_transport/dtls_transport_state.rs b/webrtc/src/dtls_transport/dtls_transport_state.rs deleted file mode 100644 index 3700d8b81..000000000 --- a/webrtc/src/dtls_transport/dtls_transport_state.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::fmt; - -/// DTLSTransportState indicates the DTLS transport establishment state. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCDtlsTransportState { - #[default] - Unspecified = 0, - - /// DTLSTransportStateNew indicates that DTLS has not started negotiating - /// yet. - New = 1, - - /// DTLSTransportStateConnecting indicates that DTLS is in the process of - /// negotiating a secure connection and verifying the remote fingerprint. - Connecting = 2, - - /// DTLSTransportStateConnected indicates that DTLS has completed - /// negotiation of a secure connection and verified the remote fingerprint. - Connected = 3, - - /// DTLSTransportStateClosed indicates that the transport has been closed - /// intentionally as the result of receipt of a close_notify alert, or - /// calling close(). - Closed = 4, - - /// DTLSTransportStateFailed indicates that the transport has failed as - /// the result of an error (such as receipt of an error alert or failure to - /// validate the remote fingerprint). - Failed = 5, -} - -const DTLS_TRANSPORT_STATE_NEW_STR: &str = "new"; -const DTLS_TRANSPORT_STATE_CONNECTING_STR: &str = "connecting"; -const DTLS_TRANSPORT_STATE_CONNECTED_STR: &str = "connected"; -const DTLS_TRANSPORT_STATE_CLOSED_STR: &str = "closed"; -const DTLS_TRANSPORT_STATE_FAILED_STR: &str = "failed"; - -impl From<&str> for RTCDtlsTransportState { - fn from(raw: &str) -> Self { - match raw { - DTLS_TRANSPORT_STATE_NEW_STR => RTCDtlsTransportState::New, - DTLS_TRANSPORT_STATE_CONNECTING_STR => RTCDtlsTransportState::Connecting, - DTLS_TRANSPORT_STATE_CONNECTED_STR => RTCDtlsTransportState::Connected, - DTLS_TRANSPORT_STATE_CLOSED_STR => RTCDtlsTransportState::Closed, - DTLS_TRANSPORT_STATE_FAILED_STR => RTCDtlsTransportState::Failed, - _ => RTCDtlsTransportState::Unspecified, - } - } -} - -impl From for RTCDtlsTransportState { - fn from(v: u8) -> Self { - match v { - 1 => RTCDtlsTransportState::New, - 2 => RTCDtlsTransportState::Connecting, - 3 => RTCDtlsTransportState::Connected, - 4 => RTCDtlsTransportState::Closed, - 5 => RTCDtlsTransportState::Failed, - _ => RTCDtlsTransportState::Unspecified, - } - } -} - -impl fmt::Display for RTCDtlsTransportState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCDtlsTransportState::New => DTLS_TRANSPORT_STATE_NEW_STR, - RTCDtlsTransportState::Connecting => DTLS_TRANSPORT_STATE_CONNECTING_STR, - RTCDtlsTransportState::Connected => DTLS_TRANSPORT_STATE_CONNECTED_STR, - RTCDtlsTransportState::Closed => DTLS_TRANSPORT_STATE_CLOSED_STR, - RTCDtlsTransportState::Failed => DTLS_TRANSPORT_STATE_FAILED_STR, - RTCDtlsTransportState::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_dtls_transport_state() { - let tests = vec![ - (crate::UNSPECIFIED_STR, RTCDtlsTransportState::Unspecified), - ("new", RTCDtlsTransportState::New), - ("connecting", RTCDtlsTransportState::Connecting), - ("connected", RTCDtlsTransportState::Connected), - ("closed", RTCDtlsTransportState::Closed), - ("failed", RTCDtlsTransportState::Failed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!( - RTCDtlsTransportState::from(state_string), - expected_state, - "testCase: {expected_state}", - ); - } - } - - #[test] - fn test_dtls_transport_state_string() { - let tests = vec![ - (RTCDtlsTransportState::Unspecified, crate::UNSPECIFIED_STR), - (RTCDtlsTransportState::New, "new"), - (RTCDtlsTransportState::Connecting, "connecting"), - (RTCDtlsTransportState::Connected, "connected"), - (RTCDtlsTransportState::Closed, "closed"), - (RTCDtlsTransportState::Failed, "failed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string) - } - } -} diff --git a/webrtc/src/dtls_transport/dtls_transport_test.rs b/webrtc/src/dtls_transport/dtls_transport_test.rs deleted file mode 100644 index a9e71aca0..000000000 --- a/webrtc/src/dtls_transport/dtls_transport_test.rs +++ /dev/null @@ -1,204 +0,0 @@ -use ice::mdns::MulticastDnsMode; -use ice::network_type::NetworkType; -use regex::Regex; -use tokio::time::Duration; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::media_engine::MediaEngine; -use crate::api::APIBuilder; -use crate::data_channel::RTCDataChannel; -use crate::ice_transport::ice_candidate::RTCIceCandidate; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::peer_connection_state::RTCPeerConnectionState; -use crate::peer_connection::peer_connection_test::{ - close_pair_now, new_pair, signal_pair, until_connection_state, -}; - -//use log::LevelFilter; -//use std::io::Write; - -// An invalid fingerprint MUST cause PeerConnectionState to go to PeerConnectionStateFailed -#[tokio::test] -async fn test_invalid_fingerprint_causes_failed() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_offer, mut pc_answer) = new_pair(&api).await?; - - pc_answer.on_data_channel(Box::new(|_: Arc| { - panic!("A DataChannel must not be created when Fingerprint verification fails"); - })); - - let (offer_chan_tx, mut offer_chan_rx) = mpsc::channel::<()>(1); - - let offer_chan_tx = Arc::new(offer_chan_tx); - pc_offer.on_ice_candidate(Box::new(move |candidate: Option| { - let offer_chan_tx2 = Arc::clone(&offer_chan_tx); - Box::pin(async move { - if candidate.is_none() { - let _ = offer_chan_tx2.send(()).await; - } - }) - })); - - let offer_connection_has_failed = WaitGroup::new(); - until_connection_state( - &mut pc_offer, - &offer_connection_has_failed, - RTCPeerConnectionState::Failed, - ) - .await; - let answer_connection_has_failed = WaitGroup::new(); - until_connection_state( - &mut pc_answer, - &answer_connection_has_failed, - RTCPeerConnectionState::Failed, - ) - .await; - - let _ = pc_offer - .create_data_channel("unusedDataChannel", None) - .await?; - - let offer = pc_offer.create_offer(None).await?; - pc_offer.set_local_description(offer).await?; - - let timeout = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(timeout); - - tokio::select! { - _ = offer_chan_rx.recv() =>{ - let mut offer = pc_offer.pending_local_description().await.unwrap(); - - log::trace!("receiving pending local desc: {:?}", offer); - - // Replace with invalid fingerprint - let re = Regex::new(r"sha-256 (.*?)\r").unwrap(); - offer.sdp = re.replace_all(offer.sdp.as_str(), "sha-256 AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA\r").to_string(); - - pc_answer.set_remote_description(offer).await?; - - let mut answer = pc_answer.create_answer(None).await?; - - pc_answer.set_local_description(answer.clone()).await?; - - answer.sdp = re.replace_all(answer.sdp.as_str(), "sha-256 AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA\r").to_string(); - - pc_offer.set_remote_description(answer).await?; - } - _ = timeout.as_mut() =>{ - panic!("timed out waiting to receive offer"); - } - } - - log::trace!("offer_connection_has_failed wait begin"); - - offer_connection_has_failed.wait().await; - answer_connection_has_failed.wait().await; - - log::trace!("offer_connection_has_failed wait end"); - { - let transport = pc_offer.sctp().transport(); - assert_eq!(transport.state(), RTCDtlsTransportState::Failed); - assert!(transport.conn().await.is_none()); - } - - { - let transport = pc_answer.sctp().transport(); - assert_eq!(transport.state(), RTCDtlsTransportState::Failed); - assert!(transport.conn().await.is_none()); - } - - close_pair_now(&pc_offer, &pc_answer).await; - - Ok(()) -} - -async fn run_test(r: DTLSRole) -> Result<()> { - let mut offer_s = SettingEngine::default(); - offer_s.set_answering_dtls_role(r)?; - offer_s.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled); - offer_s.set_network_types(vec![NetworkType::Udp4]); - let mut offer_pc = APIBuilder::new() - .with_setting_engine(offer_s) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - let mut answer_s = SettingEngine::default(); - answer_s.set_answering_dtls_role(r)?; - answer_s.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled); - answer_s.set_network_types(vec![NetworkType::Udp4]); - let mut answer_pc = APIBuilder::new() - .with_setting_engine(answer_s) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - signal_pair(&mut offer_pc, &mut answer_pc).await?; - - let wg = WaitGroup::new(); - until_connection_state(&mut answer_pc, &wg, RTCPeerConnectionState::Connected).await; - wg.wait().await; - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} - -#[tokio::test] -async fn test_peer_connection_dtls_role_setting_engine_server() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - run_test(DTLSRole::Server).await -} - -#[tokio::test] -async fn test_peer_connection_dtls_role_setting_engine_client() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - run_test(DTLSRole::Client).await -} diff --git a/webrtc/src/dtls_transport/mod.rs b/webrtc/src/dtls_transport/mod.rs deleted file mode 100644 index 7e07f6316..000000000 --- a/webrtc/src/dtls_transport/mod.rs +++ /dev/null @@ -1,617 +0,0 @@ -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use arc_swap::ArcSwapOption; -use bytes::Bytes; -use dtls::config::ClientAuthType; -use dtls::conn::DTLSConn; -use dtls::extension::extension_use_srtp::SrtpProtectionProfile; -use dtls_role::*; -use interceptor::stream_info::StreamInfo; -use interceptor::{Interceptor, RTCPReader, RTPReader}; -use portable_atomic::{AtomicBool, AtomicU8}; -use sha2::{Digest, Sha256}; -use srtp::protection_profile::ProtectionProfile; -use srtp::session::Session; -use srtp::stream::Stream; -use tokio::sync::{mpsc, Mutex}; -use util::Conn; - -use crate::api::setting_engine::SettingEngine; -use crate::dtls_transport::dtls_parameters::DTLSParameters; -use crate::dtls_transport::dtls_transport_state::RTCDtlsTransportState; -use crate::error::{flatten_errs, Error, Result}; -use crate::ice_transport::ice_role::RTCIceRole; -use crate::ice_transport::ice_transport_state::RTCIceTransportState; -use crate::ice_transport::RTCIceTransport; -use crate::mux::endpoint::Endpoint; -use crate::mux::mux_func::{match_dtls, match_srtcp, match_srtp, MatchFunc}; -use crate::peer_connection::certificate::RTCCertificate; -use crate::rtp_transceiver::SSRC; -use crate::stats::stats_collector::StatsCollector; - -#[cfg(test)] -mod dtls_transport_test; - -pub mod dtls_fingerprint; -pub mod dtls_parameters; -pub mod dtls_role; -pub mod dtls_transport_state; - -pub(crate) fn default_srtp_protection_profiles() -> Vec { - vec![ - SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, - SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, - ] -} - -pub type OnDTLSTransportStateChangeHdlrFn = Box< - dyn (FnMut(RTCDtlsTransportState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -/// DTLSTransport allows an application access to information about the DTLS -/// transport over which RTP and RTCP packets are sent and received by -/// RTPSender and RTPReceiver, as well other data such as SCTP packets sent -/// and received by data channels. -#[derive(Default)] -pub struct RTCDtlsTransport { - pub(crate) ice_transport: Arc, - pub(crate) certificates: Vec, - pub(crate) setting_engine: Arc, - - pub(crate) remote_parameters: Mutex, - pub(crate) remote_certificate: Mutex, - pub(crate) state: AtomicU8, //DTLSTransportState, - pub(crate) srtp_protection_profile: Mutex, - pub(crate) on_state_change_handler: ArcSwapOption>, - pub(crate) conn: Mutex>>, - - pub(crate) srtp_session: Mutex>>, - pub(crate) srtcp_session: Mutex>>, - pub(crate) srtp_endpoint: Mutex>>, - pub(crate) srtcp_endpoint: Mutex>>, - - pub(crate) simulcast_streams: Mutex>>, - - pub(crate) srtp_ready_signal: Arc, - pub(crate) srtp_ready_tx: Mutex>>, - pub(crate) srtp_ready_rx: Mutex>>, - - pub(crate) dtls_matcher: Option, -} - -impl RTCDtlsTransport { - pub(crate) fn new( - ice_transport: Arc, - certificates: Vec, - setting_engine: Arc, - ) -> Self { - let (srtp_ready_tx, srtp_ready_rx) = mpsc::channel(1); - RTCDtlsTransport { - ice_transport, - certificates, - setting_engine, - srtp_ready_signal: Arc::new(AtomicBool::new(false)), - srtp_ready_tx: Mutex::new(Some(srtp_ready_tx)), - srtp_ready_rx: Mutex::new(Some(srtp_ready_rx)), - state: AtomicU8::new(RTCDtlsTransportState::New as u8), - dtls_matcher: Some(Box::new(match_dtls)), - ..Default::default() - } - } - - pub(crate) async fn conn(&self) -> Option> { - let conn = self.conn.lock().await; - conn.clone() - } - - /// returns the currently-configured ICETransport or None - /// if one has not been configured - pub fn ice_transport(&self) -> &RTCIceTransport { - &self.ice_transport - } - - /// state_change requires the caller holds the lock - async fn state_change(&self, state: RTCDtlsTransportState) { - self.state.store(state as u8, Ordering::SeqCst); - if let Some(handler) = &*self.on_state_change_handler.load() { - let mut f = handler.lock().await; - f(state).await; - } - } - - /// on_state_change sets a handler that is fired when the DTLS - /// connection state changes. - pub fn on_state_change(&self, f: OnDTLSTransportStateChangeHdlrFn) { - self.on_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// state returns the current dtls_transport transport state. - pub fn state(&self) -> RTCDtlsTransportState { - self.state.load(Ordering::SeqCst).into() - } - - /// write_rtcp sends a user provided RTCP packet to the connected peer. If no peer is connected the - /// packet is discarded. - pub async fn write_rtcp( - &self, - pkts: &[Box], - ) -> Result { - let srtcp_session = self.srtcp_session.lock().await; - if let Some(srtcp_session) = &*srtcp_session { - let raw = rtcp::packet::marshal(pkts)?; - Ok(srtcp_session.write(&raw, false).await?) - } else { - Ok(0) - } - } - - /// get_local_parameters returns the DTLS parameters of the local DTLSTransport upon construction. - pub fn get_local_parameters(&self) -> Result { - let mut fingerprints = vec![]; - - for c in &self.certificates { - fingerprints.extend(c.get_fingerprints()); - } - - Ok(DTLSParameters { - role: DTLSRole::Auto, // always returns the default role - fingerprints, - }) - } - - /// get_remote_certificate returns the certificate chain in use by the remote side - /// returns an empty list prior to selection of the remote certificate - pub async fn get_remote_certificate(&self) -> Bytes { - let remote_certificate = self.remote_certificate.lock().await; - remote_certificate.clone() - } - - pub(crate) async fn start_srtp(&self) -> Result<()> { - let profile = { - let srtp_protection_profile = self.srtp_protection_profile.lock().await; - *srtp_protection_profile - }; - - let mut srtp_config = srtp::config::Config { - profile, - ..Default::default() - }; - - if self.setting_engine.replay_protection.srtp != 0 { - srtp_config.remote_rtp_options = Some(srtp::option::srtp_replay_protection( - self.setting_engine.replay_protection.srtp, - )); - } else if self.setting_engine.disable_srtp_replay_protection { - srtp_config.remote_rtp_options = Some(srtp::option::srtp_no_replay_protection()); - } - - if let Some(conn) = self.conn().await { - let conn_state = conn.connection_state().await; - srtp_config - .extract_session_keys_from_dtls(conn_state, self.role().await == DTLSRole::Client) - .await?; - } else { - return Err(Error::ErrDtlsTransportNotStarted); - } - - { - let mut srtp_session = self.srtp_session.lock().await; - *srtp_session = { - let se = self.srtp_endpoint.lock().await; - if let Some(srtp_endpoint) = &*se { - Some(Arc::new( - Session::new( - Arc::clone(srtp_endpoint) as Arc, - srtp_config, - true, - ) - .await?, - )) - } else { - None - } - }; - } - - let mut srtcp_config = srtp::config::Config { - profile, - ..Default::default() - }; - if self.setting_engine.replay_protection.srtcp != 0 { - srtcp_config.remote_rtcp_options = Some(srtp::option::srtcp_replay_protection( - self.setting_engine.replay_protection.srtcp, - )); - } else if self.setting_engine.disable_srtcp_replay_protection { - srtcp_config.remote_rtcp_options = Some(srtp::option::srtcp_no_replay_protection()); - } - - if let Some(conn) = self.conn().await { - let conn_state = conn.connection_state().await; - srtcp_config - .extract_session_keys_from_dtls(conn_state, self.role().await == DTLSRole::Client) - .await?; - } else { - return Err(Error::ErrDtlsTransportNotStarted); - } - - { - let mut srtcp_session = self.srtcp_session.lock().await; - *srtcp_session = { - let se = self.srtcp_endpoint.lock().await; - if let Some(srtcp_endpoint) = &*se { - Some(Arc::new( - Session::new( - Arc::clone(srtcp_endpoint) as Arc, - srtcp_config, - false, - ) - .await?, - )) - } else { - None - } - }; - } - - { - let mut srtp_ready_tx = self.srtp_ready_tx.lock().await; - srtp_ready_tx.take(); - if srtp_ready_tx.is_none() { - self.srtp_ready_signal.store(true, Ordering::SeqCst); - } - } - - Ok(()) - } - - pub(crate) async fn get_srtp_session(&self) -> Option> { - let srtp_session = self.srtp_session.lock().await; - srtp_session.clone() - } - - pub(crate) async fn get_srtcp_session(&self) -> Option> { - let srtcp_session = self.srtcp_session.lock().await; - srtcp_session.clone() - } - - pub(crate) async fn role(&self) -> DTLSRole { - // If remote has an explicit role use the inverse - { - let remote_parameters = self.remote_parameters.lock().await; - match remote_parameters.role { - DTLSRole::Client => return DTLSRole::Server, - DTLSRole::Server => return DTLSRole::Client, - _ => {} - }; - } - - // If SettingEngine has an explicit role - match self.setting_engine.answering_dtls_role { - DTLSRole::Server => return DTLSRole::Server, - DTLSRole::Client => return DTLSRole::Client, - _ => {} - }; - - // Remote was auto and no explicit role was configured via SettingEngine - if self.ice_transport.role().await == RTCIceRole::Controlling { - return DTLSRole::Server; - } - - DEFAULT_DTLS_ROLE_ANSWER - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - for cert in &self.certificates { - cert.collect_stats(collector).await; - } - } - - async fn prepare_transport( - &self, - remote_parameters: DTLSParameters, - ) -> Result<(DTLSRole, dtls::config::Config)> { - self.ensure_ice_conn()?; - - if self.state() != RTCDtlsTransportState::New { - return Err(Error::ErrInvalidDTLSStart); - } - - { - let mut srtp_endpoint = self.srtp_endpoint.lock().await; - *srtp_endpoint = self.ice_transport.new_endpoint(Box::new(match_srtp)).await; - } - { - let mut srtcp_endpoint = self.srtcp_endpoint.lock().await; - *srtcp_endpoint = self.ice_transport.new_endpoint(Box::new(match_srtcp)).await; - } - { - let mut rp = self.remote_parameters.lock().await; - *rp = remote_parameters; - } - - let certificate = if let Some(cert) = self.certificates.first() { - cert.dtls_certificate.clone() - } else { - return Err(Error::ErrNonCertificate); - }; - self.state_change(RTCDtlsTransportState::Connecting).await; - - Ok(( - self.role().await, - dtls::config::Config { - certificates: vec![certificate], - srtp_protection_profiles: if !self - .setting_engine - .srtp_protection_profiles - .is_empty() - { - self.setting_engine.srtp_protection_profiles.clone() - } else { - default_srtp_protection_profiles() - }, - client_auth: ClientAuthType::RequireAnyClientCert, - insecure_skip_verify: true, - insecure_verification: self.setting_engine.allow_insecure_verification_algorithm, - ..Default::default() - }, - )) - } - - /// start DTLS transport negotiation with the parameters of the remote DTLS transport - pub async fn start(&self, remote_parameters: DTLSParameters) -> Result<()> { - let dtls_conn_result = if let Some(dtls_endpoint) = - self.ice_transport.new_endpoint(Box::new(match_dtls)).await - { - let (role, mut dtls_config) = self.prepare_transport(remote_parameters).await?; - if self.setting_engine.replay_protection.dtls != 0 { - dtls_config.replay_protection_window = self.setting_engine.replay_protection.dtls; - } - - // Connect as DTLS Client/Server, function is blocking and we - // must not hold the DTLSTransport lock - if role == DTLSRole::Client { - dtls::conn::DTLSConn::new( - dtls_endpoint as Arc, - dtls_config, - true, - None, - ) - .await - } else { - dtls::conn::DTLSConn::new( - dtls_endpoint as Arc, - dtls_config, - false, - None, - ) - .await - } - } else { - Err(dtls::Error::Other( - "ice_transport.new_endpoint failed".to_owned(), - )) - }; - - let dtls_conn = match dtls_conn_result { - Ok(dtls_conn) => dtls_conn, - Err(err) => { - self.state_change(RTCDtlsTransportState::Failed).await; - return Err(err.into()); - } - }; - - let srtp_profile = dtls_conn.selected_srtpprotection_profile(); - { - let mut srtp_protection_profile = self.srtp_protection_profile.lock().await; - *srtp_protection_profile = match srtp_profile { - dtls::extension::extension_use_srtp::SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm => { - srtp::protection_profile::ProtectionProfile::AeadAes128Gcm - } - dtls::extension::extension_use_srtp::SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80 => { - srtp::protection_profile::ProtectionProfile::Aes128CmHmacSha1_80 - } - _ => { - if let Err(err) = dtls_conn.close().await { - log::error!("{}", err); - } - - self.state_change(RTCDtlsTransportState::Failed).await; - return Err(Error::ErrNoSRTPProtectionProfile); - } - }; - } - - // Check the fingerprint if a certificate was exchanged - let remote_certs = &dtls_conn.connection_state().await.peer_certificates; - if remote_certs.is_empty() { - if let Err(err) = dtls_conn.close().await { - log::error!("{}", err); - } - - self.state_change(RTCDtlsTransportState::Failed).await; - return Err(Error::ErrNoRemoteCertificate); - } - - { - let mut remote_certificate = self.remote_certificate.lock().await; - *remote_certificate = Bytes::from(remote_certs[0].clone()); - } - - if !self - .setting_engine - .disable_certificate_fingerprint_verification - { - if let Err(err) = self.validate_fingerprint(&remote_certs[0]).await { - if let Err(close_err) = dtls_conn.close().await { - log::error!("{}", close_err); - } - - self.state_change(RTCDtlsTransportState::Failed).await; - return Err(err); - } - } - - { - let mut conn = self.conn.lock().await; - *conn = Some(Arc::new(dtls_conn)); - } - self.state_change(RTCDtlsTransportState::Connected).await; - - self.start_srtp().await - } - - /// stops and closes the DTLSTransport object. - pub async fn stop(&self) -> Result<()> { - // Try closing everything and collect the errors - let mut close_errs: Vec = vec![]; - { - let srtp_session = { - let mut srtp_session = self.srtp_session.lock().await; - srtp_session.take() - }; - if let Some(srtp_session) = srtp_session { - match srtp_session.close().await { - Ok(_) => {} - Err(err) => { - close_errs.push(err.into()); - } - }; - } - } - - { - let srtcp_session = { - let mut srtcp_session = self.srtcp_session.lock().await; - srtcp_session.take() - }; - if let Some(srtcp_session) = srtcp_session { - match srtcp_session.close().await { - Ok(_) => {} - Err(err) => { - close_errs.push(err.into()); - } - }; - } - } - - { - let simulcast_streams: Vec> = { - let mut simulcast_streams = self.simulcast_streams.lock().await; - simulcast_streams.drain().map(|(_, v)| v).collect() - }; - for ss in simulcast_streams { - match ss.close().await { - Ok(_) => {} - Err(err) => { - close_errs.push(Error::new(format!( - "simulcast_streams ssrc={}: {}", - ss.get_ssrc(), - err - ))); - } - }; - } - } - - if let Some(conn) = self.conn().await { - // dtls_transport connection may be closed on sctp close. - match conn.close().await { - Ok(_) => {} - Err(err) => { - if err.to_string() != dtls::Error::ErrConnClosed.to_string() { - close_errs.push(err.into()); - } - } - } - } - - self.state_change(RTCDtlsTransportState::Closed).await; - - flatten_errs(close_errs) - } - - pub(crate) async fn validate_fingerprint(&self, remote_cert: &[u8]) -> Result<()> { - let remote_parameters = self.remote_parameters.lock().await; - for fp in &remote_parameters.fingerprints { - if fp.algorithm != "sha-256" { - return Err(Error::ErrUnsupportedFingerprintAlgorithm); - } - - let mut h = Sha256::new(); - h.update(remote_cert); - let hashed = h.finalize(); - let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); - let remote_value = values.join(":").to_lowercase(); - - if remote_value == fp.value.to_lowercase() { - return Ok(()); - } - } - - Err(Error::ErrNoMatchingCertificateFingerprint) - } - - pub(crate) fn ensure_ice_conn(&self) -> Result<()> { - if self.ice_transport.state() == RTCIceTransportState::New { - Err(Error::ErrICEConnectionNotStarted) - } else { - Ok(()) - } - } - - pub(crate) async fn store_simulcast_stream(&self, ssrc: SSRC, stream: Arc) { - let mut simulcast_streams = self.simulcast_streams.lock().await; - simulcast_streams.insert(ssrc, stream); - } - - pub(crate) async fn remove_simulcast_stream(&self, ssrc: SSRC) { - let mut simulcast_streams = self.simulcast_streams.lock().await; - simulcast_streams.remove(&ssrc); - } - - pub(crate) async fn streams_for_ssrc( - &self, - ssrc: SSRC, - stream_info: &StreamInfo, - interceptor: &Arc, - ) -> Result<( - Arc, - Arc, - Arc, - Arc, - )> { - let srtp_session = self - .get_srtp_session() - .await - .ok_or(Error::ErrDtlsTransportNotStarted)?; - //log::debug!("streams_for_ssrc: srtp_session.listen ssrc={}", ssrc); - let rtp_read_stream = srtp_session.open(ssrc).await; - let rtp_stream_reader = Arc::clone(&rtp_read_stream) as Arc; - let rtp_interceptor = interceptor - .bind_remote_stream(stream_info, rtp_stream_reader) - .await; - - let srtcp_session = self - .get_srtcp_session() - .await - .ok_or(Error::ErrDtlsTransportNotStarted)?; - //log::debug!("streams_for_ssrc: srtcp_session.listen ssrc={}", ssrc); - let rtcp_read_stream = srtcp_session.open(ssrc).await; - let rtcp_stream_reader = Arc::clone(&rtcp_read_stream) as Arc; - let rtcp_interceptor = interceptor.bind_rtcp_reader(rtcp_stream_reader).await; - - Ok(( - rtp_read_stream, - rtp_interceptor, - rtcp_read_stream, - rtcp_interceptor, - )) - } -} diff --git a/webrtc/src/error.rs b/webrtc/src/error.rs deleted file mode 100644 index 9a9ff4403..000000000 --- a/webrtc/src/error.rs +++ /dev/null @@ -1,482 +0,0 @@ -use std::future::Future; -use std::num::ParseIntError; -use std::pin::Pin; -use std::string::FromUtf8Error; - -use thiserror::Error; -use tokio::sync::mpsc::error::SendError as MpscSendError; - -use crate::peer_connection::sdp::sdp_type::RTCSdpType; -use crate::peer_connection::signaling_state::RTCSignalingState; -use crate::rtp_transceiver::rtp_receiver; -#[cfg(doc)] -use crate::rtp_transceiver::rtp_sender; - -pub type Result = std::result::Result; - -#[derive(Error, Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - /// ErrUnknownType indicates an error with Unknown info. - #[error("unknown")] - ErrUnknownType, - - /// ErrConnectionClosed indicates an operation executed after connection - /// has already been closed. - #[error("connection closed")] - ErrConnectionClosed, - - /// ErrDataChannelNotOpen indicates an operation executed when the data - /// channel is not (yet) open. - #[error("data channel not open")] - ErrDataChannelNotOpen, - - /// ErrCertificateExpired indicates that an x509 certificate has expired. - #[error("x509Cert expired")] - ErrCertificateExpired, - - /// ErrNoTurnCredentials indicates that a TURN server URL was provided - /// without required credentials. - #[error("turn server credentials required")] - ErrNoTurnCredentials, - - /// ErrTurnCredentials indicates that provided TURN credentials are partial - /// or malformed. - #[error("invalid turn server credentials")] - ErrTurnCredentials, - - /// ErrExistingTrack indicates that a track already exists. - #[error("track already exists")] - ErrExistingTrack, - - /// ErrPrivateKeyType indicates that a particular private key encryption - /// chosen to generate a certificate is not supported. - #[error("private key type not supported")] - ErrPrivateKeyType, - - /// ErrModifyingPeerIdentity indicates that an attempt to modify - /// PeerIdentity was made after PeerConnection has been initialized. - #[error("peerIdentity cannot be modified")] - ErrModifyingPeerIdentity, - - /// ErrModifyingCertificates indicates that an attempt to modify - /// Certificates was made after PeerConnection has been initialized. - #[error("certificates cannot be modified")] - ErrModifyingCertificates, - - /// ErrNonCertificate indicates that there is no certificate - #[error("no certificate")] - ErrNonCertificate, - - /// ErrModifyingBundlePolicy indicates that an attempt to modify - /// BundlePolicy was made after PeerConnection has been initialized. - #[error("bundle policy cannot be modified")] - ErrModifyingBundlePolicy, - - /// ErrModifyingRTCPMuxPolicy indicates that an attempt to modify - /// RTCPMuxPolicy was made after PeerConnection has been initialized. - #[error("rtcp mux policy cannot be modified")] - ErrModifyingRTCPMuxPolicy, - - /// ErrModifyingICECandidatePoolSize indicates that an attempt to modify - /// ICECandidatePoolSize was made after PeerConnection has been initialized. - #[error("ice candidate pool size cannot be modified")] - ErrModifyingICECandidatePoolSize, - - /// ErrStringSizeLimit indicates that the character size limit of string is - /// exceeded. The limit is hardcoded to 65535 according to specifications. - #[error("data channel label exceeds size limit")] - ErrStringSizeLimit, - - /// ErrMaxDataChannelID indicates that the maximum number ID that could be - /// specified for a data channel has been exceeded. - #[error("maximum number ID for datachannel specified")] - ErrMaxDataChannelID, - - /// ErrNegotiatedWithoutID indicates that an attempt to create a data channel - /// was made while setting the negotiated option to true without providing - /// the negotiated channel ID. - #[error("negotiated set without channel id")] - ErrNegotiatedWithoutID, - - /// ErrRetransmitsOrPacketLifeTime indicates that an attempt to create a data - /// channel was made with both options max_packet_life_time and max_retransmits - /// set together. Such configuration is not supported by the specification - /// and is mutually exclusive. - #[error("both max_packet_life_time and max_retransmits was set")] - ErrRetransmitsOrPacketLifeTime, - - /// ErrCodecNotFound is returned when a codec search to the Media Engine fails - #[error("codec not found")] - ErrCodecNotFound, - - /// ErrNoRemoteDescription indicates that an operation was rejected because - /// the remote description is not set - #[error("remote description is not set")] - ErrNoRemoteDescription, - - /// ErrIncorrectSDPSemantics indicates that the PeerConnection was configured to - /// generate SDP Answers with different SDP Semantics than the received Offer - #[error("offer SDP semantics does not match configuration")] - ErrIncorrectSDPSemantics, - - /// ErrIncorrectSignalingState indicates that the signaling state of PeerConnection is not correct - #[error("operation can not be run in current signaling state")] - ErrIncorrectSignalingState, - - /// ErrProtocolTooLarge indicates that value given for a DataChannelInit protocol is - /// longer then 65535 bytes - #[error("protocol is larger then 65535 bytes")] - ErrProtocolTooLarge, - - /// ErrSenderNotCreatedByConnection indicates remove_track was called with a - /// [`rtp_sender::RTCRtpSender`] not created by this PeerConnection - #[error("RtpSender not created by this PeerConnection")] - ErrSenderNotCreatedByConnection, - - /// ErrSenderInitialTrackIdAlreadySet indicates a second call to - /// `RTCRtpSender::set_initial_track_id` which is not allowed. Purely internal error, should not happen in practice. - #[error("RtpSender's initial_track_id has already been set")] - ErrSenderInitialTrackIdAlreadySet, - - /// ErrSessionDescriptionNoFingerprint indicates set_remote_description was called with a SessionDescription that has no - /// fingerprint - #[error("set_remote_description called with no fingerprint")] - ErrSessionDescriptionNoFingerprint, - - /// ErrSessionDescriptionInvalidFingerprint indicates set_remote_description was called with a SessionDescription that - /// has an invalid fingerprint - #[error("set_remote_description called with an invalid fingerprint")] - ErrSessionDescriptionInvalidFingerprint, - - /// ErrSessionDescriptionConflictingFingerprints indicates set_remote_description was called with a SessionDescription that - /// has an conflicting fingerprints - #[error("set_remote_description called with multiple conflicting fingerprint")] - ErrSessionDescriptionConflictingFingerprints, - - /// ErrSessionDescriptionMissingIceUfrag indicates set_remote_description was called with a SessionDescription that - /// is missing an ice-ufrag value - #[error("set_remote_description called with no ice-ufrag")] - ErrSessionDescriptionMissingIceUfrag, - - /// ErrSessionDescriptionMissingIcePwd indicates set_remote_description was called with a SessionDescription that - /// is missing an ice-pwd value - #[error("set_remote_description called with no ice-pwd")] - ErrSessionDescriptionMissingIcePwd, - - /// ErrSessionDescriptionConflictingIceUfrag indicates set_remote_description was called with a SessionDescription that - /// contains multiple conflicting ice-ufrag values - #[error("set_remote_description called with multiple conflicting ice-ufrag values")] - ErrSessionDescriptionConflictingIceUfrag, - - /// ErrSessionDescriptionConflictingIcePwd indicates set_remote_description was called with a SessionDescription that - /// contains multiple conflicting ice-pwd values - #[error("set_remote_description called with multiple conflicting ice-pwd values")] - ErrSessionDescriptionConflictingIcePwd, - - /// ErrNoSRTPProtectionProfile indicates that the DTLS handshake completed and no SRTP Protection Profile was chosen - #[error("DTLS Handshake completed and no SRTP Protection Profile was chosen")] - ErrNoSRTPProtectionProfile, - - /// ErrFailedToGenerateCertificateFingerprint indicates that we failed to generate the fingerprint used for comparing certificates - #[error("failed to generate certificate fingerprint")] - ErrFailedToGenerateCertificateFingerprint, - - /// ErrNoCodecsAvailable indicates that operation isn't possible because the MediaEngine has no codecs available - #[error("operation failed no codecs are available")] - ErrNoCodecsAvailable, - - /// ErrUnsupportedCodec indicates the remote peer doesn't support the requested codec - #[error("unable to start track, codec is not supported by remote")] - ErrUnsupportedCodec, - - /// ErrSenderWithNoCodecs indicates that a RTPSender was created without any codecs. To send media the MediaEngine needs at - /// least one configured codec. - #[error("unable to populate media section, RTPSender created with no codecs")] - ErrSenderWithNoCodecs, - - /// ErrRTPSenderNewTrackHasIncorrectKind indicates that the new track is of a different kind than the previous/original - #[error("new track must be of the same kind as previous")] - ErrRTPSenderNewTrackHasIncorrectKind, - - /// ErrRTPSenderNewTrackHasIncorrectEnvelope indicates that the new track has a different envelope than the previous/original - #[error("new track must have the same envelope as previous")] - ErrRTPSenderNewTrackHasIncorrectEnvelope, - - /// ErrRTPSenderDataSent indicates that the sequence number transformer tries to be enabled after the data sending began - #[error("Sequence number transformer must be enabled before sending data")] - ErrRTPSenderDataSent, - - /// ErrRTPSenderSeqTransEnabled indicates that the sequence number transformer has been already enabled - #[error("Sequence number transformer has been already enabled")] - ErrRTPSenderSeqTransEnabled, - - /// ErrUnbindFailed indicates that a TrackLocal was not able to be unbind - #[error("failed to unbind TrackLocal from PeerConnection")] - ErrUnbindFailed, - - /// ErrNoPayloaderForCodec indicates that the requested codec does not have a payloader - #[error("the requested codec does not have a payloader")] - ErrNoPayloaderForCodec, - - /// ErrRegisterHeaderExtensionInvalidDirection indicates that a extension was registered with different - /// directions for two different calls. - #[error("a header extension must be registered with the same direction each time")] - ErrRegisterHeaderExtensionInvalidDirection, - - /// ErrRegisterHeaderExtensionNoFreeID indicates that there was no extension ID available which - /// in turn means that all 15 available id(1 through 14) have been used. - #[error("no header extension ID was free to use(this means the maximum of 15 extensions have been registered)")] - ErrRegisterHeaderExtensionNoFreeID, - - /// ErrSimulcastProbeOverflow indicates that too many Simulcast probe streams are in flight and the requested SSRC was ignored - #[error("simulcast probe limit has been reached, new SSRC has been discarded")] - ErrSimulcastProbeOverflow, - - #[error("enable detaching by calling webrtc.DetachDataChannels()")] - ErrDetachNotEnabled, - #[error("datachannel not opened yet, try calling Detach from OnOpen")] - ErrDetachBeforeOpened, - #[error("the DTLS transport has not started yet")] - ErrDtlsTransportNotStarted, - #[error("failed extracting keys from DTLS for SRTP")] - ErrDtlsKeyExtractionFailed, - #[error("failed to start SRTP")] - ErrFailedToStartSRTP, - #[error("failed to start SRTCP")] - ErrFailedToStartSRTCP, - #[error("attempted to start DTLSTransport that is not in new state")] - ErrInvalidDTLSStart, - #[error("peer didn't provide certificate via DTLS")] - ErrNoRemoteCertificate, - #[error("identity provider is not implemented")] - ErrIdentityProviderNotImplemented, - #[error("remote certificate does not match any fingerprint")] - ErrNoMatchingCertificateFingerprint, - #[error("unsupported fingerprint algorithm")] - ErrUnsupportedFingerprintAlgorithm, - #[error("ICE connection not started")] - ErrICEConnectionNotStarted, - #[error("unknown candidate type")] - ErrICECandidateTypeUnknown, - #[error("cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type")] - ErrICEInvalidConvertCandidateType, - #[error("ICEAgent does not exist")] - ErrICEAgentNotExist, - #[error("unable to convert ICE candidates to ICECandidates")] - ErrICECandidatesConversionFailed, - #[error("unknown ICE Role")] - ErrICERoleUnknown, - #[error("unknown protocol")] - ErrICEProtocolUnknown, - #[error("gatherer not started")] - ErrICEGathererNotStarted, - #[error("unknown network type")] - ErrNetworkTypeUnknown, - #[error("new sdp does not match previous offer")] - ErrSDPDoesNotMatchOffer, - #[error("new sdp does not match previous answer")] - ErrSDPDoesNotMatchAnswer, - #[error("provided value is not a valid enum value of type SDPType")] - ErrPeerConnSDPTypeInvalidValue, - #[error("invalid state change op")] - ErrPeerConnStateChangeInvalid, - #[error("unhandled state change op")] - ErrPeerConnStateChangeUnhandled, - #[error("invalid SDP type supplied to SetLocalDescription()")] - ErrPeerConnSDPTypeInvalidValueSetLocalDescription, - #[error("remoteDescription contained media section without mid value")] - ErrPeerConnRemoteDescriptionWithoutMidValue, - #[error("remoteDescription has not been set yet")] - ErrPeerConnRemoteDescriptionNil, - #[error("single media section has an explicit SSRC")] - ErrPeerConnSingleMediaSectionHasExplicitSSRC, - #[error("could not add transceiver for remote SSRC")] - ErrPeerConnRemoteSSRCAddTransceiver, - #[error("mid RTP Extensions required for Simulcast")] - ErrPeerConnSimulcastMidRTPExtensionRequired, - #[error("stream id RTP Extensions required for Simulcast")] - ErrPeerConnSimulcastStreamIDRTPExtensionRequired, - #[error("incoming SSRC failed Simulcast probing")] - ErrPeerConnSimulcastIncomingSSRCFailed, - #[error("failed collecting stats")] - ErrPeerConnStatsCollectionFailed, - #[error("add_transceiver_from_kind only accepts one RTPTransceiverInit")] - ErrPeerConnAddTransceiverFromKindOnlyAcceptsOne, - #[error("add_transceiver_from_track only accepts one RTPTransceiverInit")] - ErrPeerConnAddTransceiverFromTrackOnlyAcceptsOne, - #[error("add_transceiver_from_kind currently only supports recvonly")] - ErrPeerConnAddTransceiverFromKindSupport, - #[error("add_transceiver_from_track currently only supports sendonly and sendrecv")] - ErrPeerConnAddTransceiverFromTrackSupport, - #[error("TODO set_identity_provider")] - ErrPeerConnSetIdentityProviderNotImplemented, - #[error("write_rtcp failed to open write_stream")] - ErrPeerConnWriteRTCPOpenWriteStream, - #[error("cannot find transceiver with mid")] - ErrPeerConnTransceiverMidNil, - #[error("DTLSTransport must not be nil")] - ErrRTPReceiverDTLSTransportNil, - #[error("Receive has already been called")] - ErrRTPReceiverReceiveAlreadyCalled, - #[error("unable to find stream for Track with SSRC")] - ErrRTPReceiverWithSSRCTrackStreamNotFound, - #[error("no trackStreams found for SSRC")] - ErrRTPReceiverForSSRCTrackStreamNotFound, - #[error("no trackStreams found for RID")] - ErrRTPReceiverForRIDTrackStreamNotFound, - #[error("invalid RTP Receiver transition from {from} to {to}")] - ErrRTPReceiverStateChangeInvalid { - from: rtp_receiver::State, - to: rtp_receiver::State, - }, - #[error("Track must not be nil")] - ErrRTPSenderTrackNil, - #[error("Sender has already been stopped")] - ErrRTPSenderStopped, - #[error("Sender Track has been removed or replaced to nil")] - ErrRTPSenderTrackRemoved, - #[error("Sender cannot add encoding as rid is empty")] - ErrRTPSenderRidNil, - #[error("Sender cannot add encoding as there is no base track")] - ErrRTPSenderNoBaseEncoding, - #[error("Sender cannot add encoding as provided track does not match base track")] - ErrRTPSenderBaseEncodingMismatch, - #[error("Sender cannot encoding due to RID collision")] - ErrRTPSenderRIDCollision, - #[error("Sender does not have track for RID")] - ErrRTPSenderNoTrackForRID, - #[error("RTPSender must not be nil")] - ErrRTPSenderNil, - #[error("RTPReceiver must not be nil")] - ErrRTPReceiverNil, - #[error("DTLSTransport must not be nil")] - ErrRTPSenderDTLSTransportNil, - #[error("Send has already been called")] - ErrRTPSenderSendAlreadyCalled, - #[error("errRTPSenderTrackNil")] - ErrRTPTransceiverCannotChangeMid, - #[error("invalid state change in RTPTransceiver.setSending")] - ErrRTPTransceiverSetSendingInvalidState, - #[error("unsupported codec type by this transceiver")] - ErrRTPTransceiverCodecUnsupported, - #[error("DTLS not established")] - ErrSCTPTransportDTLS, - #[error("add_transceiver_sdp() called with 0 transceivers")] - ErrSDPZeroTransceivers, - #[error("invalid Media Section. Media + DataChannel both enabled")] - ErrSDPMediaSectionMediaDataChanInvalid, - #[error( - "invalid Media Section. Can not have multiple tracks in one MediaSection in UnifiedPlan" - )] - ErrSDPMediaSectionMultipleTrackInvalid, - #[error("set_answering_dtlsrole must DTLSRoleClient or DTLSRoleServer")] - ErrSettingEngineSetAnsweringDTLSRole, - #[error("can't rollback from stable state")] - ErrSignalingStateCannotRollback, - #[error( - "invalid proposed signaling state transition from {} applying {} {}", - from, - if *is_local { "local" } else { "remote" }, - applying - )] - ErrSignalingStateProposedTransitionInvalid { - from: RTCSignalingState, - applying: RTCSdpType, - is_local: bool, - }, - #[error("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state")] - ErrStatsICECandidateStateInvalid, - #[error("ICETransport can only be called in ICETransportStateNew")] - ErrICETransportNotInNew, - #[error("bad Certificate PEM format")] - ErrCertificatePEMFormatError, - #[error("SCTP is not established")] - ErrSCTPNotEstablished, - - #[error("DataChannel is not opened")] - ErrClosedPipe, - #[error("Interceptor is not bind")] - ErrInterceptorNotBind, - #[error("excessive retries in CreateOffer")] - ErrExcessiveRetries, - - #[error("not long enough to be a RTP Packet")] - ErrRTPTooShort, - - #[error("{0}")] - Util(#[from] util::Error), - #[error("{0}")] - Ice(#[from] ice::Error), - #[error("{0}")] - Srtp(#[from] srtp::Error), - #[error("{0}")] - Dtls(#[from] dtls::Error), - #[error("{0}")] - Data(#[from] data::Error), - #[error("{0}")] - Sctp(#[from] sctp::Error), - #[error("{0}")] - Sdp(#[from] sdp::Error), - #[error("{0}")] - Interceptor(#[from] interceptor::Error), - #[error("{0}")] - Rtcp(#[from] rtcp::Error), - #[error("{0}")] - Rtp(#[from] rtp::Error), - - #[error("utf-8 error: {0}")] - Utf8(#[from] FromUtf8Error), - #[error("{0}")] - RcGen(#[from] rcgen::Error), - #[error("mpsc send: {0}")] - MpscSend(String), - #[error("parse int: {0}")] - ParseInt(#[from] ParseIntError), - #[error("parse url: {0}")] - ParseUrl(#[from] url::ParseError), - - /// Error parsing a given PEM string. - #[error("invalid PEM: {0}")] - InvalidPEM(String), - - #[allow(non_camel_case_types)] - #[error("{0}")] - new(String), -} - -pub type OnErrorHdlrFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -// Because Tokio SendError is parameterized, we sadly lose the backtrace. -impl From> for Error { - fn from(e: MpscSendError) -> Self { - Error::MpscSend(e.to_string()) - } -} - -impl From for interceptor::Error { - fn from(e: Error) -> Self { - // this is a bit lol, but we do preserve the stack trace - interceptor::Error::Util(util::Error::from_std(e)) - } -} - -impl PartialEq for Error { - fn eq(&self, other: &ice::Error) -> bool { - if let Error::Ice(e) = self { - return e == other; - } - false - } -} - -/// flatten_errs flattens multiple errors into one -pub fn flatten_errs(errs: Vec>) -> Result<()> { - if errs.is_empty() { - Ok(()) - } else { - let errs_strs: Vec = errs.into_iter().map(|e| e.into().to_string()).collect(); - Err(Error::new(errs_strs.join("\n"))) - } -} diff --git a/webrtc/src/ice_transport/ice_candidate.rs b/webrtc/src/ice_transport/ice_candidate.rs deleted file mode 100644 index 9eab74dae..000000000 --- a/webrtc/src/ice_transport/ice_candidate.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::fmt; -use std::sync::Arc; - -use ice::candidate::candidate_base::CandidateBaseConfig; -use ice::candidate::candidate_host::CandidateHostConfig; -use ice::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; -use ice::candidate::candidate_relay::CandidateRelayConfig; -use ice::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; -use ice::candidate::Candidate; -use serde::{Deserialize, Serialize}; - -use crate::error::{Error, Result}; -use crate::ice_transport::ice_candidate_type::RTCIceCandidateType; -use crate::ice_transport::ice_protocol::RTCIceProtocol; - -/// ICECandidate represents a ice candidate -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RTCIceCandidate { - pub stats_id: String, - pub foundation: String, - pub priority: u32, - pub address: String, - pub protocol: RTCIceProtocol, - pub port: u16, - pub typ: RTCIceCandidateType, - pub component: u16, - pub related_address: String, - pub related_port: u16, - pub tcp_type: String, -} - -/// Conversion for ice_candidates -pub(crate) fn rtc_ice_candidates_from_ice_candidates( - ice_candidates: &[Arc], -) -> Vec { - ice_candidates.iter().map(|c| c.into()).collect() -} - -impl From<&Arc> for RTCIceCandidate { - fn from(c: &Arc) -> Self { - let typ: RTCIceCandidateType = c.candidate_type().into(); - let protocol = RTCIceProtocol::from(c.network_type().network_short().as_str()); - let (related_address, related_port) = if let Some(ra) = c.related_address() { - (ra.address, ra.port) - } else { - (String::new(), 0) - }; - - RTCIceCandidate { - stats_id: c.id(), - foundation: c.foundation(), - priority: c.priority(), - address: c.address(), - protocol, - port: c.port(), - component: c.component(), - typ, - tcp_type: c.tcp_type().to_string(), - related_address, - related_port, - } - } -} - -impl RTCIceCandidate { - pub(crate) fn to_ice(&self) -> Result { - let candidate_id = self.stats_id.clone(); - let base_config = CandidateBaseConfig { - candidate_id, - network: self.protocol.to_string(), - address: self.address.clone(), - port: self.port, - component: self.component, - //tcp_type: ice.NewTCPType(c.TCPType), - foundation: self.foundation.clone(), - priority: self.priority, - ..Default::default() - }; - - let c = match self.typ { - RTCIceCandidateType::Host => { - let config = CandidateHostConfig { - base_config, - ..Default::default() - }; - config.new_candidate_host()? - } - RTCIceCandidateType::Srflx => { - let config = CandidateServerReflexiveConfig { - base_config, - rel_addr: self.related_address.clone(), - rel_port: self.related_port, - }; - config.new_candidate_server_reflexive()? - } - RTCIceCandidateType::Prflx => { - let config = CandidatePeerReflexiveConfig { - base_config, - rel_addr: self.related_address.clone(), - rel_port: self.related_port, - }; - config.new_candidate_peer_reflexive()? - } - RTCIceCandidateType::Relay => { - let config = CandidateRelayConfig { - base_config, - rel_addr: self.related_address.clone(), - rel_port: self.related_port, - relay_client: None, //TODO? - }; - config.new_candidate_relay()? - } - _ => return Err(Error::ErrICECandidateTypeUnknown), - }; - - Ok(c) - } - - /// to_json returns an ICECandidateInit - /// as indicated by the spec - pub fn to_json(&self) -> Result { - let candidate = self.to_ice()?; - - Ok(RTCIceCandidateInit { - candidate: format!("candidate:{}", candidate.marshal()), - sdp_mid: Some("".to_owned()), - sdp_mline_index: Some(0u16), - username_fragment: None, - }) - } -} - -impl fmt::Display for RTCIceCandidate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {}:{}{}", - self.protocol, self.typ, self.address, self.port, self.related_address, - ) - } -} - -/// ICECandidateInit is used to serialize ice candidates -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RTCIceCandidateInit { - pub candidate: String, - pub sdp_mid: Option, - #[serde(rename = "sdpMLineIndex")] - pub sdp_mline_index: Option, - pub username_fragment: Option, -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ice_candidate_serialization() { - let tests = vec![ - ( - RTCIceCandidateInit { - candidate: "candidate:abc123".to_string(), - sdp_mid: Some("0".to_string()), - sdp_mline_index: Some(0), - username_fragment: Some("def".to_string()), - }, - r#"{"candidate":"candidate:abc123","sdpMid":"0","sdpMLineIndex":0,"usernameFragment":"def"}"#, - ), - ( - RTCIceCandidateInit { - candidate: "candidate:abc123".to_string(), - sdp_mid: None, - sdp_mline_index: None, - username_fragment: None, - }, - r#"{"candidate":"candidate:abc123","sdpMid":null,"sdpMLineIndex":null,"usernameFragment":null}"#, - ), - ]; - - for (candidate_init, expected_string) in tests { - let result = serde_json::to_string(&candidate_init); - assert!(result.is_ok(), "testCase: marshal err: {result:?}"); - let candidate_data = result.unwrap(); - assert_eq!(candidate_data, expected_string, "string is not expected"); - - let result = serde_json::from_str::(&candidate_data); - assert!(result.is_ok(), "testCase: unmarshal err: {result:?}"); - if let Ok(actual_candidate_init) = result { - assert_eq!(actual_candidate_init, candidate_init); - } - } - } -} diff --git a/webrtc/src/ice_transport/ice_candidate_pair.rs b/webrtc/src/ice_transport/ice_candidate_pair.rs deleted file mode 100644 index 8c0684c0d..000000000 --- a/webrtc/src/ice_transport/ice_candidate_pair.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::fmt; - -use crate::ice_transport::ice_candidate::*; - -/// ICECandidatePair represents an ICE Candidate pair -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RTCIceCandidatePair { - stats_id: String, - local: RTCIceCandidate, - remote: RTCIceCandidate, -} - -impl fmt::Display for RTCIceCandidatePair { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "(local) {} <-> (remote) {}", self.local, self.remote) - } -} - -impl RTCIceCandidatePair { - fn stats_id(local_id: &str, remote_id: &str) -> String { - format!("{local_id}-{remote_id}") - } - - /// returns an initialized ICECandidatePair - /// for the given pair of ICECandidate instances - pub fn new(local: RTCIceCandidate, remote: RTCIceCandidate) -> Self { - let stats_id = Self::stats_id(&local.stats_id, &remote.stats_id); - RTCIceCandidatePair { - stats_id, - local, - remote, - } - } -} diff --git a/webrtc/src/ice_transport/ice_candidate_type.rs b/webrtc/src/ice_transport/ice_candidate_type.rs deleted file mode 100644 index ec44328f7..000000000 --- a/webrtc/src/ice_transport/ice_candidate_type.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::fmt; - -use ice::candidate::CandidateType; -use serde::{Deserialize, Serialize}; - -/// ICECandidateType represents the type of the ICE candidate used. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RTCIceCandidateType { - #[default] - Unspecified, - - /// ICECandidateTypeHost indicates that the candidate is of Host type as - /// described in . A - /// candidate obtained by binding to a specific port from an IP address on - /// the host. This includes IP addresses on physical interfaces and logical - /// ones, such as ones obtained through VPNs. - #[serde(rename = "host")] - Host, - - /// ICECandidateTypeSrflx indicates the the candidate is of Server - /// Reflexive type as described - /// . A candidate type - /// whose IP address and port are a binding allocated by a NAT for an ICE - /// agent after it sends a packet through the NAT to a server, such as a - /// STUN server. - #[serde(rename = "srflx")] - Srflx, - - /// ICECandidateTypePrflx indicates that the candidate is of Peer - /// Reflexive type. A candidate type whose IP address and port are a binding - /// allocated by a NAT for an ICE agent after it sends a packet through the - /// NAT to its peer. - #[serde(rename = "prflx")] - Prflx, - - /// ICECandidateTypeRelay indicates the the candidate is of Relay type as - /// described in . A - /// candidate type obtained from a relay server, such as a TURN server. - #[serde(rename = "relay")] - Relay, -} - -const ICE_CANDIDATE_TYPE_HOST_STR: &str = "host"; -const ICE_CANDIDATE_TYPE_SRFLX_STR: &str = "srflx"; -const ICE_CANDIDATE_TYPE_PRFLX_STR: &str = "prflx"; -const ICE_CANDIDATE_TYPE_RELAY_STR: &str = "relay"; - -/// takes a string and converts it into ICECandidateType -impl From<&str> for RTCIceCandidateType { - fn from(raw: &str) -> Self { - match raw { - ICE_CANDIDATE_TYPE_HOST_STR => RTCIceCandidateType::Host, - ICE_CANDIDATE_TYPE_SRFLX_STR => RTCIceCandidateType::Srflx, - ICE_CANDIDATE_TYPE_PRFLX_STR => RTCIceCandidateType::Prflx, - ICE_CANDIDATE_TYPE_RELAY_STR => RTCIceCandidateType::Relay, - _ => RTCIceCandidateType::Unspecified, - } - } -} - -impl From for RTCIceCandidateType { - fn from(candidate_type: CandidateType) -> Self { - match candidate_type { - CandidateType::Host => RTCIceCandidateType::Host, - CandidateType::ServerReflexive => RTCIceCandidateType::Srflx, - CandidateType::PeerReflexive => RTCIceCandidateType::Prflx, - CandidateType::Relay => RTCIceCandidateType::Relay, - _ => RTCIceCandidateType::Unspecified, - } - } -} - -impl fmt::Display for RTCIceCandidateType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceCandidateType::Host => write!(f, "{ICE_CANDIDATE_TYPE_HOST_STR}"), - RTCIceCandidateType::Srflx => write!(f, "{ICE_CANDIDATE_TYPE_SRFLX_STR}"), - RTCIceCandidateType::Prflx => write!(f, "{ICE_CANDIDATE_TYPE_PRFLX_STR}"), - RTCIceCandidateType::Relay => write!(f, "{ICE_CANDIDATE_TYPE_RELAY_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ice_candidate_type() { - let tests = vec![ - ("Unspecified", RTCIceCandidateType::Unspecified), - ("host", RTCIceCandidateType::Host), - ("srflx", RTCIceCandidateType::Srflx), - ("prflx", RTCIceCandidateType::Prflx), - ("relay", RTCIceCandidateType::Relay), - ]; - - for (type_string, expected_type) in tests { - let actual = RTCIceCandidateType::from(type_string); - assert_eq!(actual, expected_type); - } - } - - #[test] - fn test_ice_candidate_type_string() { - let tests = vec![ - (RTCIceCandidateType::Unspecified, "Unspecified"), - (RTCIceCandidateType::Host, "host"), - (RTCIceCandidateType::Srflx, "srflx"), - (RTCIceCandidateType::Prflx, "prflx"), - (RTCIceCandidateType::Relay, "relay"), - ]; - - for (ctype, expected_string) in tests { - assert_eq!(ctype.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_connection_state.rs b/webrtc/src/ice_transport/ice_connection_state.rs deleted file mode 100644 index d019854ae..000000000 --- a/webrtc/src/ice_transport/ice_connection_state.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::fmt; - -/// RTCIceConnectionState indicates signaling state of the ICE Connection. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCIceConnectionState { - #[default] - Unspecified, - - /// ICEConnectionStateNew indicates that any of the ICETransports are - /// in the "new" state and none of them are in the "checking", "disconnected" - /// or "failed" state, or all ICETransports are in the "closed" state, or - /// there are no transports. - New, - - /// ICEConnectionStateChecking indicates that any of the ICETransports - /// are in the "checking" state and none of them are in the "disconnected" - /// or "failed" state. - Checking, - - /// ICEConnectionStateConnected indicates that all ICETransports are - /// in the "connected", "completed" or "closed" state and at least one of - /// them is in the "connected" state. - Connected, - - /// ICEConnectionStateCompleted indicates that all ICETransports are - /// in the "completed" or "closed" state and at least one of them is in the - /// "completed" state. - Completed, - - /// ICEConnectionStateDisconnected indicates that any of the - /// ICETransports are in the "disconnected" state and none of them are - /// in the "failed" state. - Disconnected, - - /// ICEConnectionStateFailed indicates that any of the ICETransports - /// are in the "failed" state. - Failed, - - /// ICEConnectionStateClosed indicates that the PeerConnection's - /// isClosed is true. - Closed, -} - -const ICE_CONNECTION_STATE_NEW_STR: &str = "new"; -const ICE_CONNECTION_STATE_CHECKING_STR: &str = "checking"; -const ICE_CONNECTION_STATE_CONNECTED_STR: &str = "connected"; -const ICE_CONNECTION_STATE_COMPLETED_STR: &str = "completed"; -const ICE_CONNECTION_STATE_DISCONNECTED_STR: &str = "disconnected"; -const ICE_CONNECTION_STATE_FAILED_STR: &str = "failed"; -const ICE_CONNECTION_STATE_CLOSED_STR: &str = "closed"; - -/// takes a string and converts it to iceconnection_state -impl From<&str> for RTCIceConnectionState { - fn from(raw: &str) -> Self { - match raw { - ICE_CONNECTION_STATE_NEW_STR => RTCIceConnectionState::New, - ICE_CONNECTION_STATE_CHECKING_STR => RTCIceConnectionState::Checking, - ICE_CONNECTION_STATE_CONNECTED_STR => RTCIceConnectionState::Connected, - ICE_CONNECTION_STATE_COMPLETED_STR => RTCIceConnectionState::Completed, - ICE_CONNECTION_STATE_DISCONNECTED_STR => RTCIceConnectionState::Disconnected, - ICE_CONNECTION_STATE_FAILED_STR => RTCIceConnectionState::Failed, - ICE_CONNECTION_STATE_CLOSED_STR => RTCIceConnectionState::Closed, - _ => RTCIceConnectionState::Unspecified, - } - } -} - -impl From for RTCIceConnectionState { - fn from(v: u8) -> Self { - match v { - 1 => RTCIceConnectionState::New, - 2 => RTCIceConnectionState::Checking, - 3 => RTCIceConnectionState::Connected, - 4 => RTCIceConnectionState::Completed, - 5 => RTCIceConnectionState::Disconnected, - 6 => RTCIceConnectionState::Failed, - 7 => RTCIceConnectionState::Closed, - _ => RTCIceConnectionState::Unspecified, - } - } -} - -impl fmt::Display for RTCIceConnectionState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCIceConnectionState::New => ICE_CONNECTION_STATE_NEW_STR, - RTCIceConnectionState::Checking => ICE_CONNECTION_STATE_CHECKING_STR, - RTCIceConnectionState::Connected => ICE_CONNECTION_STATE_CONNECTED_STR, - RTCIceConnectionState::Completed => ICE_CONNECTION_STATE_COMPLETED_STR, - RTCIceConnectionState::Disconnected => ICE_CONNECTION_STATE_DISCONNECTED_STR, - RTCIceConnectionState::Failed => ICE_CONNECTION_STATE_FAILED_STR, - RTCIceConnectionState::Closed => ICE_CONNECTION_STATE_CLOSED_STR, - RTCIceConnectionState::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_connection_state() { - let tests = vec![ - (crate::UNSPECIFIED_STR, RTCIceConnectionState::Unspecified), - ("new", RTCIceConnectionState::New), - ("checking", RTCIceConnectionState::Checking), - ("connected", RTCIceConnectionState::Connected), - ("completed", RTCIceConnectionState::Completed), - ("disconnected", RTCIceConnectionState::Disconnected), - ("failed", RTCIceConnectionState::Failed), - ("closed", RTCIceConnectionState::Closed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!( - RTCIceConnectionState::from(state_string), - expected_state, - "testCase: {expected_state}", - ); - } - } - - #[test] - fn test_ice_connection_state_string() { - let tests = vec![ - (RTCIceConnectionState::Unspecified, crate::UNSPECIFIED_STR), - (RTCIceConnectionState::New, "new"), - (RTCIceConnectionState::Checking, "checking"), - (RTCIceConnectionState::Connected, "connected"), - (RTCIceConnectionState::Completed, "completed"), - (RTCIceConnectionState::Disconnected, "disconnected"), - (RTCIceConnectionState::Failed, "failed"), - (RTCIceConnectionState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string) - } - } -} diff --git a/webrtc/src/ice_transport/ice_credential_type.rs b/webrtc/src/ice_transport/ice_credential_type.rs deleted file mode 100644 index 5db6c6cd2..000000000 --- a/webrtc/src/ice_transport/ice_credential_type.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// ICECredentialType indicates the type of credentials used to connect to -/// an ICE server. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub enum RTCIceCredentialType { - #[default] - Unspecified, - - /// ICECredential::Password describes username and password based - /// credentials as described in . - Password, - - /// ICECredential::Oauth describes token based credential as described - /// in . - /// Not supported in WebRTC 1.0 spec - Oauth, -} - -const ICE_CREDENTIAL_TYPE_PASSWORD_STR: &str = "password"; -const ICE_CREDENTIAL_TYPE_OAUTH_STR: &str = "oauth"; - -impl From<&str> for RTCIceCredentialType { - fn from(raw: &str) -> Self { - match raw { - ICE_CREDENTIAL_TYPE_PASSWORD_STR => RTCIceCredentialType::Password, - ICE_CREDENTIAL_TYPE_OAUTH_STR => RTCIceCredentialType::Oauth, - _ => RTCIceCredentialType::Unspecified, - } - } -} - -impl fmt::Display for RTCIceCredentialType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceCredentialType::Password => write!(f, "{ICE_CREDENTIAL_TYPE_PASSWORD_STR}"), - RTCIceCredentialType::Oauth => write!(f, "{ICE_CREDENTIAL_TYPE_OAUTH_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_credential_type() { - let tests = vec![ - ("Unspecified", RTCIceCredentialType::Unspecified), - ("password", RTCIceCredentialType::Password), - ("oauth", RTCIceCredentialType::Oauth), - ]; - - for (ct_str, expected_ct) in tests { - assert_eq!(RTCIceCredentialType::from(ct_str), expected_ct); - } - } - - #[test] - fn test_ice_credential_type_string() { - let tests = vec![ - (RTCIceCredentialType::Unspecified, "Unspecified"), - (RTCIceCredentialType::Password, "password"), - (RTCIceCredentialType::Oauth, "oauth"), - ]; - - for (ct, expected_string) in tests { - assert_eq!(ct.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_gatherer.rs b/webrtc/src/ice_transport/ice_gatherer.rs deleted file mode 100644 index d247e3bd1..000000000 --- a/webrtc/src/ice_transport/ice_gatherer.rs +++ /dev/null @@ -1,410 +0,0 @@ -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use arc_swap::ArcSwapOption; -use ice::agent::Agent; -use ice::candidate::{Candidate, CandidateType}; -use ice::url::Url; -use portable_atomic::AtomicU8; -use tokio::sync::Mutex; - -use crate::api::setting_engine::SettingEngine; -use crate::error::{Error, Result}; -use crate::ice_transport::ice_candidate::*; -use crate::ice_transport::ice_candidate_type::RTCIceCandidateType; -use crate::ice_transport::ice_gatherer_state::RTCIceGathererState; -use crate::ice_transport::ice_parameters::RTCIceParameters; -use crate::ice_transport::ice_server::RTCIceServer; -use crate::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::SourceStatsType::*; -use crate::stats::{ICECandidatePairStats, StatsReportType}; - -/// ICEGatherOptions provides options relating to the gathering of ICE candidates. -#[derive(Default, Debug, Clone)] -pub struct RTCIceGatherOptions { - pub ice_servers: Vec, - pub ice_gather_policy: RTCIceTransportPolicy, -} - -pub type OnLocalCandidateHdlrFn = Box< - dyn (FnMut(Option) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnICEGathererStateChangeHdlrFn = Box< - dyn (FnMut(RTCIceGathererState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnGatheringCompleteHdlrFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -/// ICEGatherer gathers local host, server reflexive and relay -/// candidates, as well as enabling the retrieval of local Interactive -/// Connectivity Establishment (ICE) parameters which can be -/// exchanged in signaling. -#[derive(Default)] -pub struct RTCIceGatherer { - pub(crate) validated_servers: Vec, - pub(crate) gather_policy: RTCIceTransportPolicy, - pub(crate) setting_engine: Arc, - - pub(crate) state: Arc, //ICEGathererState, - pub(crate) agent: Mutex>>, - - pub(crate) on_local_candidate_handler: Arc>>, - pub(crate) on_state_change_handler: Arc>>, - - // Used for gathering_complete_promise - pub(crate) on_gathering_complete_handler: Arc>>, -} - -impl RTCIceGatherer { - pub(crate) fn new( - validated_servers: Vec, - gather_policy: RTCIceTransportPolicy, - setting_engine: Arc, - ) -> Self { - RTCIceGatherer { - gather_policy, - validated_servers, - setting_engine, - state: Arc::new(AtomicU8::new(RTCIceGathererState::New as u8)), - ..Default::default() - } - } - - pub(crate) async fn create_agent(&self) -> Result<()> { - // NOTE: A lock is held for the duration of this function in order to - // avoid potential double-agent creations. Care should be taken to - // ensure we do not do anything expensive other than the actual agent - // creation in this function. - let mut agent = self.agent.lock().await; - - if agent.is_some() || self.state() != RTCIceGathererState::New { - return Ok(()); - } - - let mut candidate_types = vec![]; - if self.setting_engine.candidates.ice_lite { - candidate_types.push(ice::candidate::CandidateType::Host); - } else if self.gather_policy == RTCIceTransportPolicy::Relay { - candidate_types.push(ice::candidate::CandidateType::Relay); - } - - let nat_1to1_cand_type = match self.setting_engine.candidates.nat_1to1_ip_candidate_type { - RTCIceCandidateType::Host => CandidateType::Host, - RTCIceCandidateType::Srflx => CandidateType::ServerReflexive, - _ => CandidateType::Unspecified, - }; - - let mdns_mode = self.setting_engine.candidates.multicast_dns_mode; - - let mut config = ice::agent::agent_config::AgentConfig { - udp_network: self.setting_engine.udp_network.clone(), - lite: self.setting_engine.candidates.ice_lite, - urls: self.validated_servers.clone(), - disconnected_timeout: self.setting_engine.timeout.ice_disconnected_timeout, - failed_timeout: self.setting_engine.timeout.ice_failed_timeout, - keepalive_interval: self.setting_engine.timeout.ice_keepalive_interval, - candidate_types, - host_acceptance_min_wait: self.setting_engine.timeout.ice_host_acceptance_min_wait, - srflx_acceptance_min_wait: self.setting_engine.timeout.ice_srflx_acceptance_min_wait, - prflx_acceptance_min_wait: self.setting_engine.timeout.ice_prflx_acceptance_min_wait, - relay_acceptance_min_wait: self.setting_engine.timeout.ice_relay_acceptance_min_wait, - interface_filter: self.setting_engine.candidates.interface_filter.clone(), - ip_filter: self.setting_engine.candidates.ip_filter.clone(), - nat_1to1_ips: self.setting_engine.candidates.nat_1to1_ips.clone(), - nat_1to1_ip_candidate_type: nat_1to1_cand_type, - net: self.setting_engine.vnet.clone(), - multicast_dns_mode: mdns_mode, - multicast_dns_host_name: self - .setting_engine - .candidates - .multicast_dns_host_name - .clone(), - local_ufrag: self.setting_engine.candidates.username_fragment.clone(), - local_pwd: self.setting_engine.candidates.password.clone(), - //TODO: TCPMux: self.setting_engine.iceTCPMux, - //TODO: ProxyDialer: self.setting_engine.iceProxyDialer, - ..Default::default() - }; - - let requested_network_types = if self.setting_engine.candidates.ice_network_types.is_empty() - { - ice::network_type::supported_network_types() - } else { - self.setting_engine.candidates.ice_network_types.clone() - }; - - config.network_types.extend(requested_network_types); - - *agent = Some(Arc::new(ice::agent::Agent::new(config).await?)); - - Ok(()) - } - - /// Gather ICE candidates. - pub async fn gather(&self) -> Result<()> { - self.create_agent().await?; - self.set_state(RTCIceGathererState::Gathering).await; - - if let Some(agent) = self.get_agent().await { - let state = Arc::clone(&self.state); - let on_local_candidate_handler = Arc::clone(&self.on_local_candidate_handler); - let on_state_change_handler = Arc::clone(&self.on_state_change_handler); - let on_gathering_complete_handler = Arc::clone(&self.on_gathering_complete_handler); - - agent.on_candidate(Box::new( - move |candidate: Option>| { - let state_clone = Arc::clone(&state); - let on_local_candidate_handler_clone = Arc::clone(&on_local_candidate_handler); - let on_state_change_handler_clone = Arc::clone(&on_state_change_handler); - let on_gathering_complete_handler_clone = - Arc::clone(&on_gathering_complete_handler); - - Box::pin(async move { - if let Some(cand) = candidate { - if let Some(handler) = &*on_local_candidate_handler_clone.load() { - let mut f = handler.lock().await; - f(Some(RTCIceCandidate::from(&cand))).await; - } - } else { - state_clone - .store(RTCIceGathererState::Complete as u8, Ordering::SeqCst); - - if let Some(handler) = &*on_state_change_handler_clone.load() { - let mut f = handler.lock().await; - f(RTCIceGathererState::Complete).await; - } - - if let Some(handler) = &*on_gathering_complete_handler_clone.load() { - let mut f = handler.lock().await; - f().await; - } - - if let Some(handler) = &*on_local_candidate_handler_clone.load() { - let mut f = handler.lock().await; - f(None).await; - } - } - }) - }, - )); - - agent.gather_candidates()?; - } - - Ok(()) - } - - /// Close prunes all local candidates, and closes the ports. - pub async fn close(&self) -> Result<()> { - self.set_state(RTCIceGathererState::Closed).await; - - let agent = { - let mut agent_opt = self.agent.lock().await; - agent_opt.take() - }; - - if let Some(agent) = agent { - agent.close().await?; - } - - Ok(()) - } - - /// get_local_parameters returns the ICE parameters of the ICEGatherer. - pub async fn get_local_parameters(&self) -> Result { - self.create_agent().await?; - - let (frag, pwd) = if let Some(agent) = self.get_agent().await { - agent.get_local_user_credentials().await - } else { - return Err(Error::ErrICEAgentNotExist); - }; - - Ok(RTCIceParameters { - username_fragment: frag, - password: pwd, - ice_lite: false, - }) - } - - /// get_local_candidates returns the sequence of valid local candidates associated with the ICEGatherer. - pub async fn get_local_candidates(&self) -> Result> { - self.create_agent().await?; - - let ice_candidates = if let Some(agent) = self.get_agent().await { - agent.get_local_candidates().await? - } else { - return Err(Error::ErrICEAgentNotExist); - }; - - Ok(rtc_ice_candidates_from_ice_candidates(&ice_candidates)) - } - - /// on_local_candidate sets an event handler which fires when a new local ICE candidate is available - /// Take note that the handler is gonna be called with a nil pointer when gathering is finished. - pub fn on_local_candidate(&self, f: OnLocalCandidateHdlrFn) { - self.on_local_candidate_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_state_change sets an event handler which fires any time the ICEGatherer changes - pub fn on_state_change(&self, f: OnICEGathererStateChangeHdlrFn) { - self.on_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_gathering_complete sets an event handler which fires any time the ICEGatherer changes - pub fn on_gathering_complete(&self, f: OnGatheringCompleteHdlrFn) { - self.on_gathering_complete_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// State indicates the current state of the ICE gatherer. - pub fn state(&self) -> RTCIceGathererState { - self.state.load(Ordering::SeqCst).into() - } - - pub async fn set_state(&self, s: RTCIceGathererState) { - self.state.store(s as u8, Ordering::SeqCst); - - if let Some(handler) = &*self.on_state_change_handler.load() { - let mut f = handler.lock().await; - f(s).await; - } - } - - pub(crate) async fn get_agent(&self) -> Option> { - let agent = self.agent.lock().await; - agent.clone() - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - if let Some(agent) = self.get_agent().await { - let mut reports = HashMap::new(); - - for stats in agent.get_candidate_pairs_stats().await { - let stats: ICECandidatePairStats = stats.into(); - reports.insert(stats.id.clone(), StatsReportType::CandidatePair(stats)); - } - - for stats in agent.get_local_candidates_stats().await { - reports.insert( - stats.id.clone(), - StatsReportType::from(LocalCandidate(stats)), - ); - } - - for stats in agent.get_remote_candidates_stats().await { - reports.insert( - stats.id.clone(), - StatsReportType::from(RemoteCandidate(stats)), - ); - } - - collector.merge(reports); - } - } -} - -#[cfg(test)] -mod test { - use tokio::sync::mpsc; - - use super::*; - use crate::api::APIBuilder; - use crate::ice_transport::ice_gatherer::RTCIceGatherOptions; - use crate::ice_transport::ice_server::RTCIceServer; - - #[tokio::test] - async fn test_new_ice_gatherer_success() -> Result<()> { - let opts = RTCIceGatherOptions { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - let gatherer = APIBuilder::new().build().new_ice_gatherer(opts)?; - - assert_eq!( - gatherer.state(), - RTCIceGathererState::New, - "Expected gathering state new" - ); - - let (gather_finished_tx, mut gather_finished_rx) = mpsc::channel::<()>(1); - let gather_finished_tx = Arc::new(Mutex::new(Some(gather_finished_tx))); - gatherer.on_local_candidate(Box::new(move |c: Option| { - let gather_finished_tx_clone = Arc::clone(&gather_finished_tx); - Box::pin(async move { - if c.is_none() { - let mut tx = gather_finished_tx_clone.lock().await; - tx.take(); - } - }) - })); - - gatherer.gather().await?; - - let _ = gather_finished_rx.recv().await; - - let params = gatherer.get_local_parameters().await?; - - assert!( - !params.username_fragment.is_empty() && !params.password.is_empty(), - "Empty local username or password frag" - ); - - let candidates = gatherer.get_local_candidates().await?; - - assert!(!candidates.is_empty(), "No candidates gathered"); - - gatherer.close().await?; - - Ok(()) - } - - #[tokio::test] - async fn test_ice_gather_mdns_candidate_gathering() -> Result<()> { - let mut s = SettingEngine::default(); - s.set_ice_multicast_dns_mode(ice::mdns::MulticastDnsMode::QueryAndGather); - - let gatherer = APIBuilder::new() - .with_setting_engine(s) - .build() - .new_ice_gatherer(RTCIceGatherOptions::default())?; - - let (done_tx, mut done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - gatherer.on_local_candidate(Box::new(move |c: Option| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if let Some(c) = c { - if c.address.ends_with(".local") { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - } - }) - })); - - gatherer.gather().await?; - - let _ = done_rx.recv().await; - - gatherer.close().await?; - - Ok(()) - } -} diff --git a/webrtc/src/ice_transport/ice_gatherer_state.rs b/webrtc/src/ice_transport/ice_gatherer_state.rs deleted file mode 100644 index 7b24e9968..000000000 --- a/webrtc/src/ice_transport/ice_gatherer_state.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::fmt; - -/// ICEGathererState represents the current state of the ICE gatherer. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCIceGathererState { - #[default] - Unspecified, - - /// ICEGathererStateNew indicates object has been created but - /// gather() has not been called. - New, - - /// ICEGathererStateGathering indicates gather() has been called, - /// and the ICEGatherer is in the process of gathering candidates. - Gathering, - - /// ICEGathererStateComplete indicates the ICEGatherer has completed gathering. - Complete, - - /// ICEGathererStateClosed indicates the closed state can only be entered - /// when the ICEGatherer has been closed intentionally by calling close(). - Closed, -} - -const ICE_GATHERED_STATE_NEW_STR: &str = "new"; -const ICE_GATHERED_STATE_GATHERING_STR: &str = "gathering"; -const ICE_GATHERED_STATE_COMPLETE_STR: &str = "complete"; -const ICE_GATHERED_STATE_CLOSED_STR: &str = "closed"; - -impl From<&str> for RTCIceGathererState { - fn from(raw: &str) -> Self { - match raw { - ICE_GATHERED_STATE_NEW_STR => RTCIceGathererState::New, - ICE_GATHERED_STATE_GATHERING_STR => RTCIceGathererState::Gathering, - ICE_GATHERED_STATE_COMPLETE_STR => RTCIceGathererState::Complete, - ICE_GATHERED_STATE_CLOSED_STR => RTCIceGathererState::Closed, - _ => RTCIceGathererState::Unspecified, - } - } -} - -impl From for RTCIceGathererState { - fn from(v: u8) -> Self { - match v { - 1 => RTCIceGathererState::New, - 2 => RTCIceGathererState::Gathering, - 3 => RTCIceGathererState::Complete, - 4 => RTCIceGathererState::Closed, - _ => RTCIceGathererState::Unspecified, - } - } -} - -impl fmt::Display for RTCIceGathererState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceGathererState::New => write!(f, "{ICE_GATHERED_STATE_NEW_STR}"), - RTCIceGathererState::Gathering => write!(f, "{ICE_GATHERED_STATE_GATHERING_STR}"), - RTCIceGathererState::Complete => { - write!(f, "{ICE_GATHERED_STATE_COMPLETE_STR}") - } - RTCIceGathererState::Closed => { - write!(f, "{ICE_GATHERED_STATE_CLOSED_STR}") - } - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ice_gatherer_state_string() { - let tests = vec![ - (RTCIceGathererState::Unspecified, "Unspecified"), - (RTCIceGathererState::New, "new"), - (RTCIceGathererState::Gathering, "gathering"), - (RTCIceGathererState::Complete, "complete"), - (RTCIceGathererState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_gathering_state.rs b/webrtc/src/ice_transport/ice_gathering_state.rs deleted file mode 100644 index fa043312d..000000000 --- a/webrtc/src/ice_transport/ice_gathering_state.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::fmt; - -/// ICEGatheringState describes the state of the candidate gathering process. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCIceGatheringState { - #[default] - Unspecified, - - /// ICEGatheringStateNew indicates that any of the ICETransports are - /// in the "new" gathering state and none of the transports are in the - /// "gathering" state, or there are no transports. - New, - - /// ICEGatheringStateGathering indicates that any of the ICETransports - /// are in the "gathering" state. - Gathering, - - /// ICEGatheringStateComplete indicates that at least one ICETransport - /// exists, and all ICETransports are in the "completed" gathering state. - Complete, -} - -const ICE_GATHERING_STATE_NEW_STR: &str = "new"; -const ICE_GATHERING_STATE_GATHERING_STR: &str = "gathering"; -const ICE_GATHERING_STATE_COMPLETE_STR: &str = "complete"; - -/// takes a string and converts it to ICEGatheringState -impl From<&str> for RTCIceGatheringState { - fn from(raw: &str) -> Self { - match raw { - ICE_GATHERING_STATE_NEW_STR => RTCIceGatheringState::New, - ICE_GATHERING_STATE_GATHERING_STR => RTCIceGatheringState::Gathering, - ICE_GATHERING_STATE_COMPLETE_STR => RTCIceGatheringState::Complete, - _ => RTCIceGatheringState::Unspecified, - } - } -} - -impl fmt::Display for RTCIceGatheringState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceGatheringState::New => write!(f, "{ICE_GATHERING_STATE_NEW_STR}"), - RTCIceGatheringState::Gathering => write!(f, "{ICE_GATHERING_STATE_GATHERING_STR}"), - RTCIceGatheringState::Complete => { - write!(f, "{ICE_GATHERING_STATE_COMPLETE_STR}") - } - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_gathering_state() { - let tests = vec![ - ("Unspecified", RTCIceGatheringState::Unspecified), - ("new", RTCIceGatheringState::New), - ("gathering", RTCIceGatheringState::Gathering), - ("complete", RTCIceGatheringState::Complete), - ]; - - for (state_string, expected_state) in tests { - assert_eq!(RTCIceGatheringState::from(state_string), expected_state); - } - } - - #[test] - fn test_ice_gathering_state_string() { - let tests = vec![ - (RTCIceGatheringState::Unspecified, "Unspecified"), - (RTCIceGatheringState::New, "new"), - (RTCIceGatheringState::Gathering, "gathering"), - (RTCIceGatheringState::Complete, "complete"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_parameters.rs b/webrtc/src/ice_transport/ice_parameters.rs deleted file mode 100644 index 048e359b9..000000000 --- a/webrtc/src/ice_transport/ice_parameters.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// ICEParameters includes the ICE username fragment -/// and password and other ICE-related parameters. -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RTCIceParameters { - pub username_fragment: String, - pub password: String, - pub ice_lite: bool, -} diff --git a/webrtc/src/ice_transport/ice_protocol.rs b/webrtc/src/ice_transport/ice_protocol.rs deleted file mode 100644 index 308912505..000000000 --- a/webrtc/src/ice_transport/ice_protocol.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// ICEProtocol indicates the transport protocol type that is used in the -/// ice.URL structure. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RTCIceProtocol { - #[default] - Unspecified, - - /// UDP indicates the URL uses a UDP transport. - #[serde(rename = "udp")] - Udp, - - /// TCP indicates the URL uses a TCP transport. - #[serde(rename = "tcp")] - Tcp, -} - -const ICE_PROTOCOL_UDP_STR: &str = "udp"; -const ICE_PROTOCOL_TCP_STR: &str = "tcp"; - -/// takes a string and converts it to ICEProtocol -impl From<&str> for RTCIceProtocol { - fn from(raw: &str) -> Self { - if raw.to_uppercase() == ICE_PROTOCOL_UDP_STR.to_uppercase() { - RTCIceProtocol::Udp - } else if raw.to_uppercase() == ICE_PROTOCOL_TCP_STR.to_uppercase() { - RTCIceProtocol::Tcp - } else { - RTCIceProtocol::Unspecified - } - } -} - -impl fmt::Display for RTCIceProtocol { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceProtocol::Udp => write!(f, "{ICE_PROTOCOL_UDP_STR}"), - RTCIceProtocol::Tcp => write!(f, "{ICE_PROTOCOL_TCP_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_protocol() { - let tests = vec![ - ("Unspecified", RTCIceProtocol::Unspecified), - ("udp", RTCIceProtocol::Udp), - ("tcp", RTCIceProtocol::Tcp), - ("UDP", RTCIceProtocol::Udp), - ("TCP", RTCIceProtocol::Tcp), - ]; - - for (proto_string, expected_proto) in tests { - let actual = RTCIceProtocol::from(proto_string); - assert_eq!(actual, expected_proto); - } - } - - #[test] - fn test_ice_protocol_string() { - let tests = vec![ - (RTCIceProtocol::Unspecified, "Unspecified"), - (RTCIceProtocol::Udp, "udp"), - (RTCIceProtocol::Tcp, "tcp"), - ]; - - for (proto, expected_string) in tests { - assert_eq!(proto.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_role.rs b/webrtc/src/ice_transport/ice_role.rs deleted file mode 100644 index 699dd4b45..000000000 --- a/webrtc/src/ice_transport/ice_role.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt; - -/// ICERole describes the role ice.Agent is playing in selecting the -/// preferred the candidate pair. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCIceRole { - #[default] - Unspecified, - - /// ICERoleControlling indicates that the ICE agent that is responsible - /// for selecting the final choice of candidate pairs and signaling them - /// through STUN and an updated offer, if needed. In any session, one agent - /// is always controlling. The other is the controlled agent. - Controlling, - - /// ICERoleControlled indicates that an ICE agent that waits for the - /// controlling agent to select the final choice of candidate pairs. - Controlled, -} - -const ICE_ROLE_CONTROLLING_STR: &str = "controlling"; -const ICE_ROLE_CONTROLLED_STR: &str = "controlled"; - -impl From<&str> for RTCIceRole { - fn from(raw: &str) -> Self { - match raw { - ICE_ROLE_CONTROLLING_STR => RTCIceRole::Controlling, - ICE_ROLE_CONTROLLED_STR => RTCIceRole::Controlled, - _ => RTCIceRole::Unspecified, - } - } -} - -impl fmt::Display for RTCIceRole { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceRole::Controlling => write!(f, "{ICE_ROLE_CONTROLLING_STR}"), - RTCIceRole::Controlled => write!(f, "{ICE_ROLE_CONTROLLED_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_role() { - let tests = vec![ - ("Unspecified", RTCIceRole::Unspecified), - ("controlling", RTCIceRole::Controlling), - ("controlled", RTCIceRole::Controlled), - ]; - - for (role_string, expected_role) in tests { - assert_eq!(RTCIceRole::from(role_string), expected_role); - } - } - - #[test] - fn test_ice_role_string() { - let tests = vec![ - (RTCIceRole::Unspecified, "Unspecified"), - (RTCIceRole::Controlling, "controlling"), - (RTCIceRole::Controlled, "controlled"), - ]; - - for (proto, expected_string) in tests { - assert_eq!(proto.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/ice_transport/ice_server.rs b/webrtc/src/ice_transport/ice_server.rs deleted file mode 100644 index c43e2d2b8..000000000 --- a/webrtc/src/ice_transport/ice_server.rs +++ /dev/null @@ -1,173 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::error::{Error, Result}; -use crate::ice_transport::ice_credential_type::RTCIceCredentialType; - -/// ICEServer describes a single STUN and TURN server that can be used by -/// the ICEAgent to establish a connection with a peer. -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Hash)] -pub struct RTCIceServer { - pub urls: Vec, - pub username: String, - pub credential: String, - pub credential_type: RTCIceCredentialType, -} - -impl RTCIceServer { - pub(crate) fn parse_url(&self, url_str: &str) -> Result { - Ok(ice::url::Url::parse_url(url_str)?) - } - - pub(crate) fn validate(&self) -> Result<()> { - self.urls()?; - Ok(()) - } - - pub(crate) fn urls(&self) -> Result> { - let mut urls = vec![]; - - for url_str in &self.urls { - let mut url = self.parse_url(url_str)?; - if url.scheme == ice::url::SchemeType::Turn || url.scheme == ice::url::SchemeType::Turns - { - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.2) - if self.username.is_empty() || self.credential.is_empty() { - return Err(Error::ErrNoTurnCredentials); - } - url.username.clone_from(&self.username); - - match self.credential_type { - RTCIceCredentialType::Password => { - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.3) - url.password.clone_from(&self.credential); - } - RTCIceCredentialType::Oauth => { - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.4) - /*if _, ok: = s.Credential.(OAuthCredential); !ok { - return nil, - &rtcerr.InvalidAccessError{Err: ErrTurnCredentials - } - }*/ - } - _ => return Err(Error::ErrTurnCredentials), - }; - } - - urls.push(url); - } - - Ok(urls) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ice_server_validate_success() { - let tests = vec![ - ( - RTCIceServer { - urls: vec!["turn:192.158.29.39?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: "placeholder".to_owned(), - credential_type: RTCIceCredentialType::Password, - }, - true, - ), - ( - RTCIceServer { - urls: vec!["turn:[2001:db8:1234:5678::1]?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: "placeholder".to_owned(), - credential_type: RTCIceCredentialType::Password, - }, - true, - ), - /*TODO:(ICEServer{ - URLs: []string{"turn:192.158.29.39?transport=udp"}, - Username: "unittest".to_owned(), - Credential: OAuthCredential{ - MACKey: "WmtzanB3ZW9peFhtdm42NzUzNG0=", - AccessToken: "AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA==", - }, - CredentialType: ICECredentialTypeOauth, - }, true),*/ - ]; - - for (ice_server, expected_validate) in tests { - let result = ice_server.urls(); - assert_eq!(result.is_ok(), expected_validate); - } - } - - #[test] - fn test_ice_server_validate_failure() { - let tests = vec![ - ( - RTCIceServer { - urls: vec!["turn:192.158.29.39?transport=udp".to_owned()], - ..Default::default() - }, - Error::ErrNoTurnCredentials, - ), - ( - RTCIceServer { - urls: vec!["turn:192.158.29.39?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: String::new(), - credential_type: RTCIceCredentialType::Password, - }, - Error::ErrNoTurnCredentials, - ), - ( - RTCIceServer { - urls: vec!["turn:192.158.29.39?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: String::new(), - credential_type: RTCIceCredentialType::Oauth, - }, - Error::ErrNoTurnCredentials, - ), - ( - RTCIceServer { - urls: vec!["turn:192.158.29.39?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: String::new(), - credential_type: RTCIceCredentialType::Unspecified, - }, - Error::ErrNoTurnCredentials, - ), - ]; - - for (ice_server, expected_err) in tests { - if let Err(err) = ice_server.urls() { - assert_eq!(err, expected_err, "{ice_server:?} with err {err:?}"); - } else { - panic!("expected error, but got ok"); - } - } - } - - #[test] - fn test_ice_server_validate_failure_err_stun_query() { - let tests = vec![( - RTCIceServer { - urls: vec!["stun:google.de?transport=udp".to_owned()], - username: "unittest".to_owned(), - credential: String::new(), - credential_type: RTCIceCredentialType::Oauth, - }, - ice::Error::ErrStunQuery, - )]; - - for (ice_server, expected_err) in tests { - if let Err(err) = ice_server.urls() { - assert_eq!(err, expected_err, "{ice_server:?} with err {err:?}"); - } else { - panic!("expected error, but got ok"); - } - } - } -} diff --git a/webrtc/src/ice_transport/ice_transport_state.rs b/webrtc/src/ice_transport/ice_transport_state.rs deleted file mode 100644 index 2abdedef4..000000000 --- a/webrtc/src/ice_transport/ice_transport_state.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::fmt; - -use ice::state::ConnectionState; - -/// ICETransportState represents the current state of the ICE transport. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCIceTransportState { - #[default] - Unspecified, - - /// ICETransportStateNew indicates the ICETransport is waiting - /// for remote candidates to be supplied. - New, - - /// ICETransportStateChecking indicates the ICETransport has - /// received at least one remote candidate, and a local and remote - /// ICECandidateComplete dictionary was not added as the last candidate. - Checking, - - /// ICETransportStateConnected indicates the ICETransport has - /// received a response to an outgoing connectivity check, or has - /// received incoming DTLS/media after a successful response to an - /// incoming connectivity check, but is still checking other candidate - /// pairs to see if there is a better connection. - Connected, - - /// ICETransportStateCompleted indicates the ICETransport tested - /// all appropriate candidate pairs and at least one functioning - /// candidate pair has been found. - Completed, - - /// ICETransportStateFailed indicates the ICETransport the last - /// candidate was added and all appropriate candidate pairs have either - /// failed connectivity checks or have lost consent. - Failed, - - /// ICETransportStateDisconnected indicates the ICETransport has received - /// at least one local and remote candidate, but the final candidate was - /// received yet and all appropriate candidate pairs thus far have been - /// tested and failed. - Disconnected, - - /// ICETransportStateClosed indicates the ICETransport has shut down - /// and is no longer responding to STUN requests. - Closed, -} - -const ICE_TRANSPORT_STATE_NEW_STR: &str = "new"; -const ICE_TRANSPORT_STATE_CHECKING_STR: &str = "checking"; -const ICE_TRANSPORT_STATE_CONNECTED_STR: &str = "connected"; -const ICE_TRANSPORT_STATE_COMPLETED_STR: &str = "completed"; -const ICE_TRANSPORT_STATE_FAILED_STR: &str = "failed"; -const ICE_TRANSPORT_STATE_DISCONNECTED_STR: &str = "disconnected"; -const ICE_TRANSPORT_STATE_CLOSED_STR: &str = "closed"; - -impl From<&str> for RTCIceTransportState { - fn from(raw: &str) -> Self { - match raw { - ICE_TRANSPORT_STATE_NEW_STR => RTCIceTransportState::New, - ICE_TRANSPORT_STATE_CHECKING_STR => RTCIceTransportState::Checking, - ICE_TRANSPORT_STATE_CONNECTED_STR => RTCIceTransportState::Connected, - ICE_TRANSPORT_STATE_COMPLETED_STR => RTCIceTransportState::Completed, - ICE_TRANSPORT_STATE_FAILED_STR => RTCIceTransportState::Failed, - ICE_TRANSPORT_STATE_DISCONNECTED_STR => RTCIceTransportState::Disconnected, - ICE_TRANSPORT_STATE_CLOSED_STR => RTCIceTransportState::Closed, - _ => RTCIceTransportState::Unspecified, - } - } -} - -impl From for RTCIceTransportState { - fn from(v: u8) -> Self { - match v { - 1 => Self::New, - 2 => Self::Checking, - 3 => Self::Connected, - 4 => Self::Completed, - 5 => Self::Failed, - 6 => Self::Disconnected, - 7 => Self::Closed, - _ => Self::Unspecified, - } - } -} - -impl fmt::Display for RTCIceTransportState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCIceTransportState::New => write!(f, "{ICE_TRANSPORT_STATE_NEW_STR}"), - RTCIceTransportState::Checking => write!(f, "{ICE_TRANSPORT_STATE_CHECKING_STR}"), - RTCIceTransportState::Connected => { - write!(f, "{ICE_TRANSPORT_STATE_CONNECTED_STR}") - } - RTCIceTransportState::Completed => write!(f, "{ICE_TRANSPORT_STATE_COMPLETED_STR}"), - RTCIceTransportState::Failed => { - write!(f, "{ICE_TRANSPORT_STATE_FAILED_STR}") - } - RTCIceTransportState::Disconnected => { - write!(f, "{ICE_TRANSPORT_STATE_DISCONNECTED_STR}") - } - RTCIceTransportState::Closed => { - write!(f, "{ICE_TRANSPORT_STATE_CLOSED_STR}") - } - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -impl From for RTCIceTransportState { - fn from(raw: ConnectionState) -> Self { - match raw { - ConnectionState::New => RTCIceTransportState::New, - ConnectionState::Checking => RTCIceTransportState::Checking, - ConnectionState::Connected => RTCIceTransportState::Connected, - ConnectionState::Completed => RTCIceTransportState::Completed, - ConnectionState::Failed => RTCIceTransportState::Failed, - ConnectionState::Disconnected => RTCIceTransportState::Disconnected, - ConnectionState::Closed => RTCIceTransportState::Closed, - _ => RTCIceTransportState::Unspecified, - } - } -} - -impl RTCIceTransportState { - pub(crate) fn to_ice(self) -> ConnectionState { - match self { - RTCIceTransportState::New => ConnectionState::New, - RTCIceTransportState::Checking => ConnectionState::Checking, - RTCIceTransportState::Connected => ConnectionState::Connected, - RTCIceTransportState::Completed => ConnectionState::Completed, - RTCIceTransportState::Failed => ConnectionState::Failed, - RTCIceTransportState::Disconnected => ConnectionState::Disconnected, - RTCIceTransportState::Closed => ConnectionState::Closed, - _ => ConnectionState::Unspecified, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ice_transport_state_string() { - let tests = vec![ - (RTCIceTransportState::Unspecified, "Unspecified"), - (RTCIceTransportState::New, "new"), - (RTCIceTransportState::Checking, "checking"), - (RTCIceTransportState::Connected, "connected"), - (RTCIceTransportState::Completed, "completed"), - (RTCIceTransportState::Failed, "failed"), - (RTCIceTransportState::Disconnected, "disconnected"), - (RTCIceTransportState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string); - } - } - - #[test] - fn test_ice_transport_state_convert() { - let tests = vec![ - ( - RTCIceTransportState::Unspecified, - ConnectionState::Unspecified, - ), - (RTCIceTransportState::New, ConnectionState::New), - (RTCIceTransportState::Checking, ConnectionState::Checking), - (RTCIceTransportState::Connected, ConnectionState::Connected), - (RTCIceTransportState::Completed, ConnectionState::Completed), - (RTCIceTransportState::Failed, ConnectionState::Failed), - ( - RTCIceTransportState::Disconnected, - ConnectionState::Disconnected, - ), - (RTCIceTransportState::Closed, ConnectionState::Closed), - ]; - - for (native, ice_state) in tests { - assert_eq!(native.to_ice(), ice_state); - assert_eq!(native, RTCIceTransportState::from(ice_state)); - } - } -} diff --git a/webrtc/src/ice_transport/ice_transport_test.rs b/webrtc/src/ice_transport/ice_transport_test.rs deleted file mode 100644 index 7f6511204..000000000 --- a/webrtc/src/ice_transport/ice_transport_test.rs +++ /dev/null @@ -1,122 +0,0 @@ -use portable_atomic::AtomicU32; -use tokio::time::Duration; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::media_engine::MediaEngine; -use crate::api::APIBuilder; -use crate::error::Result; -use crate::ice_transport::ice_connection_state::RTCIceConnectionState; -use crate::peer_connection::peer_connection_state::RTCPeerConnectionState; -use crate::peer_connection::peer_connection_test::{ - close_pair_now, new_pair, signal_pair, until_connection_state, -}; - -#[tokio::test] -async fn test_ice_transport_on_selected_candidate_pair_change() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_offer, mut pc_answer) = new_pair(&api).await?; - - let (ice_complete_tx, mut ice_complete_rx) = mpsc::channel::<()>(1); - let ice_complete_tx = Arc::new(Mutex::new(Some(ice_complete_tx))); - pc_answer.on_ice_connection_state_change(Box::new(move |ice_state: RTCIceConnectionState| { - let ice_complete_tx2 = Arc::clone(&ice_complete_tx); - Box::pin(async move { - if ice_state == RTCIceConnectionState::Connected { - tokio::time::sleep(Duration::from_secs(1)).await; - let mut done = ice_complete_tx2.lock().await; - done.take(); - } - }) - })); - - let sender_called_candidate_change = Arc::new(AtomicU32::new(0)); - let sender_called_candidate_change2 = Arc::clone(&sender_called_candidate_change); - pc_offer - .sctp() - .transport() - .ice_transport() - .on_selected_candidate_pair_change(Box::new(move |_: RTCIceCandidatePair| { - sender_called_candidate_change2.store(1, Ordering::SeqCst); - Box::pin(async {}) - })); - - signal_pair(&mut pc_offer, &mut pc_answer).await?; - - let _ = ice_complete_rx.recv().await; - assert_eq!( - sender_called_candidate_change.load(Ordering::SeqCst), - 1, - "Sender ICETransport OnSelectedCandidateChange was never called" - ); - - close_pair_now(&pc_offer, &pc_answer).await; - - Ok(()) -} - -#[tokio::test] -async fn test_ice_transport_get_selected_candidate_pair() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offerer, mut answerer) = new_pair(&api).await?; - - let peer_connection_connected = WaitGroup::new(); - until_connection_state( - &mut offerer, - &peer_connection_connected, - RTCPeerConnectionState::Connected, - ) - .await; - until_connection_state( - &mut answerer, - &peer_connection_connected, - RTCPeerConnectionState::Connected, - ) - .await; - - let offerer_selected_pair = offerer - .sctp() - .transport() - .ice_transport() - .get_selected_candidate_pair() - .await; - assert!(offerer_selected_pair.is_none()); - - let answerer_selected_pair = answerer - .sctp() - .transport() - .ice_transport() - .get_selected_candidate_pair() - .await; - assert!(answerer_selected_pair.is_none()); - - signal_pair(&mut offerer, &mut answerer).await?; - - peer_connection_connected.wait().await; - - let offerer_selected_pair = offerer - .sctp() - .transport() - .ice_transport() - .get_selected_candidate_pair() - .await; - assert!(offerer_selected_pair.is_some()); - - let answerer_selected_pair = answerer - .sctp() - .transport() - .ice_transport() - .get_selected_candidate_pair() - .await; - assert!(answerer_selected_pair.is_some()); - - close_pair_now(&offerer, &answerer).await; - - Ok(()) -} diff --git a/webrtc/src/ice_transport/mod.rs b/webrtc/src/ice_transport/mod.rs deleted file mode 100644 index e2b96da53..000000000 --- a/webrtc/src/ice_transport/mod.rs +++ /dev/null @@ -1,356 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use arc_swap::ArcSwapOption; -use ice::candidate::Candidate; -use ice::state::ConnectionState; -use ice_candidate::RTCIceCandidate; -use ice_candidate_pair::RTCIceCandidatePair; -use ice_gatherer::RTCIceGatherer; -use ice_role::RTCIceRole; -use portable_atomic::AtomicU8; -use tokio::sync::{mpsc, Mutex}; -use util::Conn; - -use crate::error::{flatten_errs, Error, Result}; -use crate::ice_transport::ice_parameters::RTCIceParameters; -use crate::ice_transport::ice_transport_state::RTCIceTransportState; -use crate::mux::endpoint::Endpoint; -use crate::mux::mux_func::MatchFunc; -use crate::mux::{Config, Mux}; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::ICETransportStats; -use crate::stats::StatsReportType::Transport; - -#[cfg(test)] -mod ice_transport_test; - -pub mod ice_candidate; -pub mod ice_candidate_pair; -pub mod ice_candidate_type; -pub mod ice_connection_state; -pub mod ice_credential_type; -pub mod ice_gatherer; -pub mod ice_gatherer_state; -pub mod ice_gathering_state; -pub mod ice_parameters; -pub mod ice_protocol; -pub mod ice_role; -pub mod ice_server; -pub mod ice_transport_state; - -pub type OnConnectionStateChangeHdlrFn = Box< - dyn (FnMut(RTCIceTransportState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnSelectedCandidatePairChangeHdlrFn = Box< - dyn (FnMut(RTCIceCandidatePair) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -#[derive(Default)] -struct ICETransportInternal { - role: RTCIceRole, - conn: Option>, //AgentConn - mux: Option, - cancel_tx: Option>, -} - -/// ICETransport allows an application access to information about the ICE -/// transport over which packets are sent and received. -#[derive(Default)] -pub struct RTCIceTransport { - pub(crate) gatherer: Arc, - on_connection_state_change_handler: Arc>>, - on_selected_candidate_pair_change_handler: - Arc>>, - state: Arc, // ICETransportState - internal: Mutex, -} - -impl RTCIceTransport { - /// creates a new new_icetransport. - pub(crate) fn new(gatherer: Arc) -> Self { - RTCIceTransport { - state: Arc::new(AtomicU8::new(RTCIceTransportState::New as u8)), - gatherer, - ..Default::default() - } - } - - /// get_selected_candidate_pair returns the selected candidate pair on which packets are sent - /// if there is no selected pair nil is returned - pub async fn get_selected_candidate_pair(&self) -> Option { - if let Some(agent) = self.gatherer.get_agent().await { - if let Some(ice_pair) = agent.get_selected_candidate_pair() { - let local = RTCIceCandidate::from(&ice_pair.local); - let remote = RTCIceCandidate::from(&ice_pair.remote); - return Some(RTCIceCandidatePair::new(local, remote)); - } - } - None - } - - /// Start incoming connectivity checks based on its configured role. - pub async fn start(&self, params: &RTCIceParameters, role: Option) -> Result<()> { - if self.state() != RTCIceTransportState::New { - return Err(Error::ErrICETransportNotInNew); - } - - self.ensure_gatherer().await?; - - if let Some(agent) = self.gatherer.get_agent().await { - let state = Arc::clone(&self.state); - - let on_connection_state_change_handler = - Arc::clone(&self.on_connection_state_change_handler); - agent.on_connection_state_change(Box::new(move |ice_state: ConnectionState| { - let s = RTCIceTransportState::from(ice_state); - let on_connection_state_change_handler_clone = - Arc::clone(&on_connection_state_change_handler); - state.store(s as u8, Ordering::SeqCst); - Box::pin(async move { - if let Some(handler) = &*on_connection_state_change_handler_clone.load() { - let mut f = handler.lock().await; - f(s).await; - } - }) - })); - - let on_selected_candidate_pair_change_handler = - Arc::clone(&self.on_selected_candidate_pair_change_handler); - agent.on_selected_candidate_pair_change(Box::new( - move |local: &Arc, - remote: &Arc| { - let on_selected_candidate_pair_change_handler_clone = - Arc::clone(&on_selected_candidate_pair_change_handler); - let local = RTCIceCandidate::from(local); - let remote = RTCIceCandidate::from(remote); - Box::pin(async move { - if let Some(handler) = - &*on_selected_candidate_pair_change_handler_clone.load() - { - let mut f = handler.lock().await; - f(RTCIceCandidatePair::new(local, remote)).await; - } - }) - }, - )); - - let role = if let Some(role) = role { - role - } else { - RTCIceRole::Controlled - }; - - let (cancel_tx, cancel_rx) = mpsc::channel(1); - { - let mut internal = self.internal.lock().await; - internal.role = role; - internal.cancel_tx = Some(cancel_tx); - } - - let conn: Arc = match role { - RTCIceRole::Controlling => { - agent - .dial( - cancel_rx, - params.username_fragment.clone(), - params.password.clone(), - ) - .await? - } - - RTCIceRole::Controlled => { - agent - .accept( - cancel_rx, - params.username_fragment.clone(), - params.password.clone(), - ) - .await? - } - - _ => return Err(Error::ErrICERoleUnknown), - }; - - let config = Config { - conn: Arc::clone(&conn), - buffer_size: self.gatherer.setting_engine.get_receive_mtu(), - }; - - { - let mut internal = self.internal.lock().await; - internal.conn = Some(conn); - internal.mux = Some(Mux::new(config)); - } - - Ok(()) - } else { - Err(Error::ErrICEAgentNotExist) - } - } - - /// restart is not exposed currently because ORTC has users create a whole new ICETransport - /// so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs - pub(crate) async fn restart(&self) -> Result<()> { - if let Some(agent) = self.gatherer.get_agent().await { - agent - .restart( - self.gatherer - .setting_engine - .candidates - .username_fragment - .clone(), - self.gatherer.setting_engine.candidates.password.clone(), - ) - .await?; - } else { - return Err(Error::ErrICEAgentNotExist); - } - self.gatherer.gather().await - } - - /// Stop irreversibly stops the ICETransport. - pub async fn stop(&self) -> Result<()> { - self.set_state(RTCIceTransportState::Closed); - - let mut errs: Vec = vec![]; - { - let mut internal = self.internal.lock().await; - internal.cancel_tx.take(); - if let Some(mut mux) = internal.mux.take() { - mux.close().await; - } - if let Some(conn) = internal.conn.take() { - if let Err(err) = conn.close().await { - errs.push(err.into()); - } - } - } - - if let Err(err) = self.gatherer.close().await { - errs.push(err); - } - - flatten_errs(errs) - } - - /// on_selected_candidate_pair_change sets a handler that is invoked when a new - /// ICE candidate pair is selected - pub fn on_selected_candidate_pair_change(&self, f: OnSelectedCandidatePairChangeHdlrFn) { - self.on_selected_candidate_pair_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_connection_state_change sets a handler that is fired when the ICE - /// connection state changes. - pub fn on_connection_state_change(&self, f: OnConnectionStateChangeHdlrFn) { - self.on_connection_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// Role indicates the current role of the ICE transport. - pub async fn role(&self) -> RTCIceRole { - let internal = self.internal.lock().await; - internal.role - } - - /// set_remote_candidates sets the sequence of candidates associated with the remote ICETransport. - pub async fn set_remote_candidates(&self, remote_candidates: &[RTCIceCandidate]) -> Result<()> { - self.ensure_gatherer().await?; - - if let Some(agent) = self.gatherer.get_agent().await { - for rc in remote_candidates { - let c: Arc = Arc::new(rc.to_ice()?); - agent.add_remote_candidate(&c)?; - } - Ok(()) - } else { - Err(Error::ErrICEAgentNotExist) - } - } - - /// adds a candidate associated with the remote ICETransport. - pub async fn add_remote_candidate( - &self, - remote_candidate: Option, - ) -> Result<()> { - self.ensure_gatherer().await?; - - if let Some(agent) = self.gatherer.get_agent().await { - if let Some(r) = remote_candidate { - let c: Arc = Arc::new(r.to_ice()?); - agent.add_remote_candidate(&c)?; - } - - Ok(()) - } else { - Err(Error::ErrICEAgentNotExist) - } - } - - /// State returns the current ice transport state. - pub fn state(&self) -> RTCIceTransportState { - RTCIceTransportState::from(self.state.load(Ordering::SeqCst)) - } - - pub(crate) fn set_state(&self, s: RTCIceTransportState) { - self.state.store(s as u8, Ordering::SeqCst) - } - - pub(crate) async fn new_endpoint(&self, f: MatchFunc) -> Option> { - let internal = self.internal.lock().await; - if let Some(mux) = &internal.mux { - Some(mux.new_endpoint(f).await) - } else { - None - } - } - - pub(crate) async fn ensure_gatherer(&self) -> Result<()> { - if self.gatherer.get_agent().await.is_none() { - self.gatherer.create_agent().await - } else { - Ok(()) - } - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - if let Some(agent) = self.gatherer.get_agent().await { - let stats = ICETransportStats::new("ice_transport".to_string(), agent); - - collector.insert("ice_transport".to_string(), Transport(stats)); - } - } - - pub(crate) async fn have_remote_credentials_change( - &self, - new_ufrag: &str, - new_pwd: &str, - ) -> bool { - if let Some(agent) = self.gatherer.get_agent().await { - let (ufrag, upwd) = agent.get_remote_user_credentials().await; - ufrag != new_ufrag || upwd != new_pwd - } else { - false - } - } - - pub(crate) async fn set_remote_credentials( - &self, - new_ufrag: String, - new_pwd: String, - ) -> Result<()> { - if let Some(agent) = self.gatherer.get_agent().await { - Ok(agent.set_remote_credentials(new_ufrag, new_pwd).await?) - } else { - Err(Error::ErrICEAgentNotExist) - } - } -} diff --git a/webrtc/src/lib.rs b/webrtc/src/lib.rs deleted file mode 100644 index a2decd674..000000000 --- a/webrtc/src/lib.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![warn(rust_2018_idioms)] -#![allow(dead_code)] - -pub use {data, dtls, ice, interceptor, mdns, media, rtcp, rtp, sctp, sdp, srtp, stun, turn, util}; - -/// [`peer_connection::RTCPeerConnection`] allows to establish connection between two peers given RTC configuration. Its API is similar to one in JavaScript. -pub mod peer_connection; - -/// The utilities defining transport between peers. Contains [`ice_transport::ice_server::RTCIceServer`] struct which describes how peer does ICE (Interactive Connectivity Establishment). -pub mod ice_transport; - -/// WebRTC DataChannel can be used for peer-to-peer transmitting arbitrary binary data. -pub mod data_channel; - -/// Module responsible for multiplexing data streams of different protocols on one socket. Custom [`mux::endpoint::Endpoint`] with [`mux::mux_func::MatchFunc`] can be used for parsing your application-specific byte stream. -pub mod mux; // TODO: why is this public? does someone really extend WebRTC stack? - -/// Measuring connection statistics, such as amount of data transmitted or round trip time. -pub mod stats; - -/// [`Error`] enumerates WebRTC problems, [`error::OnErrorHdlrFn`] defines type for callback-logger. -pub mod error; - -/// Set of constructors for WebRTC primitives. Subject to deprecation in future. -pub mod api; - -pub mod dtls_transport; -pub mod rtp_transceiver; -pub mod sctp_transport; -pub mod track; - -pub use error::Error; - -#[macro_use] -extern crate lazy_static; - -pub(crate) const UNSPECIFIED_STR: &str = "Unspecified"; - -/// Equal to UDP MTU -pub(crate) const RECEIVE_MTU: usize = 1460; - -pub(crate) const SDP_ATTRIBUTE_RID: &str = "rid"; -pub(crate) const SDP_ATTRIBUTE_SIMULCAST: &str = "simulcast"; -pub(crate) const GENERATED_CERTIFICATE_ORIGIN: &str = "WebRTC"; diff --git a/webrtc/src/mux/endpoint.rs b/webrtc/src/mux/endpoint.rs deleted file mode 100644 index ac01f4027..000000000 --- a/webrtc/src/mux/endpoint.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::collections::HashMap; -use std::io; -use std::net::SocketAddr; -use std::sync::Arc; - -use async_trait::async_trait; -use tokio::sync::Mutex; -use util::{Buffer, Conn}; - -use crate::mux::mux_func::MatchFunc; - -/// Endpoint implements net.Conn. It is used to read muxed packets. -pub struct Endpoint { - pub(crate) id: usize, - pub(crate) buffer: Buffer, - pub(crate) match_fn: MatchFunc, - pub(crate) next_conn: Arc, - pub(crate) endpoints: Arc>>>, -} - -impl Endpoint { - /// Close unregisters the endpoint from the Mux - pub async fn close(&self) -> Result<()> { - self.buffer.close().await; - - let mut endpoints = self.endpoints.lock().await; - endpoints.remove(&self.id); - - Ok(()) - } -} - -type Result = std::result::Result; - -#[async_trait] -impl Conn for Endpoint { - async fn connect(&self, _addr: SocketAddr) -> Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - /// reads a packet of len(p) bytes from the underlying conn - /// that are matched by the associated MuxFunc - async fn recv(&self, buf: &mut [u8]) -> Result { - match self.buffer.read(buf, None).await { - Ok(n) => Ok(n), - Err(err) => Err(io::Error::new(io::ErrorKind::Other, err.to_string()).into()), - } - } - async fn recv_from(&self, _buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - /// writes bytes to the underlying conn - async fn send(&self, buf: &[u8]) -> Result { - self.next_conn.send(buf).await - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> Result { - self.next_conn.local_addr() - } - - fn remote_addr(&self) -> Option { - self.next_conn.remote_addr() - } - - async fn close(&self) -> Result<()> { - self.next_conn.close().await - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} diff --git a/webrtc/src/mux/mod.rs b/webrtc/src/mux/mod.rs deleted file mode 100644 index 876150900..000000000 --- a/webrtc/src/mux/mod.rs +++ /dev/null @@ -1,157 +0,0 @@ -#[cfg(test)] -mod mux_test; - -pub mod endpoint; -pub mod mux_func; - -use std::collections::HashMap; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicUsize; -use tokio::sync::{mpsc, Mutex}; -use util::{Buffer, Conn}; - -use crate::error::Result; -use crate::mux::endpoint::Endpoint; -use crate::mux::mux_func::MatchFunc; -use crate::util::Error; - -/// mux multiplexes packets on a single socket (RFC7983) - -/// The maximum amount of data that can be buffered before returning errors. -const MAX_BUFFER_SIZE: usize = 1000 * 1000; // 1MB - -/// Config collects the arguments to mux.Mux construction into -/// a single structure -pub struct Config { - pub conn: Arc, - pub buffer_size: usize, -} - -/// Mux allows multiplexing -#[derive(Clone)] -pub struct Mux { - id: Arc, - next_conn: Arc, - endpoints: Arc>>>, - buffer_size: usize, - closed_ch_tx: Option>, -} - -impl Mux { - pub fn new(config: Config) -> Self { - let (closed_ch_tx, closed_ch_rx) = mpsc::channel(1); - let m = Mux { - id: Arc::new(AtomicUsize::new(0)), - next_conn: Arc::clone(&config.conn), - endpoints: Arc::new(Mutex::new(HashMap::new())), - buffer_size: config.buffer_size, - closed_ch_tx: Some(closed_ch_tx), - }; - - let buffer_size = m.buffer_size; - let next_conn = Arc::clone(&m.next_conn); - let endpoints = Arc::clone(&m.endpoints); - tokio::spawn(async move { - Mux::read_loop(buffer_size, next_conn, closed_ch_rx, endpoints).await; - }); - - m - } - - /// creates a new Endpoint - pub async fn new_endpoint(&self, f: MatchFunc) -> Arc { - let mut endpoints = self.endpoints.lock().await; - - let id = self.id.fetch_add(1, Ordering::SeqCst); - // Set a maximum size of the buffer in bytes. - let e = Arc::new(Endpoint { - id, - buffer: Buffer::new(0, MAX_BUFFER_SIZE), - match_fn: f, - next_conn: Arc::clone(&self.next_conn), - endpoints: Arc::clone(&self.endpoints), - }); - - endpoints.insert(e.id, Arc::clone(&e)); - - e - } - - /// remove_endpoint removes an endpoint from the Mux - pub async fn remove_endpoint(&mut self, e: &Endpoint) { - let mut endpoints = self.endpoints.lock().await; - endpoints.remove(&e.id); - } - - /// Close closes the Mux and all associated Endpoints. - pub async fn close(&mut self) { - self.closed_ch_tx.take(); - - let mut endpoints = self.endpoints.lock().await; - endpoints.clear(); - } - - async fn read_loop( - buffer_size: usize, - next_conn: Arc, - mut closed_ch_rx: mpsc::Receiver<()>, - endpoints: Arc>>>, - ) { - let mut buf = vec![0u8; buffer_size]; - let mut n = 0usize; - loop { - tokio::select! { - _ = closed_ch_rx.recv() => break, - result = next_conn.recv(&mut buf) => { - if let Ok(m) = result{ - n = m; - } - } - }; - - if let Err(err) = Mux::dispatch(&buf[..n], &endpoints).await { - log::error!("mux: ending readLoop dispatch error {:?}", err); - break; - } - } - } - - async fn dispatch( - buf: &[u8], - endpoints: &Arc>>>, - ) -> Result<()> { - let mut endpoint = None; - - { - let eps = endpoints.lock().await; - for ep in eps.values() { - if (ep.match_fn)(buf) { - endpoint = Some(Arc::clone(ep)); - break; - } - } - } - - if let Some(ep) = endpoint { - match ep.buffer.write(buf).await { - // Expected when bytes are received faster than the endpoint can process them - Err(Error::ErrBufferFull) => { - log::info!("mux: endpoint buffer is full, dropping packet") - } - Ok(_) => (), - Err(e) => return Err(crate::Error::Util(e)), - } - } else if !buf.is_empty() { - log::warn!( - "Warning: mux: no endpoint for packet starting with {}", - buf[0] - ); - } else { - log::warn!("Warning: mux: no endpoint for zero length packet"); - } - - Ok(()) - } -} diff --git a/webrtc/src/mux/mux_func.rs b/webrtc/src/mux/mux_func.rs deleted file mode 100644 index dfc30eefc..000000000 --- a/webrtc/src/mux/mux_func.rs +++ /dev/null @@ -1,63 +0,0 @@ -/// MatchFunc allows custom logic for mapping packets to an Endpoint -pub type MatchFunc = Box bool) + Send + Sync>; - -/// match_all always returns true -pub fn match_all(_b: &[u8]) -> bool { - true -} - -/// match_range is a MatchFunc that accepts packets with the first byte in [lower..upper] -pub fn match_range(lower: u8, upper: u8) -> MatchFunc { - Box::new(move |buf: &[u8]| -> bool { - if buf.is_empty() { - return false; - } - let b = buf[0]; - b >= lower && b <= upper - }) -} - -/// MatchFuncs as described in RFC7983 -/// -/// +----------------+ -/// | [0..3] -+--> forward to STUN -/// | | -/// | [16..19] -+--> forward to ZRTP -/// | | -/// packet --> | [20..63] -+--> forward to DTLS -/// | | -/// | [64..79] -+--> forward to TURN Channel -/// | | -/// | [128..191] -+--> forward to RTP/RTCP -/// +----------------+ -/// match_dtls is a MatchFunc that accepts packets with the first byte in [20..63] -/// as defined in RFC7983 -pub fn match_dtls(b: &[u8]) -> bool { - match_range(20, 63)(b) -} - -// match_srtp_or_srtcp is a MatchFunc that accepts packets with the first byte in [128..191] -// as defined in RFC7983 -pub fn match_srtp_or_srtcp(b: &[u8]) -> bool { - match_range(128, 191)(b) -} - -pub(crate) fn is_rtcp(buf: &[u8]) -> bool { - // Not long enough to determine RTP/RTCP - if buf.len() < 4 { - return false; - } - - let rtcp_packet_type = buf[1]; - (192..=223).contains(&rtcp_packet_type) -} - -/// match_srtp is a MatchFunc that only matches SRTP and not SRTCP -pub fn match_srtp(buf: &[u8]) -> bool { - match_srtp_or_srtcp(buf) && !is_rtcp(buf) -} - -/// match_srtcp is a MatchFunc that only matches SRTCP and not SRTP -pub fn match_srtcp(buf: &[u8]) -> bool { - match_srtp_or_srtcp(buf) && is_rtcp(buf) -} diff --git a/webrtc/src/mux/mux_test.rs b/webrtc/src/mux/mux_test.rs deleted file mode 100644 index e9a8b5e0a..000000000 --- a/webrtc/src/mux/mux_test.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::io; -use std::net::SocketAddr; -use std::sync::atomic::Ordering; - -use async_trait::async_trait; -use portable_atomic::AtomicUsize; -use util::conn::conn_pipe::pipe; - -use super::*; -use crate::mux::mux_func::{match_all, match_srtp}; - -const TEST_PIPE_BUFFER_SIZE: usize = 8192; - -#[tokio::test] -async fn test_no_endpoints() -> crate::error::Result<()> { - // In memory pipe - let (ca, _) = pipe(); - - let mut m = Mux::new(Config { - conn: Arc::new(ca), - buffer_size: TEST_PIPE_BUFFER_SIZE, - }); - - Mux::dispatch(&[0], &m.endpoints).await?; - m.close().await; - - Ok(()) -} - -struct MuxErrorConn { - idx: AtomicUsize, - data: Vec>, -} - -type Result = std::result::Result; - -#[async_trait] -impl Conn for MuxErrorConn { - async fn connect(&self, _addr: SocketAddr) -> Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn recv(&self, buf: &mut [u8]) -> Result { - let idx = self.idx.fetch_add(1, Ordering::SeqCst); - if idx < self.data.len() { - let n = std::cmp::min(buf.len(), self.data[idx].len()); - buf[..n].copy_from_slice(&self.data[idx][..n]); - Ok(n) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - format!("idx {} >= data.len {}", idx, self.data.len()), - ) - .into()) - } - } - - async fn recv_from(&self, _buf: &mut [u8]) -> Result<(usize, SocketAddr)> { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn send(&self, _buf: &[u8]) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn local_addr(&self) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "Not applicable").into()) - } - - fn remote_addr(&self) -> Option { - None - } - - async fn close(&self) -> Result<()> { - Ok(()) - } - - fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { - self - } -} - -#[tokio::test] -async fn test_non_fatal_read() -> Result<()> { - let expected_data = b"expected_data".to_vec(); - - let conn = Arc::new(MuxErrorConn { - idx: AtomicUsize::new(0), - data: vec![ - expected_data.clone(), - expected_data.clone(), - expected_data.clone(), - ], - }); - - let mut m = Mux::new(Config { - conn, - buffer_size: TEST_PIPE_BUFFER_SIZE, - }); - - let e = m.new_endpoint(Box::new(match_all)).await; - let mut buff = vec![0u8; TEST_PIPE_BUFFER_SIZE]; - - let n = e.recv(&mut buff).await?; - assert_eq!(&buff[..n], expected_data); - - let n = e.recv(&mut buff).await?; - assert_eq!(&buff[..n], expected_data); - - let n = e.recv(&mut buff).await?; - assert_eq!(&buff[..n], expected_data); - - m.close().await; - - Ok(()) -} - -#[tokio::test] -async fn test_non_fatal_dispatch() -> Result<()> { - let (ca, cb) = pipe(); - - let mut m = Mux::new(Config { - conn: Arc::new(ca), - buffer_size: TEST_PIPE_BUFFER_SIZE, - }); - - let e = m.new_endpoint(Box::new(match_srtp)).await; - e.buffer.set_limit_size(1).await; - - for _ in 0..25 { - let srtp_packet = [128, 1, 2, 3, 4].to_vec(); - cb.send(&srtp_packet).await?; - } - - m.close().await; - - Ok(()) -} diff --git a/webrtc/src/peer_connection/certificate.rs b/webrtc/src/peer_connection/certificate.rs deleted file mode 100644 index 5f9b962a9..000000000 --- a/webrtc/src/peer_connection/certificate.rs +++ /dev/null @@ -1,291 +0,0 @@ -use std::ops::Add; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind}; -use rcgen::{CertificateParams, KeyPair}; -use ring::rand::SystemRandom; -use ring::rsa; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; -use sha2::{Digest, Sha256}; - -use crate::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint; -use crate::error::{Error, Result}; -use crate::peer_connection::math_rand_alpha; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::{CertificateStats, StatsReportType}; - -/// Certificate represents a X.509 certificate used to authenticate WebRTC communications. -#[derive(Clone, Debug)] -pub struct RTCCertificate { - /// DTLS certificate. - pub(crate) dtls_certificate: dtls::crypto::Certificate, - /// Timestamp after which this certificate is no longer valid. - pub(crate) expires: SystemTime, - /// Certificate's ID used for statistics. - /// - /// Example: "certificate-1667202302853538793" - /// - /// See [`CertificateStats`]. - pub(crate) stats_id: String, -} - -impl PartialEq for RTCCertificate { - fn eq(&self, other: &Self) -> bool { - self.dtls_certificate == other.dtls_certificate - } -} - -impl RTCCertificate { - /// Generates a new certificate from the given parameters. - /// - /// See [`rcgen::Certificate::from_params`]. - fn from_params(params: CertificateParams, key_pair: KeyPair) -> Result { - let not_after = params.not_after; - - let x509_cert = params.self_signed(&key_pair).unwrap(); - let serialized_der = key_pair.serialize_der(); - - let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::new(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - &SystemRandom::new(), - ) - .map_err(|e| Error::new(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::new(e.to_string()))?, - ), - serialized_der, - } - } else { - return Err(Error::new("Unsupported key_pair".to_owned())); - }; - - let expires = if cfg!(target_arch = "arm") { - // Workaround for issue overflow when adding duration to instant on armv7 - // https://github.com/webrtc-rs/examples/issues/5 https://github.com/chronotope/chrono/issues/343 - SystemTime::now().add(Duration::from_secs(172800)) //60*60*48 or 2 days - } else { - not_after.into() - }; - - Ok(Self { - dtls_certificate: dtls::crypto::Certificate { - certificate: vec![x509_cert.der().to_owned()], - private_key, - }, - expires, - stats_id: gen_stats_id(), - }) - } - - /// Generates a new certificate with default [`CertificateParams`] using the given keypair. - pub fn from_key_pair(key_pair: KeyPair) -> Result { - if !(key_pair.is_compatible(&rcgen::PKCS_ED25519) - || key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) - || key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256)) - { - return Err(Error::new("Unsupported key_pair".to_owned())); - } - - RTCCertificate::from_params( - CertificateParams::new(vec![math_rand_alpha(16)]).unwrap(), - key_pair, - ) - } - - /// Parses a certificate from the ASCII PEM format. - #[cfg(feature = "pem")] - pub fn from_pem(pem_str: &str) -> Result { - let mut pem_blocks = pem_str.split("\n\n"); - let first_block = if let Some(b) = pem_blocks.next() { - b - } else { - return Err(Error::InvalidPEM("empty PEM".into())); - }; - let expires_pem = - pem::parse(first_block).map_err(|e| Error::new(format!("can't parse PEM: {e}")))?; - if expires_pem.tag() != "EXPIRES" { - return Err(Error::InvalidPEM(format!( - "invalid tag (expected: 'EXPIRES', got '{}')", - expires_pem.tag() - ))); - } - let mut bytes = [0u8; 8]; - bytes.copy_from_slice(&expires_pem.contents()[..8]); - let expires = if let Some(e) = - SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(u64::from_le_bytes(bytes))) - { - e - } else { - return Err(Error::InvalidPEM("failed to calculate SystemTime".into())); - }; - let dtls_certificate = - dtls::crypto::Certificate::from_pem(&pem_blocks.collect::>().join("\n\n"))?; - Ok(RTCCertificate::from_existing(dtls_certificate, expires)) - } - - /// Builds a [`RTCCertificate`] using the existing DTLS certificate. - /// - /// Use this method when you have a persistent certificate (i.e. you don't want to generate a - /// new one for each DTLS connection). - /// - /// NOTE: ID used for statistics will be different as it's neither derived from the given - /// certificate nor persisted along it when using [`RTCCertificate::serialize_pem`]. - pub fn from_existing(dtls_certificate: dtls::crypto::Certificate, expires: SystemTime) -> Self { - Self { - dtls_certificate, - expires, - // TODO: figure out if it needs to be persisted - stats_id: gen_stats_id(), - } - } - - /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. - #[cfg(any(doc, feature = "pem"))] - pub fn serialize_pem(&self) -> String { - // Encode `expires` as a PEM block. - // - // TODO: serialize as nanos when https://github.com/rust-lang/rust/issues/103332 is fixed. - let expires_pem = pem::Pem::new( - "EXPIRES".to_string(), - self.expires - .duration_since(SystemTime::UNIX_EPOCH) - .expect("expires to be valid") - .as_secs() - .to_le_bytes() - .to_vec(), - ); - format!( - "{}\n{}", - pem::encode(&expires_pem), - self.dtls_certificate.serialize_pem() - ) - } - - /// get_fingerprints returns a SHA-256 fingerprint of this certificate. - /// - /// TODO: return a fingerprint computed with the digest algorithm used in the certificate - /// signature. - pub fn get_fingerprints(&self) -> Vec { - let mut fingerprints = Vec::new(); - - for c in &self.dtls_certificate.certificate { - let mut h = Sha256::new(); - h.update(c.as_ref()); - let hashed = h.finalize(); - let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); - - fingerprints.push(RTCDtlsFingerprint { - algorithm: "sha-256".to_owned(), - value: values.join(":"), - }); - } - - fingerprints - } - - pub(crate) async fn collect_stats(&self, collector: &StatsCollector) { - if let Some(fingerprint) = self.get_fingerprints().into_iter().next() { - let stats = CertificateStats::new(self, fingerprint); - collector.insert( - self.stats_id.clone(), - StatsReportType::CertificateStats(stats), - ); - } - } -} - -fn gen_stats_id() -> String { - format!( - "certificate-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() as u64 - ) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_generate_certificate_rsa() -> Result<()> { - let key_pair = KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256); - assert!(key_pair.is_err(), "RcgenError::KeyGenerationUnavailable"); - - Ok(()) - } - - #[test] - fn test_generate_certificate_ecdsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let _cert = RTCCertificate::from_key_pair(kp)?; - - Ok(()) - } - - #[test] - fn test_generate_certificate_eddsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; - let _cert = RTCCertificate::from_key_pair(kp)?; - - Ok(()) - } - - #[test] - fn test_certificate_equal() -> Result<()> { - let kp1 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert1 = RTCCertificate::from_key_pair(kp1)?; - - let kp2 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert2 = RTCCertificate::from_key_pair(kp2)?; - - assert_ne!(cert1, cert2); - - Ok(()) - } - - #[test] - fn test_generate_certificate_expires_and_stats_id() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - - let now = SystemTime::now(); - assert!(cert.expires.duration_since(now).is_ok()); - assert!(cert.stats_id.contains("certificate")); - - Ok(()) - } - - #[cfg(feature = "pem")] - #[test] - fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - - let pem = cert.serialize_pem(); - let loaded_cert = RTCCertificate::from_pem(&pem)?; - - assert_eq!(loaded_cert, cert); - - Ok(()) - } -} diff --git a/webrtc/src/peer_connection/configuration.rs b/webrtc/src/peer_connection/configuration.rs deleted file mode 100644 index 58793fb53..000000000 --- a/webrtc/src/peer_connection/configuration.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::ice_transport::ice_server::RTCIceServer; -use crate::peer_connection::certificate::RTCCertificate; -use crate::peer_connection::policy::bundle_policy::RTCBundlePolicy; -use crate::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy; -use crate::peer_connection::policy::rtcp_mux_policy::RTCRtcpMuxPolicy; - -/// A Configuration defines how peer-to-peer communication via PeerConnection -/// is established or re-established. -/// Configurations may be set up once and reused across multiple connections. -/// Configurations are treated as readonly. As long as they are unmodified, -/// they are safe for concurrent use. -#[derive(Default, Clone)] -pub struct RTCConfiguration { - /// iceservers defines a slice describing servers available to be used by - /// ICE, such as STUN and TURN servers. - pub ice_servers: Vec, - - /// icetransport_policy indicates which candidates the ICEAgent is allowed - /// to use. - pub ice_transport_policy: RTCIceTransportPolicy, - - /// bundle_policy indicates which media-bundling policy to use when gathering - /// ICE candidates. - pub bundle_policy: RTCBundlePolicy, - - /// rtcp_mux_policy indicates which rtcp-mux policy to use when gathering ICE - /// candidates. - pub rtcp_mux_policy: RTCRtcpMuxPolicy, - - /// peer_identity sets the target peer identity for the PeerConnection. - /// The PeerConnection will not establish a connection to a remote peer - /// unless it can be successfully authenticated with the provided name. - pub peer_identity: String, - - /// Certificates describes a set of certificates that the PeerConnection - /// uses to authenticate. Valid values for this parameter are created - /// through calls to the generate_certificate function. Although any given - /// DTLS connection will use only one certificate, this attribute allows the - /// caller to provide multiple certificates that support different - /// algorithms. The final certificate will be selected based on the DTLS - /// handshake, which establishes which certificates are allowed. The - /// PeerConnection implementation selects which of the certificates is - /// used for a given connection; how certificates are selected is outside - /// the scope of this specification. If this value is absent, then a default - /// set of certificates is generated for each PeerConnection instance. - pub certificates: Vec, - - /// icecandidate_pool_size describes the size of the prefetched ICE pool. - pub ice_candidate_pool_size: u8, -} - -impl RTCConfiguration { - /// get_iceservers side-steps the strict parsing mode of the ice package - /// (as defined in https://tools.ietf.org/html/rfc7064) by copying and then - /// stripping any erroneous queries from "stun(s):" URLs before parsing. - #[allow(clippy::assigning_clones)] - pub(crate) fn get_ice_servers(&self) -> Vec { - let mut ice_servers = self.ice_servers.clone(); - - for ice_server in &mut ice_servers { - for raw_url in &mut ice_server.urls { - if raw_url.starts_with("stun") { - // strip the query from "stun(s):" if present - let parts: Vec<&str> = raw_url.split('?').collect(); - *raw_url = parts[0].to_owned(); - } - } - } - - ice_servers - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_configuration_get_iceservers() { - { - let expected_server_str = "stun:stun.l.google.com:19302"; - let cfg = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec![expected_server_str.to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - let parsed_urls = cfg.get_ice_servers(); - assert_eq!(parsed_urls[0].urls[0], expected_server_str); - } - - { - // ignore the fact that stun URLs shouldn't have a query - let server_str = "stun:global.stun.twilio.com:3478?transport=udp"; - let expected_server_str = "stun:global.stun.twilio.com:3478"; - let cfg = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec![server_str.to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - let parsed_urls = cfg.get_ice_servers(); - assert_eq!(parsed_urls[0].urls[0], expected_server_str); - } - } - - /*TODO:#[test] fn test_configuration_json() { - - let j = r#" - { - "iceServers": [{"URLs": ["turn:turn.example.org"], - "username": "jch", - "credential": "topsecret" - }], - "iceTransportPolicy": "relay", - "bundlePolicy": "balanced", - "rtcpMuxPolicy": "require" - }"#; - - conf := Configuration{ - ICEServers: []ICEServer{ - { - URLs: []string{"turn:turn.example.org"}, - Username: "jch", - Credential: "topsecret", - }, - }, - ICETransportPolicy: ICETransportPolicyRelay, - BundlePolicy: BundlePolicyBalanced, - RTCPMuxPolicy: RTCPMuxPolicyRequire, - } - - var conf2 Configuration - assert.NoError(t, json.Unmarshal([]byte(j), &conf2)) - assert.Equal(t, conf, conf2) - - j2, err := json.Marshal(conf2) - assert.NoError(t, err) - - var conf3 Configuration - assert.NoError(t, json.Unmarshal(j2, &conf3)) - assert.Equal(t, conf2, conf3) - }*/ -} diff --git a/webrtc/src/peer_connection/mod.rs b/webrtc/src/peer_connection/mod.rs deleted file mode 100644 index e5cec0894..000000000 --- a/webrtc/src/peer_connection/mod.rs +++ /dev/null @@ -1,2118 +0,0 @@ -#[cfg(test)] -pub(crate) mod peer_connection_test; - -/// Custom media-related options, such as `voice_activity_detection`, which are negotiated while establishing connection. -pub mod offer_answer_options; - -/// [`RTCSessionDescription`] - wrapper for SDP text and negotiations stage ([`RTCSdpType`]: offer - pranswer - answer - rollback). -pub mod sdp; - -pub mod certificate; -pub mod configuration; -pub(crate) mod operation; -mod peer_connection_internal; -pub mod peer_connection_state; -pub mod policy; -pub mod signaling_state; - -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; - -use ::ice::candidate::candidate_base::unmarshal_candidate; -use ::ice::candidate::Candidate; -use ::sdp::description::session::*; -use ::sdp::util::ConnectionRole; -use arc_swap::ArcSwapOption; -use async_trait::async_trait; -use interceptor::{stats, Attributes, Interceptor, RTCPWriter}; -use peer_connection_internal::*; -use portable_atomic::{AtomicBool, AtomicU64, AtomicU8}; -use rand::{thread_rng, Rng}; -use rcgen::KeyPair; -use smol_str::SmolStr; -use srtp::stream::Stream; -use tokio::sync::{mpsc, Mutex}; - -use crate::api::media_engine::MediaEngine; -use crate::api::setting_engine::SettingEngine; -use crate::api::API; -use crate::data_channel::data_channel_init::RTCDataChannelInit; -use crate::data_channel::data_channel_parameters::DataChannelParameters; -use crate::data_channel::data_channel_state::RTCDataChannelState; -use crate::data_channel::RTCDataChannel; -use crate::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint; -use crate::dtls_transport::dtls_parameters::DTLSParameters; -use crate::dtls_transport::dtls_role::{ - DTLSRole, DEFAULT_DTLS_ROLE_ANSWER, DEFAULT_DTLS_ROLE_OFFER, -}; -use crate::dtls_transport::dtls_transport_state::RTCDtlsTransportState; -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::{flatten_errs, Error, Result}; -use crate::ice_transport::ice_candidate::{RTCIceCandidate, RTCIceCandidateInit}; -use crate::ice_transport::ice_connection_state::RTCIceConnectionState; -use crate::ice_transport::ice_gatherer::{ - OnGatheringCompleteHdlrFn, OnICEGathererStateChangeHdlrFn, OnLocalCandidateHdlrFn, - RTCIceGatherOptions, RTCIceGatherer, -}; -use crate::ice_transport::ice_gatherer_state::RTCIceGathererState; -use crate::ice_transport::ice_gathering_state::RTCIceGatheringState; -use crate::ice_transport::ice_parameters::RTCIceParameters; -use crate::ice_transport::ice_role::RTCIceRole; -use crate::ice_transport::ice_transport_state::RTCIceTransportState; -use crate::ice_transport::RTCIceTransport; -use crate::peer_connection::certificate::RTCCertificate; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::offer_answer_options::{RTCAnswerOptions, RTCOfferOptions}; -use crate::peer_connection::operation::{Operation, Operations}; -use crate::peer_connection::peer_connection_state::{ - NegotiationNeededState, RTCPeerConnectionState, -}; -use crate::peer_connection::sdp::sdp_type::RTCSdpType; -use crate::peer_connection::sdp::session_description::RTCSessionDescription; -use crate::peer_connection::sdp::*; -use crate::peer_connection::signaling_state::{ - check_next_signaling_state, RTCSignalingState, StateChangeOp, -}; -use crate::rtp_transceiver::rtp_codec::{RTCRtpHeaderExtensionCapability, RTPCodecType}; -use crate::rtp_transceiver::rtp_receiver::RTCRtpReceiver; -use crate::rtp_transceiver::rtp_sender::RTCRtpSender; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::rtp_transceiver::{ - find_by_mid, handle_unknown_rtp_packet, satisfy_type_and_direction, RTCRtpTransceiver, - RTCRtpTransceiverInit, SSRC, -}; -use crate::sctp_transport::sctp_transport_capabilities::SCTPTransportCapabilities; -use crate::sctp_transport::sctp_transport_state::RTCSctpTransportState; -use crate::sctp_transport::RTCSctpTransport; -use crate::stats::StatsReport; -use crate::track::track_local::TrackLocal; -use crate::track::track_remote::TrackRemote; - -/// SIMULCAST_PROBE_COUNT is the amount of RTP Packets -/// that handleUndeclaredSSRC will read and try to dispatch from -/// mid and rid values -pub(crate) const SIMULCAST_PROBE_COUNT: usize = 10; - -/// SIMULCAST_MAX_PROBE_ROUTINES is how many active routines can be used to probe -/// If the total amount of incoming SSRCes exceeds this new requests will be ignored -pub(crate) const SIMULCAST_MAX_PROBE_ROUTINES: u64 = 25; - -pub(crate) const MEDIA_SECTION_APPLICATION: &str = "application"; - -const RUNES_ALPHA: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -/// math_rand_alpha generates a mathematical random alphabet sequence of the requested length. -pub fn math_rand_alpha(n: usize) -> String { - let mut rng = thread_rng(); - - let rand_string: String = (0..n) - .map(|_| { - let idx = rng.gen_range(0..RUNES_ALPHA.len()); - RUNES_ALPHA[idx] as char - }) - .collect(); - - rand_string -} - -pub type OnSignalingStateChangeHdlrFn = Box< - dyn (FnMut(RTCSignalingState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnICEConnectionStateChangeHdlrFn = Box< - dyn (FnMut(RTCIceConnectionState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnPeerConnectionStateChangeHdlrFn = Box< - dyn (FnMut(RTCPeerConnectionState) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnDataChannelHdlrFn = Box< - dyn (FnMut(Arc) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnTrackHdlrFn = Box< - dyn (FnMut( - Arc, - Arc, - Arc, - ) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnNegotiationNeededHdlrFn = - Box Pin + Send + 'static>>) + Send + Sync>; - -#[derive(Clone)] -struct StartTransportsParams { - ice_transport: Arc, - dtls_transport: Arc, - on_peer_connection_state_change_handler: Arc>>, - is_closed: Arc, - peer_connection_state: Arc, - ice_connection_state: Arc, -} - -#[derive(Clone)] -struct CheckNegotiationNeededParams { - sctp_transport: Arc, - rtp_transceivers: Arc>>>, - current_local_description: Arc>>, - current_remote_description: Arc>>, -} - -#[derive(Clone)] -struct NegotiationNeededParams { - on_negotiation_needed_handler: Arc>>, - is_closed: Arc, - ops: Arc, - negotiation_needed_state: Arc, - is_negotiation_needed: Arc, - signaling_state: Arc, - check_negotiation_needed_params: CheckNegotiationNeededParams, -} - -/// PeerConnection represents a WebRTC connection that establishes a -/// peer-to-peer communications with another PeerConnection instance in a -/// browser, or to another endpoint implementing the required protocols. -pub struct RTCPeerConnection { - stats_id: String, - idp_login_url: Option, - - configuration: Mutex, - - interceptor_rtcp_writer: Arc, - - interceptor: Arc, - - pub(crate) internal: Arc, -} - -impl std::fmt::Debug for RTCPeerConnection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RTCPeerConnection") - .field("stats_id", &self.stats_id) - .field("idp_login_url", &self.idp_login_url) - .field("signaling_state", &self.signaling_state()) - .field("ice_connection_state", &self.ice_connection_state()) - .finish() - } -} - -impl std::fmt::Display for RTCPeerConnection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "(RTCPeerConnection {})", self.stats_id) - } -} - -impl RTCPeerConnection { - /// creates a PeerConnection with the default codecs and - /// interceptors. See register_default_codecs and register_default_interceptors. - /// - /// If you wish to customize the set of available codecs or the set of - /// active interceptors, create a MediaEngine and call api.new_peer_connection - /// instead of this function. - pub(crate) async fn new(api: &API, mut configuration: RTCConfiguration) -> Result { - RTCPeerConnection::init_configuration(&mut configuration)?; - - let (interceptor, stats_interceptor): (Arc, _) = { - let mut chain = api.interceptor_registry.build_chain("")?; - let stats_interceptor = stats::make_stats_interceptor(""); - chain.add(stats_interceptor.clone()); - - (Arc::new(chain), stats_interceptor) - }; - - let weak_interceptor = Arc::downgrade(&interceptor); - let (internal, configuration) = - PeerConnectionInternal::new(api, weak_interceptor, stats_interceptor, configuration) - .await?; - let internal_rtcp_writer = Arc::clone(&internal) as Arc; - let interceptor_rtcp_writer = interceptor.bind_rtcp_writer(internal_rtcp_writer).await; - - // (Step #2) - // Some variables defined explicitly despite their implicit zero values to - // allow better readability to understand what is happening. - Ok(RTCPeerConnection { - stats_id: format!( - "PeerConnection-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - ), - interceptor, - interceptor_rtcp_writer, - internal, - configuration: Mutex::new(configuration), - idp_login_url: None, - }) - } - - /// init_configuration defines validation of the specified Configuration and - /// its assignment to the internal configuration variable. This function differs - /// from its set_configuration counterpart because most of the checks do not - /// include verification statements related to the existing state. Thus the - /// function describes only minor verification of some the struct variables. - fn init_configuration(configuration: &mut RTCConfiguration) -> Result<()> { - let sanitized_ice_servers = configuration.get_ice_servers(); - if !sanitized_ice_servers.is_empty() { - for server in &sanitized_ice_servers { - server.validate()?; - } - } - - // (step #3) - if !configuration.certificates.is_empty() { - let now = SystemTime::now(); - for cert in &configuration.certificates { - cert.expires - .duration_since(now) - .map_err(|_| Error::ErrCertificateExpired)?; - } - } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - configuration.certificates = vec![cert]; - }; - - Ok(()) - } - - /// on_signaling_state_change sets an event handler which is invoked when the - /// peer connection's signaling state changes - pub fn on_signaling_state_change(&self, f: OnSignalingStateChangeHdlrFn) { - self.internal - .on_signaling_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))) - } - - async fn do_signaling_state_change(&self, new_state: RTCSignalingState) { - log::info!("signaling state changed to {}", new_state); - if let Some(handler) = &*self.internal.on_signaling_state_change_handler.load() { - let mut f = handler.lock().await; - f(new_state).await; - } - } - - /// on_data_channel sets an event handler which is invoked when a data - /// channel message arrives from a remote peer. - pub fn on_data_channel(&self, f: OnDataChannelHdlrFn) { - self.internal - .on_data_channel_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_negotiation_needed sets an event handler which is invoked when - /// a change has occurred which requires session negotiation - pub fn on_negotiation_needed(&self, f: OnNegotiationNeededHdlrFn) { - self.internal - .on_negotiation_needed_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - fn do_negotiation_needed_inner(params: &NegotiationNeededParams) -> bool { - // https://w3c.github.io/webrtc-pc/#updating-the-negotiation-needed-flag - // non-canon step 1 - let state: NegotiationNeededState = params - .negotiation_needed_state - .load(Ordering::SeqCst) - .into(); - if state == NegotiationNeededState::Run { - params - .negotiation_needed_state - .store(NegotiationNeededState::Queue as u8, Ordering::SeqCst); - false - } else if state == NegotiationNeededState::Queue { - false - } else { - params - .negotiation_needed_state - .store(NegotiationNeededState::Run as u8, Ordering::SeqCst); - true - } - } - /// do_negotiation_needed enqueues negotiation_needed_op if necessary - /// caller of this method should hold `pc.mu` lock - async fn do_negotiation_needed(params: NegotiationNeededParams) { - if !RTCPeerConnection::do_negotiation_needed_inner(¶ms) { - return; - } - - let params2 = params.clone(); - let _ = params - .ops - .enqueue(Operation::new( - move || { - let params3 = params2.clone(); - Box::pin(async move { RTCPeerConnection::negotiation_needed_op(params3).await }) - }, - "do_negotiation_needed", - )) - .await; - } - - async fn after_negotiation_needed_op(params: NegotiationNeededParams) -> bool { - let old_negotiation_needed_state = params.negotiation_needed_state.load(Ordering::SeqCst); - - params - .negotiation_needed_state - .store(NegotiationNeededState::Empty as u8, Ordering::SeqCst); - - if old_negotiation_needed_state == NegotiationNeededState::Queue as u8 { - RTCPeerConnection::do_negotiation_needed_inner(¶ms) - } else { - false - } - } - - async fn negotiation_needed_op(params: NegotiationNeededParams) -> bool { - // Don't run NegotiatedNeeded checks if on_negotiation_needed is not set - let handler = &*params.on_negotiation_needed_handler.load(); - if handler.is_none() { - return false; - } - - // https://www.w3.org/TR/webrtc/#updating-the-negotiation-needed-flag - // Step 2.1 - if params.is_closed.load(Ordering::SeqCst) { - return false; - } - // non-canon step 2.2 - if !params.ops.is_empty().await { - //enqueue negotiation_needed_op again by return true - return true; - } - - // non-canon, run again if there was a request - // starting defer(after_do_negotiation_needed(params).await); - - // Step 2.3 - if params.signaling_state.load(Ordering::SeqCst) != RTCSignalingState::Stable as u8 { - return RTCPeerConnection::after_negotiation_needed_op(params).await; - } - - // Step 2.4 - if !RTCPeerConnection::check_negotiation_needed(¶ms.check_negotiation_needed_params) - .await - { - params.is_negotiation_needed.store(false, Ordering::SeqCst); - return RTCPeerConnection::after_negotiation_needed_op(params).await; - } - - // Step 2.5 - if params.is_negotiation_needed.load(Ordering::SeqCst) { - return RTCPeerConnection::after_negotiation_needed_op(params).await; - } - - // Step 2.6 - params.is_negotiation_needed.store(true, Ordering::SeqCst); - - // Step 2.7 - if let Some(handler) = handler { - let mut f = handler.lock().await; - f().await; - } - - RTCPeerConnection::after_negotiation_needed_op(params).await - } - - async fn check_negotiation_needed(params: &CheckNegotiationNeededParams) -> bool { - // To check if negotiation is needed for connection, perform the following checks: - // Skip 1, 2 steps - // Step 3 - let current_local_description = { - let current_local_description = params.current_local_description.lock().await; - current_local_description.clone() - }; - let current_remote_description = { - let current_remote_description = params.current_remote_description.lock().await; - current_remote_description.clone() - }; - - if let Some(local_desc) = ¤t_local_description { - let len_data_channel = { - let data_channels = params.sctp_transport.data_channels.lock().await; - data_channels.len() - }; - - if len_data_channel != 0 && have_data_channel(local_desc).is_none() { - return true; - } - - let transceivers = params.rtp_transceivers.lock().await; - for t in &*transceivers { - // https://www.w3.org/TR/webrtc/#dfn-update-the-negotiation-needed-flag - // Step 5.1 - // if t.stopping && !t.stopped { - // return true - // } - let mid = t.mid(); - let m = mid - .as_ref() - .and_then(|mid| get_by_mid(mid.as_str(), local_desc)); - // Step 5.2 - if !t.stopped.load(Ordering::SeqCst) { - if m.is_none() { - return true; - } - - if let Some(m) = m { - // Step 5.3.1 - if t.direction().has_send() { - let dmsid = match m.attribute(ATTR_KEY_MSID).and_then(|o| o) { - Some(m) => m, - None => return true, // doesn't contain a single a=msid line - }; - - let sender = t.sender().await; - // (...)or the number of MSIDs from the a=msid lines in this m= section, - // or the MSID values themselves, differ from what is in - // transceiver.sender.[[AssociatedMediaStreamIds]], return true. - - // TODO: This check should be robuster by storing all streams in the - // local description so we can compare all of them. For no we only - // consider the first one. - - let stream_ids = sender.associated_media_stream_ids(); - // Different number of lines, 1 vs 0 - if stream_ids.is_empty() { - return true; - } - - // different stream id - if dmsid.split_whitespace().next() != Some(&stream_ids[0]) { - return true; - } - } - match local_desc.sdp_type { - RTCSdpType::Offer => { - // Step 5.3.2 - if let Some(remote_desc) = ¤t_remote_description { - if let Some(rm) = t - .mid() - .and_then(|mid| get_by_mid(mid.as_str(), remote_desc)) - { - if get_peer_direction(m) != t.direction() - && get_peer_direction(rm) != t.direction().reverse() - { - return true; - } - } else { - return true; - } - } - } - RTCSdpType::Answer => { - let remote_desc = match ¤t_remote_description { - Some(d) => d, - None => return true, - }; - let offered_direction = match t - .mid() - .and_then(|mid| get_by_mid(mid.as_str(), remote_desc)) - { - Some(d) => { - let dir = get_peer_direction(d); - if dir == RTCRtpTransceiverDirection::Unspecified { - RTCRtpTransceiverDirection::Inactive - } else { - dir - } - } - None => RTCRtpTransceiverDirection::Inactive, - }; - - let current_direction = get_peer_direction(m); - // Step 5.3.3 - if current_direction - != t.direction().intersect(offered_direction.reverse()) - { - return true; - } - } - _ => {} - }; - } - } - // Step 5.4 - if t.stopped.load(Ordering::SeqCst) { - let search_mid = match t.mid() { - Some(mid) => mid, - None => return false, - }; - - if let Some(remote_desc) = &*params.current_remote_description.lock().await { - return get_by_mid(search_mid.as_str(), local_desc).is_some() - || get_by_mid(search_mid.as_str(), remote_desc).is_some(); - } - } - } - // Step 6 - false - } else { - true - } - } - - /// on_ice_candidate sets an event handler which is invoked when a new ICE - /// candidate is found. - /// Take note that the handler is gonna be called with a nil pointer when - /// gathering is finished. - pub fn on_ice_candidate(&self, f: OnLocalCandidateHdlrFn) { - self.internal.ice_gatherer.on_local_candidate(f) - } - - /// on_ice_gathering_state_change sets an event handler which is invoked when the - /// ICE candidate gathering state has changed. - pub fn on_ice_gathering_state_change(&self, f: OnICEGathererStateChangeHdlrFn) { - self.internal.ice_gatherer.on_state_change(f) - } - - /// on_track sets an event handler which is called when remote track - /// arrives from a remote peer. - pub fn on_track(&self, f: OnTrackHdlrFn) { - self.internal - .on_track_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - fn do_track( - on_track_handler: Arc>>, - track: Arc, - receiver: Arc, - transceiver: Arc, - ) { - log::debug!("got new track: {:?}", track); - - tokio::spawn(async move { - if let Some(handler) = &*on_track_handler.load() { - let mut f = handler.lock().await; - f(track, receiver, transceiver).await; - } else { - log::warn!("on_track unset, unable to handle incoming media streams"); - } - }); - } - - /// on_ice_connection_state_change sets an event handler which is called - /// when an ICE connection state is changed. - pub fn on_ice_connection_state_change(&self, f: OnICEConnectionStateChangeHdlrFn) { - self.internal - .on_ice_connection_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - async fn do_ice_connection_state_change( - handler: &Arc>>, - ice_connection_state: &Arc, - cs: RTCIceConnectionState, - ) { - ice_connection_state.store(cs as u8, Ordering::SeqCst); - - log::info!("ICE connection state changed: {}", cs); - if let Some(handler) = &*handler.load() { - let mut f = handler.lock().await; - f(cs).await; - } - } - - /// on_peer_connection_state_change sets an event handler which is called - /// when the PeerConnectionState has changed - pub fn on_peer_connection_state_change(&self, f: OnPeerConnectionStateChangeHdlrFn) { - self.internal - .on_peer_connection_state_change_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - async fn do_peer_connection_state_change( - handler: &Arc>>, - cs: RTCPeerConnectionState, - ) { - if let Some(handler) = &*handler.load() { - let mut f = handler.lock().await; - f(cs).await; - } - } - - // set_configuration updates the configuration of this PeerConnection object. - pub async fn set_configuration(&self, configuration: RTCConfiguration) -> Result<()> { - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2) - let mut config_lock = self.configuration.lock().await; - - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #3) - if !configuration.peer_identity.is_empty() { - if configuration.peer_identity != config_lock.peer_identity { - return Err(Error::ErrModifyingPeerIdentity); - } - config_lock.peer_identity = configuration.peer_identity; - } - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #4) - if !configuration.certificates.is_empty() { - if configuration.certificates.len() != config_lock.certificates.len() { - return Err(Error::ErrModifyingCertificates); - } - - config_lock.certificates = configuration.certificates; - } - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #5) - - if configuration.bundle_policy != config_lock.bundle_policy { - return Err(Error::ErrModifyingBundlePolicy); - } - config_lock.bundle_policy = configuration.bundle_policy; - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #6) - if configuration.rtcp_mux_policy != config_lock.rtcp_mux_policy { - return Err(Error::ErrModifyingRTCPMuxPolicy); - } - config_lock.rtcp_mux_policy = configuration.rtcp_mux_policy; - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #7) - if configuration.ice_candidate_pool_size != 0 { - if config_lock.ice_candidate_pool_size != configuration.ice_candidate_pool_size - && self.local_description().await.is_some() - { - return Err(Error::ErrModifyingICECandidatePoolSize); - } - config_lock.ice_candidate_pool_size = configuration.ice_candidate_pool_size; - } - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #8) - - config_lock.ice_transport_policy = configuration.ice_transport_policy; - - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11) - if !configuration.ice_servers.is_empty() { - // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3) - for server in &configuration.ice_servers { - server.validate()?; - } - config_lock.ice_servers = configuration.ice_servers - } - Ok(()) - } - - /// get_configuration returns a Configuration object representing the current - /// configuration of this PeerConnection object. The returned object is a - /// copy and direct mutation on it will not take affect until set_configuration - /// has been called with Configuration passed as its only argument. - /// - pub async fn get_configuration(&self) -> RTCConfiguration { - let configuration = self.configuration.lock().await; - configuration.clone() - } - - pub fn get_stats_id(&self) -> &str { - self.stats_id.as_str() - } - - /// create_offer starts the PeerConnection and generates the localDescription - /// - pub async fn create_offer( - &self, - options: Option, - ) -> Result { - let use_identity = self.idp_login_url.is_some(); - if use_identity { - return Err(Error::ErrIdentityProviderNotImplemented); - } else if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - if let Some(options) = options { - if options.ice_restart { - self.internal.ice_transport.restart().await?; - } - } - - // This may be necessary to recompute if, for example, createOffer was called when only an - // audio RTCRtpTransceiver was added to connection, but while performing the in-parallel - // steps to create an offer, a video RTCRtpTransceiver was added, requiring additional - // inspection of video system resources. - let mut count = 0; - let mut offer; - - loop { - // We cache current transceivers to ensure they aren't - // mutated during offer generation. We later check if they have - // been mutated and recompute the offer if necessary. - let current_transceivers = { - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - rtp_transceivers.clone() - }; - - // include unmatched local transceivers - // update the greater mid if the remote description provides a greater one - { - let current_remote_description = - self.internal.current_remote_description.lock().await; - if let Some(d) = &*current_remote_description { - if let Some(parsed) = &d.parsed { - for media in &parsed.media_descriptions { - if let Some(mid) = get_mid_value(media) { - if mid.is_empty() { - continue; - } - let numeric_mid = match mid.parse::() { - Ok(n) => n, - Err(_) => continue, - }; - if numeric_mid > self.internal.greater_mid.load(Ordering::SeqCst) { - self.internal - .greater_mid - .store(numeric_mid, Ordering::SeqCst); - } - } - } - } - } - } - for t in ¤t_transceivers { - if t.mid().is_some() { - continue; - } - - if let Some(gen) = &self.internal.setting_engine.mid_generator { - let current_greatest = self.internal.greater_mid.load(Ordering::SeqCst); - let mid = (gen)(current_greatest); - - // If it's possible to parse the returned mid as numeric, we will update the greater_mid field. - if let Ok(numeric_mid) = mid.parse::() { - if numeric_mid > self.internal.greater_mid.load(Ordering::SeqCst) { - self.internal - .greater_mid - .store(numeric_mid, Ordering::SeqCst); - } - } - - t.set_mid(SmolStr::from(mid))?; - } else { - let greater_mid = self.internal.greater_mid.fetch_add(1, Ordering::SeqCst); - t.set_mid(SmolStr::from(format!("{}", greater_mid + 1)))?; - } - } - - let current_remote_description_is_none = { - let current_remote_description = - self.internal.current_remote_description.lock().await; - current_remote_description.is_none() - }; - - let mut d = if current_remote_description_is_none { - self.internal - .generate_unmatched_sdp(current_transceivers, use_identity) - .await? - } else { - self.internal - .generate_matched_sdp( - current_transceivers, - use_identity, - true, /*includeUnmatched */ - DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ) - .await? - }; - - { - let mut sdp_origin = self.internal.sdp_origin.lock().await; - update_sdp_origin(&mut sdp_origin, &mut d); - } - let sdp = d.marshal(); - - offer = RTCSessionDescription { - sdp_type: RTCSdpType::Offer, - sdp, - parsed: Some(d), - }; - - // Verify local media hasn't changed during offer - // generation. Recompute if necessary - if !self.internal.has_local_description_changed(&offer).await { - break; - } - count += 1; - if count >= 128 { - return Err(Error::ErrExcessiveRetries); - } - } - - { - let mut last_offer = self.internal.last_offer.lock().await; - last_offer.clone_from(&offer.sdp); - } - Ok(offer) - } - - /// Update the PeerConnectionState given the state of relevant transports - /// - async fn update_connection_state( - on_peer_connection_state_change_handler: &Arc< - ArcSwapOption>, - >, - is_closed: &Arc, - peer_connection_state: &Arc, - ice_connection_state: RTCIceConnectionState, - dtls_transport_state: RTCDtlsTransportState, - ) { - let connection_state = - // The RTCPeerConnection object's [[IsClosed]] slot is true. - if is_closed.load(Ordering::SeqCst) { - RTCPeerConnectionState::Closed - } else if ice_connection_state == RTCIceConnectionState::Failed || dtls_transport_state == RTCDtlsTransportState::Failed { - // Any of the RTCIceTransports or RTCDtlsTransports are in a "failed" state. - RTCPeerConnectionState::Failed - } else if ice_connection_state == RTCIceConnectionState::Disconnected { - // Any of the RTCIceTransports or RTCDtlsTransports are in the "disconnected" - // state and none of them are in the "failed" or "connecting" or "checking" state. - RTCPeerConnectionState::Disconnected - } else if ice_connection_state == RTCIceConnectionState::Connected && dtls_transport_state == RTCDtlsTransportState::Connected { - // All RTCIceTransports and RTCDtlsTransports are in the "connected", "completed" or "closed" - // state and at least one of them is in the "connected" or "completed" state. - RTCPeerConnectionState::Connected - } else if ice_connection_state == RTCIceConnectionState::Checking && dtls_transport_state == RTCDtlsTransportState::Connecting { - // Any of the RTCIceTransports or RTCDtlsTransports are in the "connecting" or - // "checking" state and none of them is in the "failed" state. - RTCPeerConnectionState::Connecting - } else { - RTCPeerConnectionState::New - }; - - if peer_connection_state.load(Ordering::SeqCst) == connection_state as u8 { - return; - } - - log::info!("peer connection state changed: {}", connection_state); - peer_connection_state.store(connection_state as u8, Ordering::SeqCst); - - RTCPeerConnection::do_peer_connection_state_change( - on_peer_connection_state_change_handler, - connection_state, - ) - .await; - } - - /// create_answer starts the PeerConnection and generates the localDescription - pub async fn create_answer( - &self, - _options: Option, - ) -> Result { - let use_identity = self.idp_login_url.is_some(); - let remote_desc = self.remote_description().await; - let remote_description: RTCSessionDescription; - if let Some(desc) = remote_desc { - remote_description = desc; - } else { - return Err(Error::ErrNoRemoteDescription); - } - if use_identity { - return Err(Error::ErrIdentityProviderNotImplemented); - } else if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } else if self.signaling_state() != RTCSignalingState::HaveRemoteOffer - && self.signaling_state() != RTCSignalingState::HaveLocalPranswer - { - return Err(Error::ErrIncorrectSignalingState); - } - - let mut connection_role = self - .internal - .setting_engine - .answering_dtls_role - .to_connection_role(); - if connection_role == ConnectionRole::Unspecified { - connection_role = DEFAULT_DTLS_ROLE_ANSWER.to_connection_role(); - if let Some(parsed) = remote_description.parsed { - if Self::is_lite_set(&parsed) && !self.internal.setting_engine.candidates.ice_lite { - connection_role = DTLSRole::Server.to_connection_role(); - } - } - } - - let local_transceivers = self.get_transceivers().await; - let mut d = self - .internal - .generate_matched_sdp( - local_transceivers, - use_identity, - false, /*includeUnmatched */ - connection_role, - ) - .await?; - - { - let mut sdp_origin = self.internal.sdp_origin.lock().await; - update_sdp_origin(&mut sdp_origin, &mut d); - } - let sdp = d.marshal(); - - let answer = RTCSessionDescription { - sdp_type: RTCSdpType::Answer, - sdp, - parsed: Some(d), - }; - - { - let mut last_answer = self.internal.last_answer.lock().await; - last_answer.clone_from(&answer.sdp); - } - Ok(answer) - } - - // 4.4.1.6 Set the SessionDescription - pub(crate) async fn set_description( - &self, - sd: &RTCSessionDescription, - op: StateChangeOp, - ) -> Result<()> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } else if sd.sdp_type == RTCSdpType::Unspecified { - return Err(Error::ErrPeerConnSDPTypeInvalidValue); - } - - let next_state = { - let cur = self.signaling_state(); - let new_sdpdoes_not_match_offer = Error::ErrSDPDoesNotMatchOffer; - let new_sdpdoes_not_match_answer = Error::ErrSDPDoesNotMatchAnswer; - - match op { - StateChangeOp::SetLocal => { - match sd.sdp_type { - // stable->SetLocal(offer)->have-local-offer - RTCSdpType::Offer => { - let check = { - let last_offer = self.internal.last_offer.lock().await; - sd.sdp != *last_offer - }; - if check { - Err(new_sdpdoes_not_match_offer) - } else { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::HaveLocalOffer, - StateChangeOp::SetLocal, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_local_description = - self.internal.pending_local_description.lock().await; - *pending_local_description = Some(sd.clone()); - } - next_state - } - } - // have-remote-offer->SetLocal(answer)->stable - // have-local-pranswer->SetLocal(answer)->stable - RTCSdpType::Answer => { - let check = { - let last_answer = self.internal.last_answer.lock().await; - sd.sdp != *last_answer - }; - if check { - Err(new_sdpdoes_not_match_answer) - } else { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::Stable, - StateChangeOp::SetLocal, - sd.sdp_type, - ); - if next_state.is_ok() { - let pending_remote_description = { - let mut pending_remote_description = - self.internal.pending_remote_description.lock().await; - pending_remote_description.take() - }; - let _pending_local_description = { - let mut pending_local_description = - self.internal.pending_local_description.lock().await; - pending_local_description.take() - }; - - { - let mut current_local_description = - self.internal.current_local_description.lock().await; - *current_local_description = Some(sd.clone()); - } - { - let mut current_remote_description = - self.internal.current_remote_description.lock().await; - *current_remote_description = pending_remote_description; - } - } - next_state - } - } - RTCSdpType::Rollback => { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::Stable, - StateChangeOp::SetLocal, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_local_description = - self.internal.pending_local_description.lock().await; - *pending_local_description = None; - } - next_state - } - // have-remote-offer->SetLocal(pranswer)->have-local-pranswer - RTCSdpType::Pranswer => { - let check = { - let last_answer = self.internal.last_answer.lock().await; - sd.sdp != *last_answer - }; - if check { - Err(new_sdpdoes_not_match_answer) - } else { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::HaveLocalPranswer, - StateChangeOp::SetLocal, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_local_description = - self.internal.pending_local_description.lock().await; - *pending_local_description = Some(sd.clone()); - } - next_state - } - } - _ => Err(Error::ErrPeerConnStateChangeInvalid), - } - } - StateChangeOp::SetRemote => { - match sd.sdp_type { - // stable->SetRemote(offer)->have-remote-offer - RTCSdpType::Offer => { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::HaveRemoteOffer, - StateChangeOp::SetRemote, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_remote_description = - self.internal.pending_remote_description.lock().await; - *pending_remote_description = Some(sd.clone()); - } - next_state - } - // have-local-offer->SetRemote(answer)->stable - // have-remote-pranswer->SetRemote(answer)->stable - RTCSdpType::Answer => { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::Stable, - StateChangeOp::SetRemote, - sd.sdp_type, - ); - if next_state.is_ok() { - let pending_local_description = { - let mut pending_local_description = - self.internal.pending_local_description.lock().await; - pending_local_description.take() - }; - - let _pending_remote_description = { - let mut pending_remote_description = - self.internal.pending_remote_description.lock().await; - pending_remote_description.take() - }; - - { - let mut current_remote_description = - self.internal.current_remote_description.lock().await; - *current_remote_description = Some(sd.clone()); - } - { - let mut current_local_description = - self.internal.current_local_description.lock().await; - *current_local_description = pending_local_description; - } - } - next_state - } - RTCSdpType::Rollback => { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::Stable, - StateChangeOp::SetRemote, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_remote_description = - self.internal.pending_remote_description.lock().await; - *pending_remote_description = None; - } - next_state - } - // have-local-offer->SetRemote(pranswer)->have-remote-pranswer - RTCSdpType::Pranswer => { - let next_state = check_next_signaling_state( - cur, - RTCSignalingState::HaveRemotePranswer, - StateChangeOp::SetRemote, - sd.sdp_type, - ); - if next_state.is_ok() { - let mut pending_remote_description = - self.internal.pending_remote_description.lock().await; - *pending_remote_description = Some(sd.clone()); - } - next_state - } - _ => Err(Error::ErrPeerConnStateChangeInvalid), - } - } //_ => Err(Error::ErrPeerConnStateChangeUnhandled.into()), - } - }; - - match next_state { - Ok(next_state) => { - self.internal - .signaling_state - .store(next_state as u8, Ordering::SeqCst); - if self.signaling_state() == RTCSignalingState::Stable { - self.internal - .is_negotiation_needed - .store(false, Ordering::SeqCst); - self.internal.trigger_negotiation_needed().await; - } - self.do_signaling_state_change(next_state).await; - Ok(()) - } - Err(err) => Err(err), - } - } - - /// set_local_description sets the SessionDescription of the local peer - pub async fn set_local_description(&self, mut desc: RTCSessionDescription) -> Result<()> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let have_local_description = { - let current_local_description = self.internal.current_local_description.lock().await; - current_local_description.is_some() - }; - - // JSEP 5.4 - if desc.sdp.is_empty() { - match desc.sdp_type { - RTCSdpType::Answer | RTCSdpType::Pranswer => { - let last_answer = self.internal.last_answer.lock().await; - desc.sdp.clone_from(&last_answer); - } - RTCSdpType::Offer => { - let last_offer = self.internal.last_offer.lock().await; - desc.sdp.clone_from(&last_offer); - } - _ => return Err(Error::ErrPeerConnSDPTypeInvalidValueSetLocalDescription), - } - } - - desc.parsed = Some(desc.unmarshal()?); - self.set_description(&desc, StateChangeOp::SetLocal).await?; - - let we_answer = desc.sdp_type == RTCSdpType::Answer; - let remote_description = self.remote_description().await; - let mut local_transceivers = self.get_transceivers().await; - if we_answer { - if let Some(parsed) = desc.parsed { - // WebRTC Spec 1.0 https://www.w3.org/TR/webrtc/ - // Section 4.4.1.5 - for media in &parsed.media_descriptions { - if media.media_name.media == MEDIA_SECTION_APPLICATION { - continue; - } - - let kind = RTPCodecType::from(media.media_name.media.as_str()); - let direction = get_peer_direction(media); - if kind == RTPCodecType::Unspecified - || direction == RTCRtpTransceiverDirection::Unspecified - { - continue; - } - - let mid_value = match get_mid_value(media) { - Some(mid) if !mid.is_empty() => mid, - _ => continue, - }; - - let t = match find_by_mid(mid_value, &mut local_transceivers).await { - Some(t) => t, - None => continue, - }; - let previous_direction = t.current_direction(); - // 4.9.1.7.3 applying a local answer or pranswer - // Set transceiver.[[CurrentDirection]] and transceiver.[[FiredDirection]] to direction. - - // TODO: Also set FiredDirection here. - t.set_current_direction(direction); - t.process_new_current_direction(previous_direction).await?; - } - } - - if let Some(remote_desc) = remote_description { - self.start_rtp_senders().await?; - - let pci = Arc::clone(&self.internal); - let remote_desc = Arc::new(remote_desc); - self.internal - .ops - .enqueue(Operation::new( - move || { - let pc = Arc::clone(&pci); - let rd = Arc::clone(&remote_desc); - Box::pin(async move { - let _ = pc.start_rtp(have_local_description, rd).await; - false - }) - }, - "set_local_description", - )) - .await?; - } - } - - if self.internal.ice_gatherer.state() == RTCIceGathererState::New { - self.internal.ice_gatherer.gather().await - } else { - Ok(()) - } - } - - /// local_description returns PendingLocalDescription if it is not null and - /// otherwise it returns CurrentLocalDescription. This property is used to - /// determine if set_local_description has already been called. - /// - pub async fn local_description(&self) -> Option { - if let Some(pending_local_description) = self.pending_local_description().await { - return Some(pending_local_description); - } - self.current_local_description().await - } - - pub fn is_lite_set(desc: &SessionDescription) -> bool { - for a in &desc.attributes { - if a.key.trim() == ATTR_KEY_ICELITE { - return true; - } - } - false - } - - /// set_remote_description sets the SessionDescription of the remote peer - pub async fn set_remote_description(&self, mut desc: RTCSessionDescription) -> Result<()> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let is_renegotiation = { - let current_remote_description = self.internal.current_remote_description.lock().await; - current_remote_description.is_some() - }; - - desc.parsed = Some(desc.unmarshal()?); - self.set_description(&desc, StateChangeOp::SetRemote) - .await?; - - if let Some(parsed) = &desc.parsed { - self.internal - .media_engine - .update_from_remote_description(parsed) - .await?; - - let mut local_transceivers = self.get_transceivers().await; - let remote_description = self.remote_description().await; - let we_offer = desc.sdp_type == RTCSdpType::Answer; - - if !we_offer { - if let Some(parsed) = remote_description.as_ref().and_then(|r| r.parsed.as_ref()) { - for media in &parsed.media_descriptions { - let mid_value = match get_mid_value(media) { - Some(m) => { - if m.is_empty() { - return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue); - } else { - m - } - } - None => continue, - }; - - if media.media_name.media == MEDIA_SECTION_APPLICATION { - continue; - } - - let kind = RTPCodecType::from(media.media_name.media.as_str()); - let direction = get_peer_direction(media); - if kind == RTPCodecType::Unspecified - || direction == RTCRtpTransceiverDirection::Unspecified - { - continue; - } - - let t = if let Some(t) = - find_by_mid(mid_value, &mut local_transceivers).await - { - Some(t) - } else { - satisfy_type_and_direction(kind, direction, &mut local_transceivers) - .await - }; - - if let Some(t) = t { - if t.mid().is_none() { - t.set_mid(SmolStr::from(mid_value))?; - } - } else { - let local_direction = - if direction == RTCRtpTransceiverDirection::Recvonly { - RTCRtpTransceiverDirection::Sendonly - } else { - RTCRtpTransceiverDirection::Recvonly - }; - - let receive_mtu = self.internal.setting_engine.get_receive_mtu(); - - let receiver = Arc::new(RTCRtpReceiver::new( - receive_mtu, - kind, - Arc::clone(&self.internal.dtls_transport), - Arc::clone(&self.internal.media_engine), - Arc::clone(&self.interceptor), - )); - - let sender = Arc::new( - RTCRtpSender::new( - receive_mtu, - None, - kind, - Arc::clone(&self.internal.dtls_transport), - Arc::clone(&self.internal.media_engine), - Arc::clone(&self.interceptor), - false, - ) - .await, - ); - - let t = RTCRtpTransceiver::new( - receiver, - sender, - local_direction, - kind, - vec![], - Arc::clone(&self.internal.media_engine), - Some(Box::new(self.internal.make_negotiation_needed_trigger())), - ) - .await; - - self.internal.add_rtp_transceiver(Arc::clone(&t)).await; - - if t.mid().is_none() { - t.set_mid(SmolStr::from(mid_value))?; - } - } - } - } - } - - if we_offer { - // WebRTC Spec 1.0 https://www.w3.org/TR/webrtc/ - // 4.5.9.2 - // This is an answer from the remote. - if let Some(parsed) = remote_description.as_ref().and_then(|r| r.parsed.as_ref()) { - for media in &parsed.media_descriptions { - let mid_value = match get_mid_value(media) { - Some(m) => { - if m.is_empty() { - return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue); - } else { - m - } - } - None => continue, - }; - - if media.media_name.media == MEDIA_SECTION_APPLICATION { - continue; - } - let kind = RTPCodecType::from(media.media_name.media.as_str()); - let direction = get_peer_direction(media); - if kind == RTPCodecType::Unspecified - || direction == RTCRtpTransceiverDirection::Unspecified - { - continue; - } - - if let Some(t) = find_by_mid(mid_value, &mut local_transceivers).await { - let previous_direction = t.current_direction(); - - // 4.5.9.2.9 - // Let direction be an RTCRtpTransceiverDirection value representing the direction - // from the media description, but with the send and receive directions reversed to - // represent this peer's point of view. If the media description is rejected, - // set direction to "inactive". - let reversed_direction = direction.reverse(); - - // 4.5.9.2.13.2 - // Set transceiver.[[CurrentDirection]] and transceiver.[[Direction]]s to direction. - t.set_current_direction(reversed_direction); - // TODO: According to the specification we should set - // transceiver.[[Direction]] here, however libWebrtc doesn't do this. - // NOTE: After raising this it seems like the specification might - // change to remove the setting of transceiver.[[Direction]]. - // See https://github.com/w3c/webrtc-pc/issues/2751#issuecomment-1185901962 - // t.set_direction_internal(reversed_direction); - t.process_new_current_direction(previous_direction).await?; - } - } - } - } - - let (remote_ufrag, remote_pwd, candidates) = extract_ice_details(parsed).await?; - - if is_renegotiation - && self - .internal - .ice_transport - .have_remote_credentials_change(&remote_ufrag, &remote_pwd) - .await - { - // An ICE Restart only happens implicitly for a set_remote_description of type offer - if !we_offer { - self.internal.ice_transport.restart().await?; - } - - self.internal - .ice_transport - .set_remote_credentials(remote_ufrag.clone(), remote_pwd.clone()) - .await?; - } - - for candidate in candidates { - self.internal - .ice_transport - .add_remote_candidate(Some(candidate)) - .await?; - } - - if is_renegotiation { - if we_offer { - self.start_rtp_senders().await?; - - let pci = Arc::clone(&self.internal); - let remote_desc = Arc::new(desc); - self.internal - .ops - .enqueue(Operation::new( - move || { - let pc = Arc::clone(&pci); - let rd = Arc::clone(&remote_desc); - Box::pin(async move { - let _ = pc.start_rtp(true, rd).await; - false - }) - }, - "set_remote_description renegotiation", - )) - .await?; - } - return Ok(()); - } - - let remote_is_lite = Self::is_lite_set(parsed); - - let (fingerprint, fingerprint_hash) = extract_fingerprint(parsed)?; - - // If one of the agents is lite and the other one is not, the lite agent must be the controlling agent. - // If both or neither agents are lite the offering agent is controlling. - // RFC 8445 S6.1.1 - let ice_role = if (we_offer - && remote_is_lite == self.internal.setting_engine.candidates.ice_lite) - || (remote_is_lite && !self.internal.setting_engine.candidates.ice_lite) - { - RTCIceRole::Controlling - } else { - RTCIceRole::Controlled - }; - - // Start the networking in a new routine since it will block until - // the connection is actually established. - if we_offer { - self.start_rtp_senders().await?; - } - - //log::trace!("start_transports: parsed={:?}", parsed); - - let pci = Arc::clone(&self.internal); - let dtls_role = DTLSRole::from(parsed); - let remote_desc = Arc::new(desc); - self.internal - .ops - .enqueue(Operation::new( - move || { - let pc = Arc::clone(&pci); - let rd = Arc::clone(&remote_desc); - let ru = remote_ufrag.clone(); - let rp = remote_pwd.clone(); - let fp = fingerprint.clone(); - let fp_hash = fingerprint_hash.clone(); - Box::pin(async move { - log::trace!( - "start_transports: ice_role={}, dtls_role={}", - ice_role, - dtls_role, - ); - pc.start_transports(ice_role, dtls_role, ru, rp, fp, fp_hash) - .await; - - if we_offer { - let _ = pc.start_rtp(false, rd).await; - } - false - }) - }, - "set_remote_description", - )) - .await?; - } - - Ok(()) - } - - /// start_rtp_senders starts all outbound RTP streams - pub(crate) async fn start_rtp_senders(&self) -> Result<()> { - let current_transceivers = self.internal.rtp_transceivers.lock().await; - for transceiver in &*current_transceivers { - let sender = transceiver.sender().await; - if !sender.track_encodings.lock().await.is_empty() - && sender.is_negotiated() - && !sender.has_sent() - { - sender.send(&sender.get_parameters().await).await?; - } - } - - Ok(()) - } - - /// remote_description returns pending_remote_description if it is not null and - /// otherwise it returns current_remote_description. This property is used to - /// determine if setRemoteDescription has already been called. - /// - pub async fn remote_description(&self) -> Option { - self.internal.remote_description().await - } - - /// add_ice_candidate accepts an ICE candidate string and adds it - /// to the existing set of candidates. - pub async fn add_ice_candidate(&self, candidate: RTCIceCandidateInit) -> Result<()> { - if self.remote_description().await.is_none() { - return Err(Error::ErrNoRemoteDescription); - } - - let candidate_value = match candidate.candidate.strip_prefix("candidate:") { - Some(s) => s, - None => candidate.candidate.as_str(), - }; - - let ice_candidate = if !candidate_value.is_empty() { - let candidate: Arc = - Arc::new(unmarshal_candidate(candidate_value)?); - - Some(RTCIceCandidate::from(&candidate)) - } else { - None - }; - - self.internal - .ice_transport - .add_remote_candidate(ice_candidate) - .await - } - - /// ice_connection_state returns the ICE connection state of the - /// PeerConnection instance. - pub fn ice_connection_state(&self) -> RTCIceConnectionState { - self.internal - .ice_connection_state - .load(Ordering::SeqCst) - .into() - } - - /// get_senders returns the RTPSender that are currently attached to this PeerConnection - pub async fn get_senders(&self) -> Vec> { - let mut senders = vec![]; - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - for transceiver in &*rtp_transceivers { - let sender = transceiver.sender().await; - senders.push(sender); - } - senders - } - - /// get_receivers returns the RTPReceivers that are currently attached to this PeerConnection - pub async fn get_receivers(&self) -> Vec> { - let mut receivers = vec![]; - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - for transceiver in &*rtp_transceivers { - receivers.push(transceiver.receiver().await); - } - receivers - } - - /// get_transceivers returns the RtpTransceiver that are currently attached to this PeerConnection - pub async fn get_transceivers(&self) -> Vec> { - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - rtp_transceivers.clone() - } - - /// add_track adds a Track to the PeerConnection - pub async fn add_track( - &self, - track: Arc, - ) -> Result> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - { - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - for t in &*rtp_transceivers { - if !t.stopped.load(Ordering::SeqCst) - && t.kind == track.kind() - && t.sender() - .await - .initial_track_id() - .is_some_and(|id| id == track.id()) - { - let sender = t.sender().await; - if sender.track().await.is_none() { - if let Err(err) = sender.replace_track(Some(track)).await { - let _ = sender.stop().await; - return Err(err); - } - - t.set_direction_internal(RTCRtpTransceiverDirection::from_send_recv( - true, - t.direction().has_recv(), - )); - - self.internal.trigger_negotiation_needed().await; - return Ok(sender); - } - } - } - } - - let transceiver = self - .internal - .new_transceiver_from_track(RTCRtpTransceiverDirection::Sendrecv, track) - .await?; - self.internal - .add_rtp_transceiver(Arc::clone(&transceiver)) - .await; - - Ok(transceiver.sender().await) - } - - /// remove_track removes a Track from the PeerConnection - pub async fn remove_track(&self, sender: &Arc) -> Result<()> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let mut transceiver = None; - { - let rtp_transceivers = self.internal.rtp_transceivers.lock().await; - for t in &*rtp_transceivers { - if t.sender().await.id == sender.id { - if sender.track().await.is_none() { - return Ok(()); - } - transceiver = Some(t.clone()); - break; - } - } - } - - let t = transceiver.ok_or(Error::ErrSenderNotCreatedByConnection)?; - - // This also happens in `set_sending_track` but we need to make sure we do this - // before we call sender.stop to avoid a race condition when removing tracks and - // generating offers. - t.set_direction_internal(RTCRtpTransceiverDirection::from_send_recv( - false, - t.direction().has_recv(), - )); - // Stop the sender - let sender_result = sender.stop().await; - // This also updates direction - let sending_track_result = t.set_sending_track(None).await; - - if sender_result.is_ok() && sending_track_result.is_ok() { - self.internal.trigger_negotiation_needed().await; - } - Ok(()) - } - - /// add_transceiver_from_kind Create a new RtpTransceiver and adds it to the set of transceivers. - pub async fn add_transceiver_from_kind( - &self, - kind: RTPCodecType, - init: Option, - ) -> Result> { - self.internal.add_transceiver_from_kind(kind, init).await - } - - /// add_transceiver_from_track Create a new RtpTransceiver(SendRecv or SendOnly) and add it to the set of transceivers. - pub async fn add_transceiver_from_track( - &self, - track: Arc, - init: Option, - ) -> Result> { - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let direction = init - .map(|init| init.direction) - .unwrap_or(RTCRtpTransceiverDirection::Sendrecv); - - let t = self - .internal - .new_transceiver_from_track(direction, track) - .await?; - - self.internal.add_rtp_transceiver(Arc::clone(&t)).await; - - Ok(t) - } - - /// create_data_channel creates a new DataChannel object with the given label - /// and optional DataChannelInit used to configure properties of the - /// underlying channel such as data reliability. - pub async fn create_data_channel( - &self, - label: &str, - options: Option, - ) -> Result> { - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #2) - if self.internal.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let mut params = DataChannelParameters { - label: label.to_owned(), - ordered: true, - ..Default::default() - }; - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #19) - if let Some(options) = options { - // Ordered indicates if data is allowed to be delivered out of order. The - // default value of true, guarantees that data will be delivered in order. - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) - if let Some(ordered) = options.ordered { - params.ordered = ordered; - } - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) - if let Some(max_packet_life_time) = options.max_packet_life_time { - params.max_packet_life_time = max_packet_life_time; - } - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) - if let Some(max_retransmits) = options.max_retransmits { - params.max_retransmits = max_retransmits; - } - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) - if let Some(protocol) = options.protocol { - params.protocol = protocol; - } - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) - if params.protocol.len() > 65535 { - return Err(Error::ErrProtocolTooLarge); - } - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) - params.negotiated = options.negotiated; - } - - let d = Arc::new(RTCDataChannel::new( - params, - Arc::clone(&self.internal.setting_engine), - )); - - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) - if d.max_packet_lifetime != 0 && d.max_retransmits != 0 { - return Err(Error::ErrRetransmitsOrPacketLifeTime); - } - - { - let mut data_channels = self.internal.sctp_transport.data_channels.lock().await; - data_channels.push(Arc::clone(&d)); - } - self.internal - .sctp_transport - .data_channels_requested - .fetch_add(1, Ordering::SeqCst); - - // If SCTP already connected open all the channels - if self.internal.sctp_transport.state() == RTCSctpTransportState::Connected { - d.open(Arc::clone(&self.internal.sctp_transport)).await?; - } - - self.internal.trigger_negotiation_needed().await; - - Ok(d) - } - - /// set_identity_provider is used to configure an identity provider to generate identity assertions - pub fn set_identity_provider(&self, _provider: &str) -> Result<()> { - Err(Error::ErrPeerConnSetIdentityProviderNotImplemented) - } - - /// write_rtcp sends a user provided RTCP packet to the connected peer. If no peer is connected the - /// packet is discarded. It also runs any configured interceptors. - pub async fn write_rtcp( - &self, - pkts: &[Box], - ) -> Result { - let a = Attributes::new(); - Ok(self.interceptor_rtcp_writer.write(pkts, &a).await?) - } - - /// close ends the PeerConnection - pub async fn close(&self) -> Result<()> { - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #1) - if self.internal.is_closed.load(Ordering::SeqCst) { - return Ok(()); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #2) - self.internal.is_closed.store(true, Ordering::SeqCst); - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #3) - self.internal - .signaling_state - .store(RTCSignalingState::Closed as u8, Ordering::SeqCst); - - // Try closing everything and collect the errors - // Shutdown strategy: - // 1. All Conn close by closing their underlying Conn. - // 2. A Mux stops this chain. It won't close the underlying - // Conn if one of the endpoints is closed down. To - // continue the chain the Mux has to be closed. - let mut close_errs = vec![]; - - if let Err(err) = self.interceptor.close().await { - close_errs.push(Error::new(format!("interceptor: {err}"))); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #4) - { - let mut rtp_transceivers = self.internal.rtp_transceivers.lock().await; - for t in &*rtp_transceivers { - if let Err(err) = t.stop().await { - close_errs.push(Error::new(format!("rtp_transceivers: {err}"))); - } - } - rtp_transceivers.clear(); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #5) - { - let mut data_channels = self.internal.sctp_transport.data_channels.lock().await; - for d in &*data_channels { - if let Err(err) = d.close().await { - close_errs.push(Error::new(format!("data_channels: {err}"))); - } - } - data_channels.clear(); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #6) - if let Err(err) = self.internal.sctp_transport.stop().await { - close_errs.push(Error::new(format!("sctp_transport: {err}"))); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #7) - if let Err(err) = self.internal.dtls_transport.stop().await { - close_errs.push(Error::new(format!("dtls_transport: {err}"))); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #8, #9, #10) - if let Err(err) = self.internal.ice_transport.stop().await { - close_errs.push(Error::new(format!("ice_transport: {err}"))); - } - - // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #11) - RTCPeerConnection::update_connection_state( - &self.internal.on_peer_connection_state_change_handler, - &self.internal.is_closed, - &self.internal.peer_connection_state, - self.ice_connection_state(), - self.internal.dtls_transport.state(), - ) - .await; - - if let Err(err) = self.internal.ops.close().await { - close_errs.push(Error::new(format!("ops: {err}"))); - } - - flatten_errs(close_errs) - } - - /// CurrentLocalDescription represents the local description that was - /// successfully negotiated the last time the PeerConnection transitioned - /// into the stable state plus any local candidates that have been generated - /// by the ICEAgent since the offer or answer was created. - pub async fn current_local_description(&self) -> Option { - let local_description = { - let current_local_description = self.internal.current_local_description.lock().await; - current_local_description.clone() - }; - let ice_gather = Some(&self.internal.ice_gatherer); - let ice_gathering_state = self.ice_gathering_state(); - - populate_local_candidates(local_description.as_ref(), ice_gather, ice_gathering_state).await - } - - /// PendingLocalDescription represents a local description that is in the - /// process of being negotiated plus any local candidates that have been - /// generated by the ICEAgent since the offer or answer was created. If the - /// PeerConnection is in the stable state, the value is null. - pub async fn pending_local_description(&self) -> Option { - let local_description = { - let pending_local_description = self.internal.pending_local_description.lock().await; - pending_local_description.clone() - }; - let ice_gather = Some(&self.internal.ice_gatherer); - let ice_gathering_state = self.ice_gathering_state(); - - populate_local_candidates(local_description.as_ref(), ice_gather, ice_gathering_state).await - } - - /// current_remote_description represents the last remote description that was - /// successfully negotiated the last time the PeerConnection transitioned - /// into the stable state plus any remote candidates that have been supplied - /// via add_icecandidate() since the offer or answer was created. - pub async fn current_remote_description(&self) -> Option { - let current_remote_description = self.internal.current_remote_description.lock().await; - current_remote_description.clone() - } - - /// pending_remote_description represents a remote description that is in the - /// process of being negotiated, complete with any remote candidates that - /// have been supplied via add_icecandidate() since the offer or answer was - /// created. If the PeerConnection is in the stable state, the value is - /// null. - pub async fn pending_remote_description(&self) -> Option { - let pending_remote_description = self.internal.pending_remote_description.lock().await; - pending_remote_description.clone() - } - - /// signaling_state attribute returns the signaling state of the - /// PeerConnection instance. - pub fn signaling_state(&self) -> RTCSignalingState { - self.internal.signaling_state.load(Ordering::SeqCst).into() - } - - /// icegathering_state attribute returns the ICE gathering state of the - /// PeerConnection instance. - pub fn ice_gathering_state(&self) -> RTCIceGatheringState { - self.internal.ice_gathering_state() - } - - /// connection_state attribute returns the connection state of the - /// PeerConnection instance. - pub fn connection_state(&self) -> RTCPeerConnectionState { - self.internal - .peer_connection_state - .load(Ordering::SeqCst) - .into() - } - - pub async fn get_stats(&self) -> StatsReport { - self.internal - .get_stats(self.get_stats_id().to_owned()) - .await - .into() - } - - /// sctp returns the SCTPTransport for this PeerConnection - /// - /// The SCTP transport over which SCTP data is sent and received. If SCTP has not been negotiated, the value is nil. - /// - pub fn sctp(&self) -> Arc { - Arc::clone(&self.internal.sctp_transport) - } - - /// gathering_complete_promise is a Pion specific helper function that returns a channel that is closed when gathering is complete. - /// This function may be helpful in cases where you are unable to trickle your ICE Candidates. - /// - /// It is better to not use this function, and instead trickle candidates. If you use this function you will see longer connection startup times. - /// When the call is connected you will see no impact however. - pub async fn gathering_complete_promise(&self) -> mpsc::Receiver<()> { - let (gathering_complete_tx, gathering_complete_rx) = mpsc::channel(1); - - // It's possible to miss the GatherComplete event since setGatherCompleteHandler is an atomic operation and the - // promise might have been created after the gathering is finished. Therefore, we need to check if the ICE gathering - // state has changed to complete so that we don't block the caller forever. - let done = Arc::new(Mutex::new(Some(gathering_complete_tx))); - let done2 = Arc::clone(&done); - self.internal.set_gather_complete_handler(Box::new(move || { - log::trace!("setGatherCompleteHandler"); - let done3 = Arc::clone(&done2); - Box::pin(async move { - let mut d = done3.lock().await; - d.take(); - }) - })); - - if self.ice_gathering_state() == RTCIceGatheringState::Complete { - log::trace!("ICEGatheringState::Complete"); - let mut d = done.lock().await; - d.take(); - } - - gathering_complete_rx - } - - /// Returns the internal [`RTCDtlsTransport`]. - pub fn dtls_transport(&self) -> Arc { - Arc::clone(&self.internal.dtls_transport) - } - - /// Adds the specified [`RTCRtpTransceiver`] to this [`RTCPeerConnection`]. - pub async fn add_transceiver(&self, t: Arc) { - self.internal.add_rtp_transceiver(t).await - } -} diff --git a/webrtc/src/peer_connection/offer_answer_options.rs b/webrtc/src/peer_connection/offer_answer_options.rs deleted file mode 100644 index a7be6490b..000000000 --- a/webrtc/src/peer_connection/offer_answer_options.rs +++ /dev/null @@ -1,22 +0,0 @@ -/// AnswerOptions structure describes the options used to control the answer -/// creation process. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] -pub struct RTCAnswerOptions { - /// voice_activity_detection allows the application to provide information - /// about whether it wishes voice detection feature to be enabled or disabled. - pub voice_activity_detection: bool, -} - -/// OfferOptions structure describes the options used to control the offer -/// creation process -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] -pub struct RTCOfferOptions { - /// voice_activity_detection allows the application to provide information - /// about whether it wishes voice detection feature to be enabled or disabled. - pub voice_activity_detection: bool, - - /// ice_restart forces the underlying ice gathering process to be restarted. - /// When this value is true, the generated description will have ICE - /// credentials that are different from the current credentials - pub ice_restart: bool, -} diff --git a/webrtc/src/peer_connection/operation/mod.rs b/webrtc/src/peer_connection/operation/mod.rs deleted file mode 100644 index f8556e3ab..000000000 --- a/webrtc/src/peer_connection/operation/mod.rs +++ /dev/null @@ -1,140 +0,0 @@ -#[cfg(test)] -mod operation_test; - -use std::fmt; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use portable_atomic::AtomicUsize; -use tokio::sync::mpsc; -use waitgroup::WaitGroup; - -use crate::error::Result; - -/// Operation is a function -pub struct Operation( - pub Box Pin + Send + 'static>>) + Send + Sync>, - pub &'static str, -); - -impl Operation { - pub(crate) fn new( - op: impl FnMut() -> Pin + Send + 'static>> + Send + Sync + 'static, - description: &'static str, - ) -> Self { - Self(Box::new(op), description) - } -} - -impl fmt::Debug for Operation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Operation") - .field(&"_") - .field(&self.1) - .finish() - } -} - -/// Operations is a task executor. -#[derive(Default)] -pub(crate) struct Operations { - length: Arc, - ops_tx: Option>>, - close_tx: Option>, -} - -impl Operations { - pub(crate) fn new() -> Self { - let length = Arc::new(AtomicUsize::new(0)); - let (ops_tx, ops_rx) = mpsc::unbounded_channel(); - let (close_tx, close_rx) = mpsc::channel(1); - let l = Arc::clone(&length); - let ops_tx = Arc::new(ops_tx); - let ops_tx2 = Arc::clone(&ops_tx); - tokio::spawn(async move { - Operations::start(l, ops_tx, ops_rx, close_rx).await; - }); - - Operations { - length, - ops_tx: Some(ops_tx2), - close_tx: Some(close_tx), - } - } - - /// enqueue adds a new action to be executed. If there are no actions scheduled, - /// the execution will start immediately in a new goroutine. - pub(crate) async fn enqueue(&self, op: Operation) -> Result<()> { - if let Some(ops_tx) = &self.ops_tx { - return Operations::enqueue_inner(op, ops_tx, &self.length); - } - - Ok(()) - } - - fn enqueue_inner( - op: Operation, - ops_tx: &Arc>, - length: &Arc, - ) -> Result<()> { - length.fetch_add(1, Ordering::SeqCst); - ops_tx.send(op)?; - - Ok(()) - } - - /// is_empty checks if there are tasks in the queue - pub(crate) async fn is_empty(&self) -> bool { - self.length.load(Ordering::SeqCst) == 0 - } - - /// Done blocks until all currently enqueued operations are finished executing. - /// For more complex synchronization, use Enqueue directly. - pub(crate) async fn done(&self) { - let wg = WaitGroup::new(); - let mut w = Some(wg.worker()); - let _ = self - .enqueue(Operation::new( - move || { - let _d = w.take(); - Box::pin(async { false }) - }, - "Operation::done", - )) - .await; - wg.wait().await; - } - - pub(crate) async fn start( - length: Arc, - ops_tx: Arc>, - mut ops_rx: mpsc::UnboundedReceiver, - mut close_rx: mpsc::Receiver<()>, - ) { - loop { - tokio::select! { - _ = close_rx.recv() => { - break; - } - result = ops_rx.recv() => { - if let Some(mut f) = result { - length.fetch_sub(1, Ordering::SeqCst); - if f.0().await { - // Requeue this operation - let _ = Operations::enqueue_inner(f, &ops_tx, &length); - } - } - } - } - } - } - - pub(crate) async fn close(&self) -> Result<()> { - if let Some(close_tx) = &self.close_tx { - close_tx.send(()).await?; - } - Ok(()) - } -} diff --git a/webrtc/src/peer_connection/operation/operation_test.rs b/webrtc/src/peer_connection/operation/operation_test.rs deleted file mode 100644 index 0ecc344ca..000000000 --- a/webrtc/src/peer_connection/operation/operation_test.rs +++ /dev/null @@ -1,47 +0,0 @@ -use tokio::sync::Mutex; - -use super::*; -use crate::error::Result; - -#[tokio::test] -async fn test_operations_enqueue() -> Result<()> { - let ops = Operations::new(); - for _ in 0..100 { - let results = Arc::new(Mutex::new(vec![0; 16])); - for k in 0..16 { - let r = Arc::clone(&results); - ops.enqueue(Operation::new( - move || { - let r2 = Arc::clone(&r); - Box::pin(async move { - let mut r3 = r2.lock().await; - r3[k] += k * k; - r3[k] == 225 - }) - }, - "test_operations_enqueue", - )) - .await?; - } - - ops.done().await; - let expected = vec![ - 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 450, - ]; - { - let r = results.lock().await; - assert_eq!(r.len(), expected.len()); - assert_eq!(&*r, &expected); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_operations_done() -> Result<()> { - let ops = Operations::new(); - ops.done().await; - - Ok(()) -} diff --git a/webrtc/src/peer_connection/peer_connection_internal.rs b/webrtc/src/peer_connection/peer_connection_internal.rs deleted file mode 100644 index 50c2d8d89..000000000 --- a/webrtc/src/peer_connection/peer_connection_internal.rs +++ /dev/null @@ -1,1522 +0,0 @@ -use std::collections::VecDeque; -use std::sync::Weak; - -use arc_swap::ArcSwapOption; -use portable_atomic::AtomicIsize; -use smol_str::SmolStr; -use tokio::time::Instant; -use util::Unmarshal; - -use super::*; -use crate::rtp_transceiver::create_stream_info; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::{ - InboundRTPStats, OutboundRTPStats, RTCStatsType, RemoteInboundRTPStats, RemoteOutboundRTPStats, - StatsReportType, -}; -use crate::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use crate::track::TrackStream; -use crate::SDP_ATTRIBUTE_RID; - -pub(crate) struct PeerConnectionInternal { - /// a value containing the last known greater mid value - /// we internally generate mids as numbers. Needed since JSEP - /// requires that when reusing a media section a new unique mid - /// should be defined (see JSEP 3.4.1). - pub(super) greater_mid: AtomicIsize, - pub(super) sdp_origin: Mutex<::sdp::description::session::Origin>, - pub(super) last_offer: Mutex, - pub(super) last_answer: Mutex, - - pub(super) on_negotiation_needed_handler: Arc>>, - pub(super) is_closed: Arc, - - /// ops is an operations queue which will ensure the enqueued actions are - /// executed in order. It is used for asynchronously, but serially processing - /// remote and local descriptions - pub(crate) ops: Arc, - pub(super) negotiation_needed_state: Arc, - pub(super) is_negotiation_needed: Arc, - pub(super) signaling_state: Arc, - - pub(super) ice_transport: Arc, - pub(super) dtls_transport: Arc, - pub(super) on_peer_connection_state_change_handler: - Arc>>, - pub(super) peer_connection_state: Arc, - pub(super) ice_connection_state: Arc, - - pub(super) sctp_transport: Arc, - pub(super) rtp_transceivers: Arc>>>, - - pub(super) on_track_handler: Arc>>, - pub(super) on_signaling_state_change_handler: - ArcSwapOption>, - pub(super) on_ice_connection_state_change_handler: - Arc>>, - pub(super) on_data_channel_handler: Arc>>, - - pub(super) ice_gatherer: Arc, - - pub(super) current_local_description: Arc>>, - pub(super) current_remote_description: Arc>>, - pub(super) pending_local_description: Arc>>, - pub(super) pending_remote_description: Arc>>, - - // A reference to the associated API state used by this connection - pub(super) setting_engine: Arc, - pub(crate) media_engine: Arc, - pub(super) interceptor: Weak, - stats_interceptor: Arc, -} - -impl PeerConnectionInternal { - pub(super) async fn new( - api: &API, - interceptor: Weak, - stats_interceptor: Arc, - mut configuration: RTCConfiguration, - ) -> Result<(Arc, RTCConfiguration)> { - let mut pc = PeerConnectionInternal { - greater_mid: AtomicIsize::new(-1), - sdp_origin: Mutex::new(Default::default()), - last_offer: Mutex::new("".to_owned()), - last_answer: Mutex::new("".to_owned()), - - on_negotiation_needed_handler: Arc::new(ArcSwapOption::empty()), - ops: Arc::new(Operations::new()), - is_closed: Arc::new(AtomicBool::new(false)), - is_negotiation_needed: Arc::new(AtomicBool::new(false)), - negotiation_needed_state: Arc::new(AtomicU8::new(NegotiationNeededState::Empty as u8)), - signaling_state: Arc::new(AtomicU8::new(RTCSignalingState::Stable as u8)), - ice_transport: Arc::new(Default::default()), - dtls_transport: Arc::new(Default::default()), - ice_connection_state: Arc::new(AtomicU8::new(RTCIceConnectionState::New as u8)), - sctp_transport: Arc::new(Default::default()), - rtp_transceivers: Arc::new(Default::default()), - on_track_handler: Arc::new(ArcSwapOption::empty()), - on_signaling_state_change_handler: ArcSwapOption::empty(), - on_ice_connection_state_change_handler: Arc::new(ArcSwapOption::empty()), - on_data_channel_handler: Arc::new(Default::default()), - ice_gatherer: Arc::new(Default::default()), - current_local_description: Arc::new(Default::default()), - current_remote_description: Arc::new(Default::default()), - pending_local_description: Arc::new(Default::default()), - peer_connection_state: Arc::new(AtomicU8::new(RTCPeerConnectionState::New as u8)), - - setting_engine: Arc::clone(&api.setting_engine), - media_engine: if !api.setting_engine.disable_media_engine_copy { - Arc::new(api.media_engine.clone_to()) - } else { - Arc::clone(&api.media_engine) - }, - interceptor, - stats_interceptor, - on_peer_connection_state_change_handler: Arc::new(ArcSwapOption::empty()), - pending_remote_description: Arc::new(Default::default()), - }; - - // Create the ice gatherer - pc.ice_gatherer = Arc::new(api.new_ice_gatherer(RTCIceGatherOptions { - ice_servers: configuration.get_ice_servers(), - ice_gather_policy: configuration.ice_transport_policy, - })?); - - // Create the ice transport - pc.ice_transport = pc.create_ice_transport(api).await; - - // Create the DTLS transport - let certificates = configuration.certificates.drain(..).collect(); - pc.dtls_transport = - Arc::new(api.new_dtls_transport(Arc::clone(&pc.ice_transport), certificates)?); - - // Create the SCTP transport - pc.sctp_transport = Arc::new(api.new_sctp_transport(Arc::clone(&pc.dtls_transport))?); - - // Wire up the on datachannel handler - let on_data_channel_handler = Arc::clone(&pc.on_data_channel_handler); - pc.sctp_transport - .on_data_channel(Box::new(move |d: Arc| { - let on_data_channel_handler2 = Arc::clone(&on_data_channel_handler); - Box::pin(async move { - if let Some(handler) = &*on_data_channel_handler2.load() { - let mut f = handler.lock().await; - f(d).await; - } - }) - })); - - Ok((Arc::new(pc), configuration)) - } - - pub(super) async fn start_rtp( - self: &Arc, - is_renegotiation: bool, - remote_desc: Arc, - ) -> Result<()> { - let mut track_details = if let Some(parsed) = &remote_desc.parsed { - track_details_from_sdp(parsed, false) - } else { - vec![] - }; - - let current_transceivers = { - let current_transceivers = self.rtp_transceivers.lock().await; - current_transceivers.clone() - }; - - if !is_renegotiation { - self.undeclared_media_processor(); - } else { - for t in ¤t_transceivers { - let receiver = t.receiver().await; - let tracks = receiver.tracks().await; - if tracks.is_empty() { - continue; - } - - let mut receiver_needs_stopped = false; - - for t in tracks { - if !t.rid().is_empty() { - if let Some(details) = - track_details_for_rid(&track_details, SmolStr::from(t.rid())) - { - t.set_id(details.id.clone()); - t.set_stream_id(details.stream_id.clone()); - continue; - } - } else if t.ssrc() != 0 { - if let Some(details) = track_details_for_ssrc(&track_details, t.ssrc()) { - t.set_id(details.id.clone()); - t.set_stream_id(details.stream_id.clone()); - continue; - } - } - - receiver_needs_stopped = true; - } - - if !receiver_needs_stopped { - continue; - } - - log::info!("Stopping receiver {:?}", receiver); - if let Err(err) = receiver.stop().await { - log::warn!("Failed to stop RtpReceiver: {}", err); - continue; - } - - let interceptor = self - .interceptor - .upgrade() - .ok_or(Error::ErrInterceptorNotBind)?; - - let receiver = Arc::new(RTCRtpReceiver::new( - self.setting_engine.get_receive_mtu(), - receiver.kind(), - Arc::clone(&self.dtls_transport), - Arc::clone(&self.media_engine), - interceptor, - )); - t.set_receiver(receiver).await; - } - } - - self.start_rtp_receivers(&mut track_details, ¤t_transceivers) - .await?; - if let Some(parsed) = &remote_desc.parsed { - if have_application_media_section(parsed) { - self.start_sctp().await; - } - } - - Ok(()) - } - - /// undeclared_media_processor handles RTP/RTCP packets that don't match any a:ssrc lines - fn undeclared_media_processor(self: &Arc) { - let dtls_transport = Arc::clone(&self.dtls_transport); - let is_closed = Arc::clone(&self.is_closed); - let pci = Arc::clone(self); - - // SRTP acceptor - tokio::spawn(async move { - let simulcast_routine_count = Arc::new(AtomicU64::new(0)); - loop { - let srtp_session = match dtls_transport.get_srtp_session().await { - Some(s) => s, - None => { - log::warn!("undeclared_media_processor failed to open SrtpSession"); - return; - } - }; - - let stream = match srtp_session.accept().await { - Ok(stream) => stream, - Err(err) => { - log::warn!("Failed to accept RTP {}", err); - return; - } - }; - - if is_closed.load(Ordering::SeqCst) { - if let Err(err) = stream.close().await { - log::warn!("Failed to close RTP stream {}", err); - } - continue; - } - - if simulcast_routine_count.fetch_add(1, Ordering::SeqCst) + 1 - >= SIMULCAST_MAX_PROBE_ROUTINES - { - simulcast_routine_count.fetch_sub(1, Ordering::SeqCst); - log::warn!("{:?}", Error::ErrSimulcastProbeOverflow); - continue; - } - - { - let dtls_transport = Arc::clone(&dtls_transport); - let simulcast_routine_count = Arc::clone(&simulcast_routine_count); - let pci = Arc::clone(&pci); - tokio::spawn(async move { - let ssrc = stream.get_ssrc(); - - dtls_transport - .store_simulcast_stream(ssrc, Arc::clone(&stream)) - .await; - - if let Err(err) = pci.handle_incoming_ssrc(stream, ssrc).await { - log::error!( - "Incoming unhandled RTP ssrc({}), on_track will not be fired. {}", - ssrc, - err - ); - } - - simulcast_routine_count.fetch_sub(1, Ordering::SeqCst); - }); - } - } - }); - - // SRTCP acceptor - { - let dtls_transport = Arc::clone(&self.dtls_transport); - tokio::spawn(async move { - loop { - let srtcp_session = match dtls_transport.get_srtcp_session().await { - Some(s) => s, - None => { - log::warn!("undeclared_media_processor failed to open SrtcpSession"); - return; - } - }; - - let stream = match srtcp_session.accept().await { - Ok(stream) => stream, - Err(err) => { - log::warn!("Failed to accept RTCP {}", err); - return; - } - }; - log::warn!( - "Incoming unhandled RTCP ssrc({}), on_track will not be fired", - stream.get_ssrc() - ); - } - }); - } - } - - /// start_rtp_receivers opens knows inbound SRTP streams from the remote_description - async fn start_rtp_receivers( - self: &Arc, - incoming_tracks: &mut Vec, - local_transceivers: &[Arc], - ) -> Result<()> { - // Ensure we haven't already started a transceiver for this ssrc - let mut filtered_tracks = incoming_tracks.clone(); - for incoming_track in incoming_tracks { - // If we already have a TrackRemote for a given SSRC don't handle it again - for t in local_transceivers { - let receiver = t.receiver().await; - for track in receiver.tracks().await { - for ssrc in &incoming_track.ssrcs { - if *ssrc == track.ssrc() { - filter_track_with_ssrc(&mut filtered_tracks, track.ssrc()); - } - } - } - } - } - - let mut unhandled_tracks = vec![]; // filtered_tracks[:0] - for incoming_track in filtered_tracks.iter() { - let mut track_handled = false; - for t in local_transceivers { - if t.mid().as_ref() != Some(&incoming_track.mid) { - continue; - } - - if (incoming_track.kind != t.kind()) - || (t.direction() != RTCRtpTransceiverDirection::Recvonly - && t.direction() != RTCRtpTransceiverDirection::Sendrecv) - { - continue; - } - - let receiver = t.receiver().await; - if receiver.have_received().await { - continue; - } - PeerConnectionInternal::start_receiver( - self.setting_engine.get_receive_mtu(), - incoming_track, - receiver, - Arc::clone(t), - Arc::clone(&self.on_track_handler), - ) - .await; - track_handled = true; - } - - if !track_handled { - unhandled_tracks.push(incoming_track); - } - } - - Ok(()) - } - - /// Start SCTP subsystem - async fn start_sctp(&self) { - // Start sctp - if let Err(err) = self - .sctp_transport - .start(SCTPTransportCapabilities { - max_message_size: 0, - }) - .await - { - log::warn!("Failed to start SCTP: {}", err); - if let Err(err) = self.sctp_transport.stop().await { - log::warn!("Failed to stop SCTPTransport: {}", err); - } - - return; - } - - // DataChannels that need to be opened now that SCTP is available - // make a copy we may have incoming DataChannels mutating this while we open - let data_channels = { - let data_channels = self.sctp_transport.data_channels.lock().await; - data_channels.clone() - }; - - let mut opened_dc_count = 0; - for d in data_channels { - if d.ready_state() == RTCDataChannelState::Connecting { - if let Err(err) = d.open(Arc::clone(&self.sctp_transport)).await { - log::warn!("failed to open data channel: {}", err); - continue; - } - opened_dc_count += 1; - } - } - - self.sctp_transport - .data_channels_opened - .fetch_add(opened_dc_count, Ordering::SeqCst); - } - - pub(super) async fn add_transceiver_from_kind( - &self, - kind: RTPCodecType, - init: Option, - ) -> Result> { - if self.is_closed.load(Ordering::SeqCst) { - return Err(Error::ErrConnectionClosed); - } - - let direction = init - .map(|value| value.direction) - .unwrap_or(RTCRtpTransceiverDirection::Sendrecv); - - let t = match direction { - RTCRtpTransceiverDirection::Sendonly | RTCRtpTransceiverDirection::Sendrecv => { - let codec = self - .media_engine - .get_codecs_by_kind(kind) - .first() - .map(|c| c.capability.clone()) - .ok_or(Error::ErrNoCodecsAvailable)?; - let track = Arc::new(TrackLocalStaticSample::new( - codec, - math_rand_alpha(16), - math_rand_alpha(16), - )); - self.new_transceiver_from_track(direction, track).await? - } - RTCRtpTransceiverDirection::Recvonly => { - let interceptor = self - .interceptor - .upgrade() - .ok_or(Error::ErrInterceptorNotBind)?; - let receiver = Arc::new(RTCRtpReceiver::new( - self.setting_engine.get_receive_mtu(), - kind, - Arc::clone(&self.dtls_transport), - Arc::clone(&self.media_engine), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - RTCRtpSender::new( - self.setting_engine.get_receive_mtu(), - None, - kind, - Arc::clone(&self.dtls_transport), - Arc::clone(&self.media_engine), - interceptor, - false, - ) - .await, - ); - - RTCRtpTransceiver::new( - receiver, - sender, - direction, - kind, - vec![], - Arc::clone(&self.media_engine), - Some(Box::new(self.make_negotiation_needed_trigger())), - ) - .await - } - _ => return Err(Error::ErrPeerConnAddTransceiverFromKindSupport), - }; - - self.add_rtp_transceiver(Arc::clone(&t)).await; - - Ok(t) - } - - pub(super) async fn new_transceiver_from_track( - &self, - direction: RTCRtpTransceiverDirection, - track: Arc, - ) -> Result> { - let interceptor = self - .interceptor - .upgrade() - .ok_or(Error::ErrInterceptorNotBind)?; - - if direction == RTCRtpTransceiverDirection::Unspecified { - return Err(Error::ErrPeerConnAddTransceiverFromTrackSupport); - } - - let r = Arc::new(RTCRtpReceiver::new( - self.setting_engine.get_receive_mtu(), - track.kind(), - Arc::clone(&self.dtls_transport), - Arc::clone(&self.media_engine), - Arc::clone(&interceptor), - )); - - let s = Arc::new( - RTCRtpSender::new( - self.setting_engine.get_receive_mtu(), - Some(Arc::clone(&track)), - track.kind(), - Arc::clone(&self.dtls_transport), - Arc::clone(&self.media_engine), - Arc::clone(&interceptor), - false, - ) - .await, - ); - - Ok(RTCRtpTransceiver::new( - r, - s, - direction, - track.kind(), - vec![], - Arc::clone(&self.media_engine), - Some(Box::new(self.make_negotiation_needed_trigger())), - ) - .await) - } - - /// add_rtp_transceiver appends t into rtp_transceivers - /// and fires onNegotiationNeeded; - /// caller of this method should hold `self.mu` lock - pub(super) async fn add_rtp_transceiver(&self, t: Arc) { - { - let mut rtp_transceivers = self.rtp_transceivers.lock().await; - rtp_transceivers.push(t); - } - self.trigger_negotiation_needed().await; - } - - /// Helper to trigger a negotiation needed. - pub(crate) async fn trigger_negotiation_needed(&self) { - RTCPeerConnection::do_negotiation_needed(self.create_negotiation_needed_params()).await; - } - - /// Creates the parameters needed to trigger a negotiation needed. - fn create_negotiation_needed_params(&self) -> NegotiationNeededParams { - NegotiationNeededParams { - on_negotiation_needed_handler: Arc::clone(&self.on_negotiation_needed_handler), - is_closed: Arc::clone(&self.is_closed), - ops: Arc::clone(&self.ops), - negotiation_needed_state: Arc::clone(&self.negotiation_needed_state), - is_negotiation_needed: Arc::clone(&self.is_negotiation_needed), - signaling_state: Arc::clone(&self.signaling_state), - check_negotiation_needed_params: CheckNegotiationNeededParams { - sctp_transport: Arc::clone(&self.sctp_transport), - rtp_transceivers: Arc::clone(&self.rtp_transceivers), - current_local_description: Arc::clone(&self.current_local_description), - current_remote_description: Arc::clone(&self.current_remote_description), - }, - } - } - - pub(crate) fn make_negotiation_needed_trigger( - &self, - ) -> impl Fn() -> Pin + Send + Sync>> + Send + Sync { - let params = self.create_negotiation_needed_params(); - move || { - let params = params.clone(); - Box::pin(async move { - let params = params.clone(); - RTCPeerConnection::do_negotiation_needed(params).await; - }) - } - } - - pub(super) async fn remote_description(&self) -> Option { - let pending_remote_description = self.pending_remote_description.lock().await; - if pending_remote_description.is_some() { - pending_remote_description.clone() - } else { - let current_remote_description = self.current_remote_description.lock().await; - current_remote_description.clone() - } - } - - pub(super) fn set_gather_complete_handler(&self, f: OnGatheringCompleteHdlrFn) { - self.ice_gatherer.on_gathering_complete(f); - } - - /// Start all transports. PeerConnection now has enough state - pub(super) async fn start_transports( - self: &Arc, - ice_role: RTCIceRole, - dtls_role: DTLSRole, - remote_ufrag: String, - remote_pwd: String, - fingerprint: String, - fingerprint_hash: String, - ) { - // Start the ice transport - if let Err(err) = self - .ice_transport - .start( - &RTCIceParameters { - username_fragment: remote_ufrag, - password: remote_pwd, - ice_lite: false, - }, - Some(ice_role), - ) - .await - { - log::warn!("Failed to start manager ice: {}", err); - return; - } - - // Start the dtls_transport transport - let result = self - .dtls_transport - .start(DTLSParameters { - role: dtls_role, - fingerprints: vec![RTCDtlsFingerprint { - algorithm: fingerprint_hash, - value: fingerprint, - }], - }) - .await; - RTCPeerConnection::update_connection_state( - &self.on_peer_connection_state_change_handler, - &self.is_closed, - &self.peer_connection_state, - self.ice_connection_state.load(Ordering::SeqCst).into(), - self.dtls_transport.state(), - ) - .await; - if let Err(err) = result { - log::warn!("Failed to start manager dtls: {}", err); - } - } - - /// generate_unmatched_sdp generates an SDP that doesn't take remote state into account - /// This is used for the initial call for CreateOffer - pub(super) async fn generate_unmatched_sdp( - &self, - local_transceivers: Vec>, - use_identity: bool, - ) -> Result { - let d = SessionDescription::new_jsep_session_description(use_identity); - - let ice_params = self.ice_gatherer.get_local_parameters().await?; - - let candidates = self.ice_gatherer.get_local_candidates().await?; - - let mut media_sections = vec![]; - - for t in &local_transceivers { - if t.stopped.load(Ordering::SeqCst) { - // An "m=" section is generated for each - // RtpTransceiver that has been added to the PeerConnection, excluding - // any stopped RtpTransceivers; - continue; - } - - // TODO: This is dubious because of rollbacks. - t.sender().await.set_negotiated(); - media_sections.push(MediaSection { - id: t.mid().unwrap().to_string(), - transceivers: vec![Arc::clone(t)], - ..Default::default() - }); - } - - if self - .sctp_transport - .data_channels_requested - .load(Ordering::SeqCst) - != 0 - { - media_sections.push(MediaSection { - id: format!("{}", media_sections.len()), - data: true, - ..Default::default() - }); - } - - let dtls_fingerprints = if let Some(cert) = self.dtls_transport.certificates.first() { - cert.get_fingerprints() - } else { - return Err(Error::ErrNonCertificate); - }; - - let params = PopulateSdpParams { - media_description_fingerprint: self.setting_engine.sdp_media_level_fingerprints, - is_icelite: self.setting_engine.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: self.ice_gathering_state(), - match_bundle_group: None, - }; - populate_sdp( - d, - &dtls_fingerprints, - &self.media_engine, - &candidates, - &ice_params, - &media_sections, - params, - ) - .await - } - - /// generate_matched_sdp generates a SDP and takes the remote state into account - /// this is used everytime we have a remote_description - pub(super) async fn generate_matched_sdp( - &self, - mut local_transceivers: Vec>, - use_identity: bool, - include_unmatched: bool, - connection_role: ConnectionRole, - ) -> Result { - let d = SessionDescription::new_jsep_session_description(use_identity); - - let ice_params = self.ice_gatherer.get_local_parameters().await?; - let candidates = self.ice_gatherer.get_local_candidates().await?; - - let remote_description = self.remote_description().await; - let mut media_sections = vec![]; - let mut already_have_application_media_section = false; - if let Some(remote_description) = remote_description.as_ref() { - if let Some(parsed) = &remote_description.parsed { - for media in &parsed.media_descriptions { - if let Some(mid_value) = get_mid_value(media) { - if mid_value.is_empty() { - return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue); - } - - if media.media_name.media == MEDIA_SECTION_APPLICATION { - media_sections.push(MediaSection { - id: mid_value.to_owned(), - data: true, - ..Default::default() - }); - already_have_application_media_section = true; - continue; - } - - let kind = RTPCodecType::from(media.media_name.media.as_str()); - let direction = get_peer_direction(media); - if kind == RTPCodecType::Unspecified - || direction == RTCRtpTransceiverDirection::Unspecified - { - continue; - } - - if let Some(t) = find_by_mid(mid_value, &mut local_transceivers).await { - t.sender().await.set_negotiated(); - let media_transceivers = vec![t]; - - // NB: The below could use `then_some`, but with our current MSRV - // it's not possible to actually do this. The clippy version that - // ships with 1.64.0 complains about this so we disable it for now. - #[allow(clippy::unnecessary_lazy_evaluations)] - media_sections.push(MediaSection { - id: mid_value.to_owned(), - transceivers: media_transceivers, - rid_map: get_rids(media), - offered_direction: (!include_unmatched).then(|| direction), - ..Default::default() - }); - } else { - return Err(Error::ErrPeerConnTransceiverMidNil); - } - } - } - } - } - - // If we are offering also include unmatched local transceivers - let match_bundle_group = if include_unmatched { - for t in &local_transceivers { - t.sender().await.set_negotiated(); - media_sections.push(MediaSection { - id: t.mid().unwrap().to_string(), - transceivers: vec![Arc::clone(t)], - ..Default::default() - }); - } - - if self - .sctp_transport - .data_channels_requested - .load(Ordering::SeqCst) - != 0 - && !already_have_application_media_section - { - media_sections.push(MediaSection { - id: format!("{}", media_sections.len()), - data: true, - ..Default::default() - }); - } - None - } else { - remote_description - .as_ref() - .and_then(|d| d.parsed.as_ref()) - .and_then(|d| d.attribute(ATTR_KEY_GROUP)) - .map(ToOwned::to_owned) - }; - - let dtls_fingerprints = if let Some(cert) = self.dtls_transport.certificates.first() { - cert.get_fingerprints() - } else { - return Err(Error::ErrNonCertificate); - }; - - let params = PopulateSdpParams { - media_description_fingerprint: self.setting_engine.sdp_media_level_fingerprints, - is_icelite: self.setting_engine.candidates.ice_lite, - connection_role, - ice_gathering_state: self.ice_gathering_state(), - match_bundle_group, - }; - populate_sdp( - d, - &dtls_fingerprints, - &self.media_engine, - &candidates, - &ice_params, - &media_sections, - params, - ) - .await - } - - pub(super) fn ice_gathering_state(&self) -> RTCIceGatheringState { - match self.ice_gatherer.state() { - RTCIceGathererState::New => RTCIceGatheringState::New, - RTCIceGathererState::Gathering => RTCIceGatheringState::Gathering, - _ => RTCIceGatheringState::Complete, - } - } - - async fn handle_undeclared_ssrc( - self: &Arc, - ssrc: SSRC, - remote_description: &SessionDescription, - ) -> Result { - if remote_description.media_descriptions.len() != 1 { - return Ok(false); - } - - let only_media_section = &remote_description.media_descriptions[0]; - let mut stream_id = ""; - let mut id = ""; - let mut has_rid = false; - let mut has_ssrc = false; - - for a in &only_media_section.attributes { - match a.key.as_str() { - ATTR_KEY_MSID => { - if let Some(value) = &a.value { - let split: Vec<&str> = value.split(' ').collect(); - if split.len() == 2 { - stream_id = split[0]; - id = split[1]; - } - } - } - ATTR_KEY_SSRC => has_ssrc = true, - SDP_ATTRIBUTE_RID => has_rid = true, - _ => {} - }; - } - - if has_rid { - return Ok(false); - } else if has_ssrc { - return Err(Error::ErrPeerConnSingleMediaSectionHasExplicitSSRC); - } - - let mut incoming = TrackDetails { - ssrcs: vec![ssrc], - kind: RTPCodecType::Video, - stream_id: stream_id.to_owned(), - id: id.to_owned(), - ..Default::default() - }; - if only_media_section.media_name.media == RTPCodecType::Audio.to_string() { - incoming.kind = RTPCodecType::Audio; - } - - let t = self - .add_transceiver_from_kind( - incoming.kind, - Some(RTCRtpTransceiverInit { - direction: RTCRtpTransceiverDirection::Sendrecv, - send_encodings: vec![], - }), - ) - .await?; - - let receiver = t.receiver().await; - PeerConnectionInternal::start_receiver( - self.setting_engine.get_receive_mtu(), - &incoming, - receiver, - t, - Arc::clone(&self.on_track_handler), - ) - .await; - Ok(true) - } - - async fn handle_incoming_ssrc( - self: &Arc, - rtp_stream: Arc, - ssrc: SSRC, - ) -> Result<()> { - let parsed = match self.remote_description().await.and_then(|rd| rd.parsed) { - Some(r) => r, - None => return Err(Error::ErrPeerConnRemoteDescriptionNil), - }; - // If the remote SDP was only one media section the ssrc doesn't have to be explicitly declared - let handled = self.handle_undeclared_ssrc(ssrc, &parsed).await?; - if handled { - return Ok(()); - } - - // Get MID extension ID - let (mid_extension_id, audio_supported, video_supported) = self - .media_engine - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: ::sdp::extmap::SDES_MID_URI.to_owned(), - }) - .await; - if !audio_supported && !video_supported { - return Err(Error::ErrPeerConnSimulcastMidRTPExtensionRequired); - } - - // Get RID extension ID - let (sid_extension_id, audio_supported, video_supported) = self - .media_engine - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: ::sdp::extmap::SDES_RTP_STREAM_ID_URI.to_owned(), - }) - .await; - if !audio_supported && !video_supported { - return Err(Error::ErrPeerConnSimulcastStreamIDRTPExtensionRequired); - } - - let (rsid_extension_id, _, _) = self - .media_engine - .get_header_extension_id(RTCRtpHeaderExtensionCapability { - uri: ::sdp::extmap::SDES_REPAIR_RTP_STREAM_ID_URI.to_owned(), - }) - .await; - - let mut buf = vec![0u8; self.setting_engine.get_receive_mtu()]; - // Packets that we read as part of simulcast probing that we need to make available - // if we do find a track later. - let mut buffered_packets: VecDeque<(rtp::packet::Packet, Attributes)> = VecDeque::default(); - - let n = rtp_stream.read(&mut buf).await?; - - let (mut mid, mut rid, mut rsid, payload_type) = handle_unknown_rtp_packet( - &buf[..n], - mid_extension_id as u8, - sid_extension_id as u8, - rsid_extension_id as u8, - )?; - - let packet = rtp::packet::Packet::unmarshal(&mut buf.as_slice()).unwrap(); - - // TODO: Can we have attributes on the first packets? - buffered_packets.push_back((packet, Attributes::new())); - - let params = self - .media_engine - .get_rtp_parameters_by_payload_type(payload_type) - .await?; - - let icpr = match self.interceptor.upgrade() { - Some(i) => i, - None => return Err(Error::ErrInterceptorNotBind), - }; - - let stream_info = create_stream_info( - "".to_owned(), - ssrc, - params.codecs[0].payload_type, - params.codecs[0].capability.clone(), - ¶ms.header_extensions, - ); - let (rtp_read_stream, rtp_interceptor, rtcp_read_stream, rtcp_interceptor) = self - .dtls_transport - .streams_for_ssrc(ssrc, &stream_info, &icpr) - .await?; - - let a = Attributes::new(); - for _ in 0..=SIMULCAST_PROBE_COUNT { - if mid.is_empty() || (rid.is_empty() && rsid.is_empty()) { - let (pkt, _) = rtp_interceptor.read(&mut buf, &a).await?; - let (m, r, rs, _) = handle_unknown_rtp_packet( - &buf[..n], - mid_extension_id as u8, - sid_extension_id as u8, - rsid_extension_id as u8, - )?; - mid = m; - rid = r; - rsid = rs; - - buffered_packets.push_back((pkt, a.clone())); - continue; - } - - let transceivers = self.rtp_transceivers.lock().await; - for t in &*transceivers { - if t.mid().as_ref() != Some(&SmolStr::from(&mid)) { - continue; - } - - let receiver = t.receiver().await; - - if !rsid.is_empty() { - return receiver - .receive_for_rtx( - 0, - rsid, - TrackStream { - stream_info: Some(stream_info.clone()), - rtp_read_stream: Some(rtp_read_stream), - rtp_interceptor: Some(rtp_interceptor), - rtcp_read_stream: Some(rtcp_read_stream), - rtcp_interceptor: Some(rtcp_interceptor), - }, - ) - .await; - } - - let track = receiver - .receive_for_rid( - SmolStr::from(rid), - params, - TrackStream { - stream_info: Some(stream_info.clone()), - rtp_read_stream: Some(rtp_read_stream), - rtp_interceptor: Some(rtp_interceptor), - rtcp_read_stream: Some(rtcp_read_stream), - rtcp_interceptor: Some(rtcp_interceptor), - }, - ) - .await?; - track.prepopulate_peeked_data(buffered_packets).await; - - RTCPeerConnection::do_track( - Arc::clone(&self.on_track_handler), - track, - receiver, - Arc::clone(t), - ); - return Ok(()); - } - } - - let _ = rtp_read_stream.close().await; - let _ = rtcp_read_stream.close().await; - icpr.unbind_remote_stream(&stream_info).await; - self.dtls_transport.remove_simulcast_stream(ssrc).await; - - Err(Error::ErrPeerConnSimulcastIncomingSSRCFailed) - } - - async fn start_receiver( - receive_mtu: usize, - incoming: &TrackDetails, - receiver: Arc, - transceiver: Arc, - on_track_handler: Arc>>, - ) { - receiver.start(incoming).await; - for track in receiver.tracks().await { - if track.ssrc() == 0 { - return; - } - - let receiver = Arc::clone(&receiver); - let transceiver = Arc::clone(&transceiver); - let on_track_handler = Arc::clone(&on_track_handler); - tokio::spawn(async move { - let mut b = vec![0u8; receive_mtu]; - let pkt = match track.peek(&mut b).await { - Ok((pkt, _)) => pkt, - Err(err) => { - log::warn!( - "Could not determine PayloadType for SSRC {} ({})", - track.ssrc(), - err - ); - return; - } - }; - - if let Err(err) = track.check_and_update_track(&pkt).await { - log::warn!( - "Failed to set codec settings for track SSRC {} ({})", - track.ssrc(), - err - ); - return; - } - - RTCPeerConnection::do_track(on_track_handler, track, receiver, transceiver); - }); - } - } - - pub(super) async fn create_ice_transport(&self, api: &API) -> Arc { - let ice_transport = Arc::new(api.new_ice_transport(Arc::clone(&self.ice_gatherer))); - - let ice_connection_state = Arc::clone(&self.ice_connection_state); - let peer_connection_state = Arc::clone(&self.peer_connection_state); - let is_closed = Arc::clone(&self.is_closed); - let dtls_transport = Arc::clone(&self.dtls_transport); - let on_ice_connection_state_change_handler = - Arc::clone(&self.on_ice_connection_state_change_handler); - let on_peer_connection_state_change_handler = - Arc::clone(&self.on_peer_connection_state_change_handler); - - ice_transport.on_connection_state_change(Box::new(move |state: RTCIceTransportState| { - let cs = match state { - RTCIceTransportState::New => RTCIceConnectionState::New, - RTCIceTransportState::Checking => RTCIceConnectionState::Checking, - RTCIceTransportState::Connected => RTCIceConnectionState::Connected, - RTCIceTransportState::Completed => RTCIceConnectionState::Completed, - RTCIceTransportState::Failed => RTCIceConnectionState::Failed, - RTCIceTransportState::Disconnected => RTCIceConnectionState::Disconnected, - RTCIceTransportState::Closed => RTCIceConnectionState::Closed, - _ => { - log::warn!("on_connection_state_change: unhandled ICE state: {}", state); - return Box::pin(async {}); - } - }; - - let ice_connection_state2 = Arc::clone(&ice_connection_state); - let on_ice_connection_state_change_handler2 = - Arc::clone(&on_ice_connection_state_change_handler); - let on_peer_connection_state_change_handler2 = - Arc::clone(&on_peer_connection_state_change_handler); - let is_closed2 = Arc::clone(&is_closed); - let dtls_transport_state = dtls_transport.state(); - let peer_connection_state2 = Arc::clone(&peer_connection_state); - Box::pin(async move { - RTCPeerConnection::do_ice_connection_state_change( - &on_ice_connection_state_change_handler2, - &ice_connection_state2, - cs, - ) - .await; - - RTCPeerConnection::update_connection_state( - &on_peer_connection_state_change_handler2, - &is_closed2, - &peer_connection_state2, - cs, - dtls_transport_state, - ) - .await; - }) - })); - - ice_transport - } - - /// has_local_description_changed returns whether local media (rtp_transceivers) has changed - /// caller of this method should hold `pc.mu` lock - pub(super) async fn has_local_description_changed(&self, desc: &RTCSessionDescription) -> bool { - let rtp_transceivers = self.rtp_transceivers.lock().await; - for t in &*rtp_transceivers { - let m = match t.mid().and_then(|mid| get_by_mid(mid.as_str(), desc)) { - Some(m) => m, - None => return true, - }; - - if get_peer_direction(m) != t.direction() { - return true; - } - } - false - } - - pub(super) async fn get_stats(&self, stats_id: String) -> StatsCollector { - let collector = StatsCollector::new(); - let transceivers = { self.rtp_transceivers.lock().await.clone() }; - - tokio::join!( - self.ice_gatherer.collect_stats(&collector), - self.ice_transport.collect_stats(&collector), - self.sctp_transport.collect_stats(&collector, stats_id), - self.dtls_transport.collect_stats(&collector), - self.media_engine.collect_stats(&collector), - self.collect_inbound_stats(&collector, transceivers.clone()), - self.collect_outbound_stats(&collector, transceivers) - ); - - collector - } - - async fn collect_inbound_stats( - &self, - collector: &StatsCollector, - transceivers: Vec>, - ) { - // TODO: There's a lot of await points here that could run concurrently with `futures::join_all`. - struct TrackInfo { - ssrc: SSRC, - mid: SmolStr, - track_id: String, - kind: &'static str, - } - let mut track_infos = vec![]; - for transeiver in transceivers { - let receiver = transeiver.receiver().await; - - if let Some(mid) = transeiver.mid() { - let tracks = receiver.tracks().await; - - for track in tracks { - let track_id = track.id(); - let kind = match track.kind() { - RTPCodecType::Unspecified => continue, - RTPCodecType::Audio => "audio", - RTPCodecType::Video => "video", - }; - - track_infos.push(TrackInfo { - ssrc: track.ssrc(), - mid: mid.clone(), - track_id, - kind, - }); - } - } - } - - let stream_stats = self - .stats_interceptor - .fetch_inbound_stats(track_infos.iter().map(|t| t.ssrc).collect()) - .await; - - for (stats, info) in - (stream_stats.into_iter().zip(track_infos)).filter_map(|(s, i)| s.map(|s| (s, i))) - { - let ssrc = info.ssrc; - let kind = info.kind; - - let id = format!("RTCInboundRTP{}Stream_{}", capitalize(kind), ssrc); - let ( - packets_received, - header_bytes_received, - bytes_received, - last_packet_received_timestamp, - nack_count, - remote_packets_sent, - remote_bytes_sent, - remote_reports_sent, - remote_round_trip_time, - remote_total_round_trip_time, - remote_round_trip_time_measurements, - ) = ( - stats.packets_received(), - stats.header_bytes_received(), - stats.payload_bytes_received(), - stats.last_packet_received_timestamp(), - stats.nacks_sent(), - stats.remote_packets_sent(), - stats.remote_bytes_sent(), - stats.remote_reports_sent(), - stats.remote_round_trip_time(), - stats.remote_total_round_trip_time(), - stats.remote_round_trip_time_measurements(), - ); - - collector.insert( - id.clone(), - crate::stats::StatsReportType::InboundRTP(InboundRTPStats { - timestamp: Instant::now(), - stats_type: RTCStatsType::InboundRTP, - id: id.clone(), - ssrc, - kind: kind.to_owned(), - packets_received, - track_identifier: info.track_id, - mid: info.mid, - last_packet_received_timestamp, - header_bytes_received, - bytes_received, - nack_count, - - fir_count: (info.kind == "video").then(|| stats.firs_sent()), - pli_count: (info.kind == "video").then(|| stats.plis_sent()), - }), - ); - - let local_id = id; - let id = format!( - "RTCRemoteOutboundRTP{}Stream_{}", - capitalize(info.kind), - info.ssrc - ); - collector.insert( - id.clone(), - crate::stats::StatsReportType::RemoteOutboundRTP(RemoteOutboundRTPStats { - timestamp: Instant::now(), - stats_type: RTCStatsType::RemoteOutboundRTP, - id, - - ssrc, - kind: kind.to_owned(), - - packets_sent: remote_packets_sent as u64, - bytes_sent: remote_bytes_sent as u64, - local_id, - reports_sent: remote_reports_sent, - round_trip_time: remote_round_trip_time, - total_round_trip_time: remote_total_round_trip_time, - round_trip_time_measurements: remote_round_trip_time_measurements, - }), - ); - } - } - - async fn collect_outbound_stats( - &self, - collector: &StatsCollector, - transceivers: Vec>, - ) { - // TODO: There's a lot of await points here that could run concurrently with `futures::join_all`. - struct TrackInfo { - track_id: String, - ssrc: SSRC, - mid: SmolStr, - rid: Option, - kind: &'static str, - } - let mut track_infos = vec![]; - for transceiver in transceivers { - let mid = match transceiver.mid() { - Some(mid) => mid, - None => continue, - }; - - let sender = transceiver.sender().await; - let track_encodings = sender.track_encodings.lock().await; - for encoding in track_encodings.iter() { - let track_id = encoding.track.id().to_string(); - let kind = match encoding.track.kind() { - RTPCodecType::Unspecified => continue, - RTPCodecType::Audio => "audio", - RTPCodecType::Video => "video", - }; - - track_infos.push(TrackInfo { - track_id, - ssrc: encoding.ssrc, - mid: mid.to_owned(), - rid: encoding.track.rid().map(Into::into), - kind, - }); - } - } - - let stream_stats = self - .stats_interceptor - .fetch_outbound_stats(track_infos.iter().map(|t| t.ssrc).collect()) - .await; - - for (stats, info) in stream_stats - .into_iter() - .zip(track_infos) - .filter_map(|(s, i)| s.map(|s| (s, i))) - { - // RTCOutboundRtpStreamStats - let id = format!( - "RTCOutboundRTP{}Stream_{}", - capitalize(info.kind), - info.ssrc - ); - let ( - packets_sent, - bytes_sent, - header_bytes_sent, - nack_count, - remote_inbound_packets_received, - remote_inbound_packets_lost, - remote_rtt_ms, - remote_total_rtt_ms, - remote_rtt_measurements, - remote_fraction_lost, - ) = ( - stats.packets_sent(), - stats.payload_bytes_sent(), - stats.header_bytes_sent(), - stats.nacks_received(), - stats.remote_packets_received(), - stats.remote_total_lost(), - stats.remote_round_trip_time(), - stats.remote_total_round_trip_time(), - stats.remote_round_trip_time_measurements(), - stats.remote_fraction_lost(), - ); - - let TrackInfo { - mid, - ssrc, - rid, - kind, - track_id: track_identifier, - } = info; - - collector.insert( - id.clone(), - crate::stats::StatsReportType::OutboundRTP(OutboundRTPStats { - timestamp: Instant::now(), - stats_type: RTCStatsType::OutboundRTP, - track_identifier, - id: id.clone(), - ssrc, - kind: kind.to_owned(), - packets_sent, - mid, - rid, - header_bytes_sent, - bytes_sent, - nack_count, - - fir_count: (info.kind == "video").then(|| stats.firs_received()), - pli_count: (info.kind == "video").then(|| stats.plis_received()), - }), - ); - - let local_id = id; - let id = format!( - "RTCRemoteInboundRTP{}Stream_{}", - capitalize(info.kind), - info.ssrc - ); - - collector.insert( - id.clone(), - StatsReportType::RemoteInboundRTP(RemoteInboundRTPStats { - timestamp: Instant::now(), - stats_type: RTCStatsType::RemoteInboundRTP, - id, - ssrc, - kind: kind.to_owned(), - - packets_received: remote_inbound_packets_received, - packets_lost: remote_inbound_packets_lost as i64, - - local_id, - - round_trip_time: remote_rtt_ms, - total_round_trip_time: remote_total_rtt_ms, - fraction_lost: remote_fraction_lost.unwrap_or(0.0), - round_trip_time_measurements: remote_rtt_measurements, - }), - ); - } - } -} - -type IResult = std::result::Result; - -#[async_trait] -impl RTCPWriter for PeerConnectionInternal { - async fn write( - &self, - pkts: &[Box], - _a: &Attributes, - ) -> IResult { - Ok(self.dtls_transport.write_rtcp(pkts).await?) - } -} - -fn capitalize(s: &str) -> String { - let first = s - .chars() - .next() - .expect("Must have at least one character to uppercase") - .to_uppercase(); - let mut result = String::new(); - - result.extend(first); - result.extend(s.chars().skip(1)); - - result -} diff --git a/webrtc/src/peer_connection/peer_connection_state.rs b/webrtc/src/peer_connection/peer_connection_state.rs deleted file mode 100644 index 905e26769..000000000 --- a/webrtc/src/peer_connection/peer_connection_state.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::fmt; - -/// PeerConnectionState indicates the state of the PeerConnection. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCPeerConnectionState { - #[default] - Unspecified, - - /// PeerConnectionStateNew indicates that any of the ICETransports or - /// DTLSTransports are in the "new" state and none of the transports are - /// in the "connecting", "checking", "failed" or "disconnected" state, or - /// all transports are in the "closed" state, or there are no transports. - New, - - /// PeerConnectionStateConnecting indicates that any of the - /// ICETransports or DTLSTransports are in the "connecting" or - /// "checking" state and none of them is in the "failed" state. - Connecting, - - /// PeerConnectionStateConnected indicates that all ICETransports and - /// DTLSTransports are in the "connected", "completed" or "closed" state - /// and at least one of them is in the "connected" or "completed" state. - Connected, - - /// PeerConnectionStateDisconnected indicates that any of the - /// ICETransports or DTLSTransports are in the "disconnected" state - /// and none of them are in the "failed" or "connecting" or "checking" state. - Disconnected, - - /// PeerConnectionStateFailed indicates that any of the ICETransports - /// or DTLSTransports are in a "failed" state. - Failed, - - /// PeerConnectionStateClosed indicates the peer connection is closed - /// and the isClosed member variable of PeerConnection is true. - Closed, -} - -const PEER_CONNECTION_STATE_NEW_STR: &str = "new"; -const PEER_CONNECTION_STATE_CONNECTING_STR: &str = "connecting"; -const PEER_CONNECTION_STATE_CONNECTED_STR: &str = "connected"; -const PEER_CONNECTION_STATE_DISCONNECTED_STR: &str = "disconnected"; -const PEER_CONNECTION_STATE_FAILED_STR: &str = "failed"; -const PEER_CONNECTION_STATE_CLOSED_STR: &str = "closed"; - -impl From<&str> for RTCPeerConnectionState { - fn from(raw: &str) -> Self { - match raw { - PEER_CONNECTION_STATE_NEW_STR => RTCPeerConnectionState::New, - PEER_CONNECTION_STATE_CONNECTING_STR => RTCPeerConnectionState::Connecting, - PEER_CONNECTION_STATE_CONNECTED_STR => RTCPeerConnectionState::Connected, - PEER_CONNECTION_STATE_DISCONNECTED_STR => RTCPeerConnectionState::Disconnected, - PEER_CONNECTION_STATE_FAILED_STR => RTCPeerConnectionState::Failed, - PEER_CONNECTION_STATE_CLOSED_STR => RTCPeerConnectionState::Closed, - _ => RTCPeerConnectionState::Unspecified, - } - } -} - -impl From for RTCPeerConnectionState { - fn from(v: u8) -> Self { - match v { - 1 => RTCPeerConnectionState::New, - 2 => RTCPeerConnectionState::Connecting, - 3 => RTCPeerConnectionState::Connected, - 4 => RTCPeerConnectionState::Disconnected, - 5 => RTCPeerConnectionState::Failed, - 6 => RTCPeerConnectionState::Closed, - _ => RTCPeerConnectionState::Unspecified, - } - } -} - -impl fmt::Display for RTCPeerConnectionState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCPeerConnectionState::New => PEER_CONNECTION_STATE_NEW_STR, - RTCPeerConnectionState::Connecting => PEER_CONNECTION_STATE_CONNECTING_STR, - RTCPeerConnectionState::Connected => PEER_CONNECTION_STATE_CONNECTED_STR, - RTCPeerConnectionState::Disconnected => PEER_CONNECTION_STATE_DISCONNECTED_STR, - RTCPeerConnectionState::Failed => PEER_CONNECTION_STATE_FAILED_STR, - RTCPeerConnectionState::Closed => PEER_CONNECTION_STATE_CLOSED_STR, - RTCPeerConnectionState::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum NegotiationNeededState { - /// NegotiationNeededStateEmpty not running and queue is empty - #[default] - Empty, - /// NegotiationNeededStateEmpty running and queue is empty - Run, - /// NegotiationNeededStateEmpty running and queue - Queue, -} - -impl From for NegotiationNeededState { - fn from(v: u8) -> Self { - match v { - 1 => NegotiationNeededState::Run, - 2 => NegotiationNeededState::Queue, - _ => NegotiationNeededState::Empty, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_peer_connection_state() { - let tests = vec![ - (crate::UNSPECIFIED_STR, RTCPeerConnectionState::Unspecified), - ("new", RTCPeerConnectionState::New), - ("connecting", RTCPeerConnectionState::Connecting), - ("connected", RTCPeerConnectionState::Connected), - ("disconnected", RTCPeerConnectionState::Disconnected), - ("failed", RTCPeerConnectionState::Failed), - ("closed", RTCPeerConnectionState::Closed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!( - RTCPeerConnectionState::from(state_string), - expected_state, - "testCase: {expected_state}", - ); - } - } - - #[test] - fn test_peer_connection_state_string() { - let tests = vec![ - (RTCPeerConnectionState::Unspecified, crate::UNSPECIFIED_STR), - (RTCPeerConnectionState::New, "new"), - (RTCPeerConnectionState::Connecting, "connecting"), - (RTCPeerConnectionState::Connected, "connected"), - (RTCPeerConnectionState::Disconnected, "disconnected"), - (RTCPeerConnectionState::Failed, "failed"), - (RTCPeerConnectionState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string) - } - } -} diff --git a/webrtc/src/peer_connection/peer_connection_test.rs b/webrtc/src/peer_connection/peer_connection_test.rs deleted file mode 100644 index 2c9676037..000000000 --- a/webrtc/src/peer_connection/peer_connection_test.rs +++ /dev/null @@ -1,640 +0,0 @@ -use std::sync::Arc; - -use bytes::Bytes; -use interceptor::registry::Registry; -use media::Sample; -use portable_atomic::AtomicU32; -use tokio::time::Duration; -use util::vnet::net::{Net, NetConfig}; -use util::vnet::router::{Router, RouterConfig}; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::interceptor_registry::register_default_interceptors; -use crate::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use crate::api::APIBuilder; -use crate::ice_transport::ice_candidate_pair::RTCIceCandidatePair; -use crate::ice_transport::ice_credential_type::RTCIceCredentialType; -use crate::ice_transport::ice_server::RTCIceServer; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use crate::stats::StatsReportType; -use crate::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; -use crate::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use crate::Error; - -pub(crate) async fn create_vnet_pair( -) -> Result<(RTCPeerConnection, RTCPeerConnection, Arc>)> { - // Create a root router - let wan = Arc::new(Mutex::new(Router::new(RouterConfig { - cidr: "1.2.3.0/24".to_owned(), - ..Default::default() - })?)); - - // Create a network interface for offerer - let offer_vnet = Arc::new(Net::new(Some(NetConfig { - static_ips: vec!["1.2.3.4".to_owned()], - ..Default::default() - }))); - - // Add the network interface to the router - let nic = offer_vnet.get_nic()?; - { - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - let mut offer_setting_engine = SettingEngine::default(); - offer_setting_engine.set_vnet(Some(offer_vnet)); - offer_setting_engine.set_ice_timeouts( - Some(Duration::from_secs(1)), - Some(Duration::from_secs(1)), - Some(Duration::from_millis(200)), - ); - - // Create a network interface for answerer - let answer_vnet = Arc::new(Net::new(Some(NetConfig { - static_ips: vec!["1.2.3.5".to_owned()], - ..Default::default() - }))); - - // Add the network interface to the router - let nic = answer_vnet.get_nic()?; - { - let mut w = wan.lock().await; - w.add_net(Arc::clone(&nic)).await?; - } - { - let n = nic.lock().await; - n.set_router(Arc::clone(&wan)).await?; - } - - let mut answer_setting_engine = SettingEngine::default(); - answer_setting_engine.set_vnet(Some(answer_vnet)); - answer_setting_engine.set_ice_timeouts( - Some(Duration::from_secs(1)), - Some(Duration::from_secs(1)), - Some(Duration::from_millis(200)), - ); - - // Start the virtual network by calling Start() on the root router - { - let mut w = wan.lock().await; - w.start().await?; - } - - let mut offer_media_engine = MediaEngine::default(); - offer_media_engine.register_default_codecs()?; - let offer_peer_connection = APIBuilder::new() - .with_setting_engine(offer_setting_engine) - .with_media_engine(offer_media_engine) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - let mut answer_media_engine = MediaEngine::default(); - answer_media_engine.register_default_codecs()?; - let answer_peer_connection = APIBuilder::new() - .with_setting_engine(answer_setting_engine) - .with_media_engine(answer_media_engine) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - Ok((offer_peer_connection, answer_peer_connection, wan)) -} - -/// new_pair creates two new peer connections (an offerer and an answerer) -/// *without* using an api (i.e. using the default settings). -pub(crate) async fn new_pair(api: &API) -> Result<(RTCPeerConnection, RTCPeerConnection)> { - let pca = api.new_peer_connection(RTCConfiguration::default()).await?; - let pcb = api.new_peer_connection(RTCConfiguration::default()).await?; - - Ok((pca, pcb)) -} - -pub(crate) async fn signal_pair( - pc_offer: &mut RTCPeerConnection, - pc_answer: &mut RTCPeerConnection, -) -> Result<()> { - // Note(albrow): We need to create a data channel in order to trigger ICE - // candidate gathering in the background for the JavaScript/Wasm bindings. If - // we don't do this, the complete offer including ICE candidates will never be - // generated. - pc_offer - .create_data_channel("initial_data_channel", None) - .await?; - - let offer = pc_offer.create_offer(None).await?; - - let mut offer_gathering_complete = pc_offer.gathering_complete_promise().await; - pc_offer.set_local_description(offer).await?; - - let _ = offer_gathering_complete.recv().await; - - pc_answer - .set_remote_description( - pc_offer - .local_description() - .await - .ok_or(Error::new("non local description".to_owned()))?, - ) - .await?; - - let answer = pc_answer.create_answer(None).await?; - - let mut answer_gathering_complete = pc_answer.gathering_complete_promise().await; - pc_answer.set_local_description(answer).await?; - - let _ = answer_gathering_complete.recv().await; - - pc_offer - .set_remote_description( - pc_answer - .local_description() - .await - .ok_or(Error::new("non local description".to_owned()))?, - ) - .await -} - -pub(crate) async fn close_pair_now(pc1: &RTCPeerConnection, pc2: &RTCPeerConnection) { - let mut fail = false; - if let Err(err) = pc1.close().await { - log::error!("Failed to close PeerConnection: {}", err); - fail = true; - } - if let Err(err) = pc2.close().await { - log::error!("Failed to close PeerConnection: {}", err); - fail = true; - } - - assert!(!fail); -} - -pub(crate) async fn close_pair( - pc1: &RTCPeerConnection, - pc2: &RTCPeerConnection, - mut done_rx: mpsc::Receiver<()>, -) { - let timeout = tokio::time::sleep(Duration::from_secs(10)); - tokio::pin!(timeout); - - tokio::select! { - _ = timeout.as_mut() =>{ - panic!("close_pair timed out waiting for done signal"); - } - _ = done_rx.recv() =>{ - close_pair_now(pc1, pc2).await; - } - } -} - -/* -func offerMediaHasDirection(offer SessionDescription, kind RTPCodecType, direction RTPTransceiverDirection) bool { - parsed := &sdp.SessionDescription{} - if err := parsed.Unmarshal([]byte(offer.SDP)); err != nil { - return false - } - - for _, media := range parsed.MediaDescriptions { - if media.MediaName.Media == kind.String() { - _, exists := media.Attribute(direction.String()) - return exists - } - } - return false -}*/ - -pub(crate) async fn send_video_until_done( - mut done_rx: mpsc::Receiver<()>, - tracks: Vec>, - data: Bytes, - max_sends: Option, -) -> bool { - let mut sends = 0; - - loop { - let timeout = tokio::time::sleep(Duration::from_millis(20)); - tokio::pin!(timeout); - - tokio::select! { - biased; - - _ = done_rx.recv() =>{ - log::debug!("sendVideoUntilDone received done"); - return false; - } - - _ = timeout.as_mut() =>{ - if max_sends.map(|s| sends >= s).unwrap_or(false) { - continue; - } - - log::debug!("sendVideoUntilDone timeout"); - for track in &tracks { - log::debug!("sendVideoUntilDone track.WriteSample"); - let result = track.write_sample(&Sample{ - data: data.clone(), - duration: Duration::from_secs(1), - ..Default::default() - }).await; - assert!(result.is_ok()); - sends += 1; - } - } - } - } -} - -pub(crate) async fn until_connection_state( - pc: &mut RTCPeerConnection, - wg: &WaitGroup, - state: RTCPeerConnectionState, -) { - let w = Arc::new(Mutex::new(Some(wg.worker()))); - pc.on_peer_connection_state_change(Box::new(move |pcs: RTCPeerConnectionState| { - let w2 = Arc::clone(&w); - Box::pin(async move { - if pcs == state { - let mut worker = w2.lock().await; - worker.take(); - } - }) - })); -} - -#[tokio::test] -async fn test_get_stats() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_offer, mut pc_answer) = new_pair(&api).await?; - - let (ice_complete_tx, mut ice_complete_rx) = mpsc::channel::<()>(1); - let ice_complete_tx = Arc::new(Mutex::new(Some(ice_complete_tx))); - pc_answer.on_ice_connection_state_change(Box::new(move |ice_state: RTCIceConnectionState| { - let ice_complete_tx2 = Arc::clone(&ice_complete_tx); - Box::pin(async move { - if ice_state == RTCIceConnectionState::Connected { - tokio::time::sleep(Duration::from_secs(1)).await; - let mut done = ice_complete_tx2.lock().await; - done.take(); - } - }) - })); - - let sender_called_candidate_change = Arc::new(AtomicU32::new(0)); - let sender_called_candidate_change2 = Arc::clone(&sender_called_candidate_change); - pc_offer - .sctp() - .transport() - .ice_transport() - .on_selected_candidate_pair_change(Box::new(move |_: RTCIceCandidatePair| { - sender_called_candidate_change2.store(1, Ordering::SeqCst); - Box::pin(async {}) - })); - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - pc_offer - .add_track(track.clone()) - .await - .expect("Failed to add track"); - let (packet_tx, packet_rx) = mpsc::channel(1); - - pc_answer.on_track(Box::new(move |track, _, _| { - let packet_tx = packet_tx.clone(); - tokio::spawn(async move { - while let Ok((pkt, _)) = track.read_rtp().await { - dbg!(&pkt); - let last = pkt.payload[pkt.payload.len() - 1]; - - if last == 0xAA { - let _ = packet_tx.send(()).await; - break; - } - } - }); - - Box::pin(async move {}) - })); - - signal_pair(&mut pc_offer, &mut pc_answer).await?; - - let _ = ice_complete_rx.recv().await; - send_video_until_done( - packet_rx, - vec![track], - Bytes::from_static(b"\xDE\xAD\xBE\xEF\xAA"), - Some(1), - ) - .await; - - let offer_stats = pc_offer.get_stats().await; - assert!(!offer_stats.reports.is_empty()); - - match offer_stats.reports.get("ice_transport") { - Some(StatsReportType::Transport(ice_transport_stats)) => { - assert!(ice_transport_stats.bytes_received > 0); - assert!(ice_transport_stats.bytes_sent > 0); - } - Some(_other) => panic!("found the wrong type"), - None => panic!("missed it"), - } - let outbound_stats = offer_stats - .reports - .values() - .find_map(|v| match v { - StatsReportType::OutboundRTP(d) => Some(d), - _ => None, - }) - .expect("Should have produced an RTP Outbound stat"); - assert_eq!(outbound_stats.packets_sent, 1); - assert_eq!(outbound_stats.kind, "video"); - assert_eq!(outbound_stats.bytes_sent, 8); - assert_eq!(outbound_stats.header_bytes_sent, 12); - - let answer_stats = pc_answer.get_stats().await; - let inbound_stats = answer_stats - .reports - .values() - .find_map(|v| match v { - StatsReportType::InboundRTP(d) => Some(d), - _ => None, - }) - .expect("Should have produced an RTP inbound stat"); - assert_eq!(inbound_stats.packets_received, 1); - assert_eq!(inbound_stats.kind, "video"); - assert_eq!(inbound_stats.bytes_received, 8); - assert_eq!(inbound_stats.header_bytes_received, 12); - - close_pair_now(&pc_offer, &pc_answer).await; - - Ok(()) -} - -#[tokio::test] -async fn test_peer_connection_close_is_send() -> Result<()> { - let handle = tokio::spawn(async move { peer().await }); - tokio::join!(handle).0.unwrap() -} - -#[tokio::test] -async fn test_set_get_configuration() { - // initialize MediaEngine and InterceptorRegistry - let media_engine = MediaEngine::default(); - let registry = Registry::default(); - - // create API instance - let api = APIBuilder::new() - .with_media_engine(media_engine) - .with_interceptor_registry(registry) - .build(); - - // create configuration - let initial_config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_string()], - username: "".to_string(), - credential: "".to_string(), - credential_type: RTCIceCredentialType::Unspecified, - }], - ..Default::default() - }; - - // create RTCPeerConnection instance - let peer = Arc::new( - api.new_peer_connection(initial_config.clone()) - .await - .expect("Failed to create RTCPeerConnection"), - ); - - // get configuration and println - let config_before = peer.get_configuration().await; - println!("Initial ICE Servers: {:?}", config_before.ice_servers); - println!( - "Initial ICE Transport Policy: {:?}", - config_before.ice_transport_policy - ); - println!("Initial Bundle Policy: {:?}", config_before.bundle_policy); - println!( - "Initial RTCP Mux Policy: {:?}", - config_before.rtcp_mux_policy - ); - println!("Initial Peer Identity: {:?}", config_before.peer_identity); - println!("Initial Certificates: {:?}", config_before.certificates); - println!( - "Initial ICE Candidate Pool Size: {:?}", - config_before.ice_candidate_pool_size - ); - - // create new configuration - let new_config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec![ - "turn:turn.22333.fun".to_string(), - "turn:cn.22333.fun".to_string(), - ], - username: "live777".to_string(), - credential: "live777".to_string(), - credential_type: RTCIceCredentialType::Password, - }], - ..Default::default() - }; - - // set new configuration - peer.set_configuration(new_config.clone()) - .await - .expect("Failed to set configuration"); - - // get new configuration and println - let updated_config = peer.get_configuration().await; - println!("Updated ICE Servers: {:?}", updated_config.ice_servers); - println!( - "Updated ICE Transport Policy: {:?}", - updated_config.ice_transport_policy - ); - println!("Updated Bundle Policy: {:?}", updated_config.bundle_policy); - println!( - "Updated RTCP Mux Policy: {:?}", - updated_config.rtcp_mux_policy - ); - println!("Updated Peer Identity: {:?}", updated_config.peer_identity); - println!("Updated Certificates: {:?}", updated_config.certificates); - println!( - "Updated ICE Candidate Pool Size: {:?}", - updated_config.ice_candidate_pool_size - ); - - // verify - assert_eq!(updated_config.ice_servers, new_config.ice_servers); -} - -async fn peer() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let mut registry = Registry::new(); - registry = register_default_interceptors(registry, &mut m)?; - let api = APIBuilder::new() - .with_media_engine(m) - .with_interceptor_registry(registry) - .build(); - - let config = RTCConfiguration { - ice_servers: vec![RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_owned()], - ..Default::default() - }], - ..Default::default() - }; - - let peer_connection = Arc::new(api.new_peer_connection(config).await?); - - let offer = peer_connection.create_offer(None).await?; - let mut gather_complete = peer_connection.gathering_complete_promise().await; - peer_connection.set_local_description(offer).await?; - let _ = gather_complete.recv().await; - - if peer_connection.local_description().await.is_some() { - //TODO? - } - - peer_connection.close().await?; - - Ok(()) -} - -pub(crate) fn on_connected() -> (OnPeerConnectionStateChangeHdlrFn, mpsc::Receiver<()>) { - let (done_tx, done_rx) = mpsc::channel::<()>(1); - let done_tx = Arc::new(Mutex::new(Some(done_tx))); - let hdlr_fn: OnPeerConnectionStateChangeHdlrFn = - Box::new(move |state: RTCPeerConnectionState| { - let done_tx_clone = Arc::clone(&done_tx); - Box::pin(async move { - if state == RTCPeerConnectionState::Connected { - let mut tx = done_tx_clone.lock().await; - tx.take(); - } - }) - }); - (hdlr_fn, done_rx) -} - -// Everytime we receive a new SSRC we probe it and try to determine the proper way to handle it. -// In most cases a Track explicitly declares a SSRC and a OnTrack is fired. In two cases we don't -// know the SSRC ahead of time -// * Undeclared SSRC in a single media section -// * Simulcast -// -// The Undeclared SSRC processing code would run before Simulcast. If a Simulcast Offer/Answer only -// contained one Media Section we would never fire the OnTrack. We would assume it was a failed -// Undeclared SSRC processing. This test asserts that we properly handled this. -#[tokio::test] -async fn test_peer_connection_simulcast_no_data_channel() -> Result<()> { - let mut m = MediaEngine::default(); - for ext in [ - ::sdp::extmap::SDES_MID_URI, - ::sdp::extmap::SDES_RTP_STREAM_ID_URI, - ] { - m.register_header_extension( - RTCRtpHeaderExtensionCapability { - uri: ext.to_owned(), - }, - RTPCodecType::Video, - None, - )?; - } - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_send, mut pc_recv) = new_pair(&api).await?; - let (send_notifier, mut send_connected) = on_connected(); - let (recv_notifier, mut recv_connected) = on_connected(); - pc_send.on_peer_connection_state_change(send_notifier); - pc_recv.on_peer_connection_state_change(recv_notifier); - let (track_tx, mut track_rx) = mpsc::unbounded_channel(); - pc_recv.on_track(Box::new(move |t, _, _| { - let rid = t.rid().to_owned(); - let _ = track_tx.send(rid); - Box::pin(async move {}) - })); - - let id = "video"; - let stream_id = "webrtc-rs"; - let track = Arc::new(TrackLocalStaticRTP::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - id.to_owned(), - "a".to_owned(), - stream_id.to_owned(), - )); - let track_a = Arc::clone(&track); - let transceiver = pc_send.add_transceiver_from_track(track, None).await?; - let sender = transceiver.sender().await; - - let track = Arc::new(TrackLocalStaticRTP::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - id.to_owned(), - "b".to_owned(), - stream_id.to_owned(), - )); - let track_b = Arc::clone(&track); - sender.add_encoding(track).await?; - - let track = Arc::new(TrackLocalStaticRTP::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - id.to_owned(), - "c".to_owned(), - stream_id.to_owned(), - )); - let track_c = Arc::clone(&track); - sender.add_encoding(track).await?; - - // signaling - signal_pair(&mut pc_send, &mut pc_recv).await?; - let _ = send_connected.recv().await; - let _ = recv_connected.recv().await; - - for sequence_number in [0; 100] { - let pkt = rtp::packet::Packet { - header: rtp::header::Header { - version: 2, - sequence_number, - payload_type: 96, - ..Default::default() - }, - payload: Bytes::from_static(&[0; 2]), - }; - - track_a.write_rtp_with_extensions(&pkt, &[]).await?; - track_b.write_rtp_with_extensions(&pkt, &[]).await?; - track_c.write_rtp_with_extensions(&pkt, &[]).await?; - } - - assert_eq!(track_rx.recv().await.unwrap(), "a".to_owned()); - assert_eq!(track_rx.recv().await.unwrap(), "b".to_owned()); - assert_eq!(track_rx.recv().await.unwrap(), "c".to_owned()); - - close_pair_now(&pc_send, &pc_recv).await; - - Ok(()) -} diff --git a/webrtc/src/peer_connection/policy/bundle_policy.rs b/webrtc/src/peer_connection/policy/bundle_policy.rs deleted file mode 100644 index 040228f26..000000000 --- a/webrtc/src/peer_connection/policy/bundle_policy.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// BundlePolicy affects which media tracks are negotiated if the remote -/// endpoint is not bundle-aware, and what ICE candidates are gathered. If the -/// remote endpoint is bundle-aware, all media tracks and data channels are -/// bundled onto the same transport. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub enum RTCBundlePolicy { - #[default] - Unspecified = 0, - - /// BundlePolicyBalanced indicates to gather ICE candidates for each - /// media type in use (audio, video, and data). If the remote endpoint is - /// not bundle-aware, negotiate only one audio and video track on separate - /// transports. - #[serde(rename = "balanced")] - Balanced = 1, - - /// BundlePolicyMaxCompat indicates to gather ICE candidates for each - /// track. If the remote endpoint is not bundle-aware, negotiate all media - /// tracks on separate transports. - #[serde(rename = "max-compat")] - MaxCompat = 2, - - /// BundlePolicyMaxBundle indicates to gather ICE candidates for only - /// one track. If the remote endpoint is not bundle-aware, negotiate only - /// one media track. - #[serde(rename = "max-bundle")] - MaxBundle = 3, -} - -/// This is done this way because of a linter. -const BUNDLE_POLICY_BALANCED_STR: &str = "balanced"; -const BUNDLE_POLICY_MAX_COMPAT_STR: &str = "max-compat"; -const BUNDLE_POLICY_MAX_BUNDLE_STR: &str = "max-bundle"; - -impl From<&str> for RTCBundlePolicy { - /// NewSchemeType defines a procedure for creating a new SchemeType from a raw - /// string naming the scheme type. - fn from(raw: &str) -> Self { - match raw { - BUNDLE_POLICY_BALANCED_STR => RTCBundlePolicy::Balanced, - BUNDLE_POLICY_MAX_COMPAT_STR => RTCBundlePolicy::MaxCompat, - BUNDLE_POLICY_MAX_BUNDLE_STR => RTCBundlePolicy::MaxBundle, - _ => RTCBundlePolicy::Unspecified, - } - } -} - -impl fmt::Display for RTCBundlePolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCBundlePolicy::Balanced => write!(f, "{BUNDLE_POLICY_BALANCED_STR}"), - RTCBundlePolicy::MaxCompat => write!(f, "{BUNDLE_POLICY_MAX_COMPAT_STR}"), - RTCBundlePolicy::MaxBundle => write!(f, "{BUNDLE_POLICY_MAX_BUNDLE_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_bundle_policy() { - let tests = vec![ - ("Unspecified", RTCBundlePolicy::Unspecified), - ("balanced", RTCBundlePolicy::Balanced), - ("max-compat", RTCBundlePolicy::MaxCompat), - ("max-bundle", RTCBundlePolicy::MaxBundle), - ]; - - for (policy_string, expected_policy) in tests { - assert_eq!(RTCBundlePolicy::from(policy_string), expected_policy); - } - } - - #[test] - fn test_bundle_policy_string() { - let tests = vec![ - (RTCBundlePolicy::Unspecified, "Unspecified"), - (RTCBundlePolicy::Balanced, "balanced"), - (RTCBundlePolicy::MaxCompat, "max-compat"), - (RTCBundlePolicy::MaxBundle, "max-bundle"), - ]; - - for (policy, expected_string) in tests { - assert_eq!(policy.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/peer_connection/policy/ice_transport_policy.rs b/webrtc/src/peer_connection/policy/ice_transport_policy.rs deleted file mode 100644 index 331ebe60b..000000000 --- a/webrtc/src/peer_connection/policy/ice_transport_policy.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// ICETransportPolicy defines the ICE candidate policy surface the -/// permitted candidates. Only these candidates are used for connectivity checks. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub enum RTCIceTransportPolicy { - #[default] - Unspecified = 0, - - /// ICETransportPolicyAll indicates any type of candidate is used. - #[serde(rename = "all")] - All = 1, - - /// ICETransportPolicyRelay indicates only media relay candidates such - /// as candidates passing through a TURN server are used. - #[serde(rename = "relay")] - Relay = 2, -} - -/// ICEGatherPolicy is the ORTC equivalent of ICETransportPolicy -pub type ICEGatherPolicy = RTCIceTransportPolicy; - -const ICE_TRANSPORT_POLICY_RELAY_STR: &str = "relay"; -const ICE_TRANSPORT_POLICY_ALL_STR: &str = "all"; - -/// takes a string and converts it to ICETransportPolicy -impl From<&str> for RTCIceTransportPolicy { - fn from(raw: &str) -> Self { - match raw { - ICE_TRANSPORT_POLICY_RELAY_STR => RTCIceTransportPolicy::Relay, - ICE_TRANSPORT_POLICY_ALL_STR => RTCIceTransportPolicy::All, - _ => RTCIceTransportPolicy::Unspecified, - } - } -} - -impl fmt::Display for RTCIceTransportPolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCIceTransportPolicy::Relay => ICE_TRANSPORT_POLICY_RELAY_STR, - RTCIceTransportPolicy::All => ICE_TRANSPORT_POLICY_ALL_STR, - RTCIceTransportPolicy::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_ice_transport_policy() { - let tests = vec![ - ("relay", RTCIceTransportPolicy::Relay), - ("all", RTCIceTransportPolicy::All), - ]; - - for (policy_string, expected_policy) in tests { - assert_eq!(RTCIceTransportPolicy::from(policy_string), expected_policy); - } - } - - #[test] - fn test_ice_transport_policy_string() { - let tests = vec![ - (RTCIceTransportPolicy::Relay, "relay"), - (RTCIceTransportPolicy::All, "all"), - ]; - - for (policy, expected_string) in tests { - assert_eq!(policy.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/peer_connection/policy/mod.rs b/webrtc/src/peer_connection/policy/mod.rs deleted file mode 100644 index 82036c518..000000000 --- a/webrtc/src/peer_connection/policy/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod bundle_policy; -pub mod ice_transport_policy; -pub mod rtcp_mux_policy; -pub mod sdp_semantics; diff --git a/webrtc/src/peer_connection/policy/rtcp_mux_policy.rs b/webrtc/src/peer_connection/policy/rtcp_mux_policy.rs deleted file mode 100644 index 35ecccaae..000000000 --- a/webrtc/src/peer_connection/policy/rtcp_mux_policy.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// RTCPMuxPolicy affects what ICE candidates are gathered to support -/// non-multiplexed RTCP. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub enum RTCRtcpMuxPolicy { - #[default] - Unspecified = 0, - - /// RTCPMuxPolicyNegotiate indicates to gather ICE candidates for both - /// RTP and RTCP candidates. If the remote-endpoint is capable of - /// multiplexing RTCP, multiplex RTCP on the RTP candidates. If it is not, - /// use both the RTP and RTCP candidates separately. - #[serde(rename = "negotiate")] - Negotiate = 1, - - /// RTCPMuxPolicyRequire indicates to gather ICE candidates only for - /// RTP and multiplex RTCP on the RTP candidates. If the remote endpoint is - /// not capable of rtcp-mux, session negotiation will fail. - #[serde(rename = "require")] - Require = 2, -} - -const RTCP_MUX_POLICY_NEGOTIATE_STR: &str = "negotiate"; -const RTCP_MUX_POLICY_REQUIRE_STR: &str = "require"; - -impl From<&str> for RTCRtcpMuxPolicy { - fn from(raw: &str) -> Self { - match raw { - RTCP_MUX_POLICY_NEGOTIATE_STR => RTCRtcpMuxPolicy::Negotiate, - RTCP_MUX_POLICY_REQUIRE_STR => RTCRtcpMuxPolicy::Require, - _ => RTCRtcpMuxPolicy::Unspecified, - } - } -} - -impl fmt::Display for RTCRtcpMuxPolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCRtcpMuxPolicy::Negotiate => RTCP_MUX_POLICY_NEGOTIATE_STR, - RTCRtcpMuxPolicy::Require => RTCP_MUX_POLICY_REQUIRE_STR, - RTCRtcpMuxPolicy::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_rtcp_mux_policy() { - let tests = vec![ - ("Unspecified", RTCRtcpMuxPolicy::Unspecified), - ("negotiate", RTCRtcpMuxPolicy::Negotiate), - ("require", RTCRtcpMuxPolicy::Require), - ]; - - for (policy_string, expected_policy) in tests { - assert_eq!(RTCRtcpMuxPolicy::from(policy_string), expected_policy); - } - } - - #[test] - fn test_rtcp_mux_policy_string() { - let tests = vec![ - (RTCRtcpMuxPolicy::Unspecified, "Unspecified"), - (RTCRtcpMuxPolicy::Negotiate, "negotiate"), - (RTCRtcpMuxPolicy::Require, "require"), - ]; - - for (policy, expected_string) in tests { - assert_eq!(policy.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/peer_connection/policy/sdp_semantics.rs b/webrtc/src/peer_connection/policy/sdp_semantics.rs deleted file mode 100644 index 4fe510f11..000000000 --- a/webrtc/src/peer_connection/policy/sdp_semantics.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// SDPSemantics determines which style of SDP offers and answers -/// can be used. -/// -/// This is unused, we only support UnifiedPlan. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub enum RTCSdpSemantics { - Unspecified = 0, - - /// UnifiedPlan uses unified-plan offers and answers - /// (the default in Chrome since M72) - /// - #[serde(rename = "unified-plan")] - #[default] - UnifiedPlan = 1, - - /// PlanB uses plan-b offers and answers - /// NB: This format should be considered deprecated - /// - #[serde(rename = "plan-b")] - PlanB = 2, - - /// UnifiedPlanWithFallback prefers unified-plan - /// offers and answers, but will respond to a plan-b offer - /// with a plan-b answer - #[serde(rename = "unified-plan-with-fallback")] - UnifiedPlanWithFallback = 3, -} - -const SDP_SEMANTICS_UNIFIED_PLAN_WITH_FALLBACK: &str = "unified-plan-with-fallback"; -const SDP_SEMANTICS_UNIFIED_PLAN: &str = "unified-plan"; -const SDP_SEMANTICS_PLAN_B: &str = "plan-b"; - -impl From<&str> for RTCSdpSemantics { - fn from(raw: &str) -> Self { - match raw { - SDP_SEMANTICS_UNIFIED_PLAN_WITH_FALLBACK => RTCSdpSemantics::UnifiedPlanWithFallback, - SDP_SEMANTICS_UNIFIED_PLAN => RTCSdpSemantics::UnifiedPlan, - SDP_SEMANTICS_PLAN_B => RTCSdpSemantics::PlanB, - _ => RTCSdpSemantics::Unspecified, - } - } -} - -impl fmt::Display for RTCSdpSemantics { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCSdpSemantics::UnifiedPlanWithFallback => SDP_SEMANTICS_UNIFIED_PLAN_WITH_FALLBACK, - RTCSdpSemantics::UnifiedPlan => SDP_SEMANTICS_UNIFIED_PLAN, - RTCSdpSemantics::PlanB => SDP_SEMANTICS_PLAN_B, - RTCSdpSemantics::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use std::collections::HashSet; - - use sdp::description::media::MediaDescription; - use sdp::description::session::{SessionDescription, ATTR_KEY_SSRC}; - - use super::*; - - #[test] - fn test_sdp_semantics_string() { - let tests = vec![ - (RTCSdpSemantics::Unspecified, "Unspecified"), - ( - RTCSdpSemantics::UnifiedPlanWithFallback, - "unified-plan-with-fallback", - ), - (RTCSdpSemantics::PlanB, "plan-b"), - (RTCSdpSemantics::UnifiedPlan, "unified-plan"), - ]; - - for (value, expected_string) in tests { - assert_eq!(value.to_string(), expected_string); - } - } - - // The following tests are for non-standard SDP semantics - // (i.e. not unified-unified) - fn get_md_names(sdp: &SessionDescription) -> Vec { - sdp.media_descriptions - .iter() - .map(|md| md.media_name.media.clone()) - .collect() - } - - fn extract_ssrc_list(md: &MediaDescription) -> Vec { - let mut ssrcs = HashSet::new(); - for attr in &md.attributes { - if attr.key == ATTR_KEY_SSRC { - if let Some(value) = &attr.value { - let fields: Vec<&str> = value.split_whitespace().collect(); - if let Some(ssrc) = fields.first() { - ssrcs.insert(*ssrc); - } - } - } - } - ssrcs - .into_iter() - .map(|ssrc| ssrc.to_owned()) - .collect::>() - } -} diff --git a/webrtc/src/peer_connection/sdp/mod.rs b/webrtc/src/peer_connection/sdp/mod.rs deleted file mode 100644 index 3dff6d151..000000000 --- a/webrtc/src/peer_connection/sdp/mod.rs +++ /dev/null @@ -1,1129 +0,0 @@ -#[cfg(test)] -mod sdp_test; - -use crate::api::media_engine::MediaEngine; -use crate::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint; -use crate::error::{Error, Result}; -use crate::ice_transport::ice_candidate::RTCIceCandidate; -use crate::ice_transport::ice_gatherer::RTCIceGatherer; -use crate::ice_transport::ice_gathering_state::RTCIceGatheringState; -use crate::ice_transport::ice_parameters::RTCIceParameters; -use crate::rtp_transceiver::rtp_codec::{ - RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType, -}; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::rtp_transceiver::{PayloadType, RTCPFeedback, RTCRtpTransceiver, SSRC}; - -pub mod sdp_type; -pub mod session_description; - -use std::collections::HashMap; -use std::convert::From; -use std::io::BufReader; -use std::sync::Arc; - -use ice::candidate::candidate_base::unmarshal_candidate; -use ice::candidate::Candidate; -use sdp::description::common::{Address, ConnectionInformation}; -use sdp::description::media::{MediaDescription, MediaName, RangedPort}; -use sdp::description::session::*; -use sdp::extmap::ExtMap; -use sdp::util::ConnectionRole; -use smol_str::SmolStr; -use url::Url; - -use crate::peer_connection::MEDIA_SECTION_APPLICATION; -use crate::{SDP_ATTRIBUTE_RID, SDP_ATTRIBUTE_SIMULCAST}; - -/// TrackDetails represents any media source that can be represented in a SDP -/// This isn't keyed by SSRC because it also needs to support rid based sources -#[derive(Default, Debug, Clone)] -pub(crate) struct TrackDetails { - pub(crate) mid: SmolStr, - pub(crate) kind: RTPCodecType, - pub(crate) stream_id: String, - pub(crate) id: String, - pub(crate) ssrcs: Vec, - pub(crate) repair_ssrc: SSRC, - pub(crate) rids: Vec, -} - -pub(crate) fn track_details_for_ssrc( - track_details: &[TrackDetails], - ssrc: SSRC, -) -> Option<&TrackDetails> { - track_details.iter().find(|x| x.ssrcs.contains(&ssrc)) -} - -pub(crate) fn track_details_for_rid( - track_details: &[TrackDetails], - rid: SmolStr, -) -> Option<&TrackDetails> { - track_details.iter().find(|x| x.rids.contains(&rid)) -} - -pub(crate) fn filter_track_with_ssrc(incoming_tracks: &mut Vec, ssrc: SSRC) { - incoming_tracks.retain(|x| !x.ssrcs.contains(&ssrc)); -} - -/// extract all TrackDetails from an SDP. -pub(crate) fn track_details_from_sdp( - s: &SessionDescription, - exclude_inactive: bool, -) -> Vec { - let mut incoming_tracks = vec![]; - - for media in &s.media_descriptions { - let mut tracks_in_media_section = vec![]; - let mut rtx_repair_flows = HashMap::new(); - - let mut stream_id = ""; - let mut track_id = ""; - - // If media section is recvonly or inactive skip - if media.attribute(ATTR_KEY_RECV_ONLY).is_some() - || (exclude_inactive && media.attribute(ATTR_KEY_INACTIVE).is_some()) - { - continue; - } - - let mid_value = match get_mid_value(media) { - Some(mid_value) => mid_value, - None => continue, - }; - - let codec_type = RTPCodecType::from(media.media_name.media.as_str()); - if codec_type == RTPCodecType::Unspecified { - continue; - } - - for attr in &media.attributes { - match attr.key.as_str() { - ATTR_KEY_SSRCGROUP => { - if let Some(value) = &attr.value { - let split: Vec<&str> = value.split(' ').collect(); - if split[0] == SEMANTIC_TOKEN_FLOW_IDENTIFICATION { - // Add rtx ssrcs to blacklist, to avoid adding them as tracks - // Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section - // as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first - // (2231627014) as specified in RFC5576 - if split.len() == 3 { - let base_ssrc = match split[1].parse::() { - Ok(ssrc) => ssrc, - Err(err) => { - log::warn!("Failed to parse SSRC: {}", err); - continue; - } - }; - let rtx_repair_flow = match split[2].parse::() { - Ok(n) => n, - Err(err) => { - log::warn!("Failed to parse SSRC: {}", err); - continue; - } - }; - rtx_repair_flows.insert(rtx_repair_flow, base_ssrc); - // Remove if rtx was added as track before - filter_track_with_ssrc( - &mut tracks_in_media_section, - rtx_repair_flow as SSRC, - ); - } - } - } - } - - // Handle `a=msid: ` The first value is the same as MediaStream.id - // in the browser and can be used to figure out which tracks belong to the same stream. The browser should - // figure this out automatically when an ontrack event is emitted on RTCPeerConnection. - ATTR_KEY_MSID => { - if let Some(value) = &attr.value { - let mut split = value.split(' '); - - if let (Some(sid), Some(tid), None) = - (split.next(), split.next(), split.next()) - { - stream_id = sid; - track_id = tid; - } - } - } - - ATTR_KEY_SSRC => { - if let Some(value) = &attr.value { - let split: Vec<&str> = value.split(' ').collect(); - let ssrc = match split[0].parse::() { - Ok(ssrc) => ssrc, - Err(err) => { - log::warn!("Failed to parse SSRC: {}", err); - continue; - } - }; - - if rtx_repair_flows.contains_key(&ssrc) { - continue; // This ssrc is a RTX repair flow, ignore - } - - if split.len() == 3 && split[1].starts_with("msid:") { - stream_id = &split[1]["msid:".len()..]; - track_id = split[2]; - } - - let mut track_idx = tracks_in_media_section.len(); - - for (i, t) in tracks_in_media_section.iter().enumerate() { - if t.ssrcs.contains(&ssrc) { - track_idx = i; - //TODO: no break? - } - } - - let mut repair_ssrc = 0; - for (repair, base) in &rtx_repair_flows { - if *base == ssrc { - repair_ssrc = *repair; - //TODO: no break? - } - } - - if track_idx < tracks_in_media_section.len() { - tracks_in_media_section[track_idx].mid = SmolStr::from(mid_value); - tracks_in_media_section[track_idx].kind = codec_type; - stream_id.clone_into(&mut tracks_in_media_section[track_idx].stream_id); - track_id.clone_into(&mut tracks_in_media_section[track_idx].id); - tracks_in_media_section[track_idx].ssrcs = vec![ssrc]; - tracks_in_media_section[track_idx].repair_ssrc = repair_ssrc; - } else { - let track_details = TrackDetails { - mid: SmolStr::from(mid_value), - kind: codec_type, - stream_id: stream_id.to_owned(), - id: track_id.to_owned(), - ssrcs: vec![ssrc], - repair_ssrc, - ..Default::default() - }; - tracks_in_media_section.push(track_details); - } - } - } - _ => {} - }; - } - - // If media line is using RTP Stream Identifier Source Description per RFC8851 - // we will need to override tracks, and remove ssrcs. - // This is in particular important for Firefox, as it uses both 'rid', 'simulcast' - // and 'a=ssrc' lines. - let rids = get_rids(media); - if !rids.is_empty() && !track_id.is_empty() && !stream_id.is_empty() { - tracks_in_media_section = vec![TrackDetails { - mid: SmolStr::from(mid_value), - kind: codec_type, - stream_id: stream_id.to_owned(), - id: track_id.to_owned(), - rids: rids.iter().map(|r| SmolStr::from(&r.id)).collect(), - ..Default::default() - }]; - } - - incoming_tracks.extend(tracks_in_media_section); - } - - incoming_tracks -} - -pub(crate) fn get_rids(media: &MediaDescription) -> Vec { - let mut rids = vec![]; - let mut simulcast_attr: Option = None; - for attr in &media.attributes { - if attr.key.as_str() == SDP_ATTRIBUTE_RID { - if let Err(err) = attr - .value - .as_ref() - .ok_or(SimulcastRidParseError::SyntaxIdDirSplit) - .and_then(SimulcastRid::try_from) - .map(|rid| rids.push(rid)) - { - log::warn!("Failed to parse RID: {}", err); - } - } else if attr.key.as_str() == SDP_ATTRIBUTE_SIMULCAST { - simulcast_attr.clone_from(&attr.value); - } - } - - if let Some(attr) = simulcast_attr { - let mut split = attr.split(' '); - loop { - let _dir = split.next(); - let sc_str_list = split.next(); - if let Some(list) = sc_str_list { - let sc_list: Vec<&str> = list.split(';').flat_map(|alt| alt.split(',')).collect(); - for sc_id in sc_list { - let (sc_id, paused) = if let Some(sc_id) = sc_id.strip_prefix('~') { - (sc_id, true) - } else { - (sc_id, false) - }; - - if let Some(rid) = rids.iter_mut().find(|f| f.id == sc_id) { - rid.paused = paused; - } - } - } else { - break; - } - } - } - - rids -} - -pub(crate) async fn add_candidates_to_media_descriptions( - candidates: &[RTCIceCandidate], - mut m: MediaDescription, - ice_gathering_state: RTCIceGatheringState, -) -> Result { - let append_candidate_if_new = |c: &dyn Candidate, m: MediaDescription| -> MediaDescription { - let marshaled = c.marshal(); - for a in &m.attributes { - if let Some(value) = &a.value { - if &marshaled == value { - return m; - } - } - } - - m.with_value_attribute("candidate".to_owned(), marshaled) - }; - - for c in candidates { - let candidate = c.to_ice()?; - - candidate.set_component(1); - m = append_candidate_if_new(&candidate, m); - - candidate.set_component(2); - m = append_candidate_if_new(&candidate, m); - } - - if ice_gathering_state != RTCIceGatheringState::Complete { - return Ok(m); - } - for a in &m.attributes { - if &a.key == "end-of-candidates" { - return Ok(m); - } - } - - Ok(m.with_property_attribute("end-of-candidates".to_owned())) -} - -pub(crate) struct AddDataMediaSectionParams { - should_add_candidates: bool, - mid_value: String, - ice_params: RTCIceParameters, - dtls_role: ConnectionRole, - ice_gathering_state: RTCIceGatheringState, -} - -pub(crate) async fn add_data_media_section( - d: SessionDescription, - dtls_fingerprints: &[RTCDtlsFingerprint], - candidates: &[RTCIceCandidate], - params: AddDataMediaSectionParams, -) -> Result { - let mut media = MediaDescription { - media_name: MediaName { - media: MEDIA_SECTION_APPLICATION.to_owned(), - port: RangedPort { - value: 9, - range: None, - }, - protos: vec!["UDP".to_owned(), "DTLS".to_owned(), "SCTP".to_owned()], - formats: vec!["webrtc-datachannel".to_owned()], - }, - media_title: None, - connection_information: Some(ConnectionInformation { - network_type: "IN".to_owned(), - address_type: "IP4".to_owned(), - address: Some(Address { - address: "0.0.0.0".to_owned(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - } - .with_value_attribute( - ATTR_KEY_CONNECTION_SETUP.to_owned(), - params.dtls_role.to_string(), - ) - .with_value_attribute(ATTR_KEY_MID.to_owned(), params.mid_value) - .with_property_attribute(RTCRtpTransceiverDirection::Sendrecv.to_string()) - .with_property_attribute("sctp-port:5000".to_owned()) - .with_ice_credentials( - params.ice_params.username_fragment, - params.ice_params.password, - ); - - for f in dtls_fingerprints { - media = media.with_fingerprint(f.algorithm.clone(), f.value.to_uppercase()); - } - - if params.should_add_candidates { - media = add_candidates_to_media_descriptions(candidates, media, params.ice_gathering_state) - .await?; - } - - Ok(d.with_media(media)) -} - -pub(crate) async fn populate_local_candidates( - session_description: Option<&session_description::RTCSessionDescription>, - ice_gatherer: Option<&Arc>, - ice_gathering_state: RTCIceGatheringState, -) -> Option { - if session_description.is_none() || ice_gatherer.is_none() { - return session_description.cloned(); - } - - if let (Some(sd), Some(ice)) = (session_description, ice_gatherer) { - let candidates = match ice.get_local_candidates().await { - Ok(candidates) => candidates, - Err(_) => return Some(sd.clone()), - }; - - let mut parsed = match sd.unmarshal() { - Ok(parsed) => parsed, - Err(_) => return Some(sd.clone()), - }; - - if !parsed.media_descriptions.is_empty() { - let mut m = parsed.media_descriptions.remove(0); - m = match add_candidates_to_media_descriptions(&candidates, m, ice_gathering_state) - .await - { - Ok(m) => m, - Err(_) => return Some(sd.clone()), - }; - parsed.media_descriptions.insert(0, m); - } - - Some(session_description::RTCSessionDescription { - sdp_type: sd.sdp_type, - sdp: parsed.marshal(), - parsed: Some(parsed), - }) - } else { - None - } -} - -pub(crate) struct AddTransceiverSdpParams { - should_add_candidates: bool, - mid_value: String, - dtls_role: ConnectionRole, - ice_gathering_state: RTCIceGatheringState, - offered_direction: Option, -} - -pub(crate) async fn add_transceiver_sdp( - mut d: SessionDescription, - dtls_fingerprints: &[RTCDtlsFingerprint], - media_engine: &Arc, - ice_params: &RTCIceParameters, - candidates: &[RTCIceCandidate], - media_section: &MediaSection, - params: AddTransceiverSdpParams, -) -> Result<(SessionDescription, bool)> { - if media_section.transceivers.is_empty() { - return Err(Error::ErrSDPZeroTransceivers); - } - let (should_add_candidates, mid_value, dtls_role, ice_gathering_state) = ( - params.should_add_candidates, - params.mid_value, - params.dtls_role, - params.ice_gathering_state, - ); - - let transceivers = &media_section.transceivers; - // Use the first transceiver to generate the section attributes - let t = &transceivers[0]; - let mut media = MediaDescription::new_jsep_media_description(t.kind.to_string(), vec![]) - .with_value_attribute(ATTR_KEY_CONNECTION_SETUP.to_owned(), dtls_role.to_string()) - .with_value_attribute(ATTR_KEY_MID.to_owned(), mid_value.clone()) - .with_ice_credentials( - ice_params.username_fragment.clone(), - ice_params.password.clone(), - ) - .with_property_attribute(ATTR_KEY_RTCPMUX.to_owned()) - .with_property_attribute(ATTR_KEY_RTCPRSIZE.to_owned()); - - let codecs = t.get_codecs().await; - for codec in &codecs { - let name = codec - .capability - .mime_type - .trim_start_matches("audio/") - .trim_start_matches("video/") - .to_owned(); - media = media.with_codec( - codec.payload_type, - name, - codec.capability.clock_rate, - codec.capability.channels, - codec.capability.sdp_fmtp_line.clone(), - ); - - for feedback in &codec.capability.rtcp_feedback { - media = media.with_value_attribute( - "rtcp-fb".to_owned(), - format!( - "{} {} {}", - codec.payload_type, feedback.typ, feedback.parameter - ), - ); - } - } - if codecs.is_empty() { - // If we are sender and we have no codecs throw an error early - if t.sender().await.track().await.is_some() { - return Err(Error::ErrSenderWithNoCodecs); - } - - // Explicitly reject track if we don't have the codec - d = d.with_media(MediaDescription { - media_name: sdp::description::media::MediaName { - media: t.kind.to_string(), - port: RangedPort { - value: 0, - range: None, - }, - protos: vec![ - "UDP".to_owned(), - "TLS".to_owned(), - "RTP".to_owned(), - "SAVPF".to_owned(), - ], - formats: vec!["0".to_owned()], - }, - media_title: None, - // We need to include connection information even if we're rejecting a track, otherwise Firefox will fail to - // parse the SDP with an error like: - // SIPCC Failed to parse SDP: SDP Parse Error on line 50: c= connection line not specified for every media level, validation failed. - // In addition this makes our SDP compliant with RFC 4566 Section 5.7: https://datatracker.ietf.org/doc/html/rfc4566#section-5.7 - connection_information: Some(ConnectionInformation { - network_type: "IN".to_owned(), - address_type: "IP4".to_owned(), - address: Some(Address { - address: "0.0.0.0".to_owned(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - }); - return Ok((d, false)); - } - - let parameters = media_engine.get_rtp_parameters_by_kind(t.kind, t.direction()); - for rtp_extension in ¶meters.header_extensions { - let ext_url = Url::parse(rtp_extension.uri.as_str())?; - media = media.with_extmap(sdp::extmap::ExtMap { - value: rtp_extension.id, - uri: Some(ext_url), - ..Default::default() - }); - } - - if !media_section.rid_map.is_empty() { - let mut recv_sc_list: Vec = vec![]; - let mut send_sc_list: Vec = vec![]; - - for rid in &media_section.rid_map { - let rid_syntax = match rid.direction { - SimulcastDirection::Send => { - // If Send rid, then reply with a recv rid - if rid.paused { - recv_sc_list.push(format!("~{}", rid.id)); - } else { - recv_sc_list.push(rid.id.to_owned()); - } - format!("{} recv", rid.id) - } - SimulcastDirection::Recv => { - // If Recv rid, then reply with a send rid - if rid.paused { - send_sc_list.push(format!("~{}", rid.id)); - } else { - send_sc_list.push(rid.id.to_owned()); - } - format!("{} send", rid.id) - } - }; - media = media.with_value_attribute(SDP_ATTRIBUTE_RID.to_owned(), rid_syntax); - } - - // Simulcast - let mut sc_attr = String::new(); - if !recv_sc_list.is_empty() { - sc_attr.push_str(&format!("recv {}", recv_sc_list.join(";"))); - } - if !send_sc_list.is_empty() { - sc_attr.push_str(&format!("send {}", send_sc_list.join(";"))); - } - media = media.with_value_attribute(SDP_ATTRIBUTE_SIMULCAST.to_owned(), sc_attr); - } - - for mt in transceivers { - let sender = mt.sender().await; - if let Some(track) = sender.track().await { - let send_parameters = sender.get_parameters().await; - for encoding in &send_parameters.encodings { - media = media.with_media_source( - encoding.ssrc, - track.stream_id().to_owned(), /* cname */ - track.stream_id().to_owned(), /* streamLabel */ - track.id().to_owned(), - ); - } - - if send_parameters.encodings.len() > 1 { - let mut send_rids = Vec::with_capacity(send_parameters.encodings.len()); - - for encoding in &send_parameters.encodings { - media = media.with_value_attribute( - SDP_ATTRIBUTE_RID.to_owned(), - format!("{} send", encoding.rid), - ); - send_rids.push(encoding.rid.to_string()); - } - - media = media.with_value_attribute( - SDP_ATTRIBUTE_SIMULCAST.to_owned(), - format!("send {}", send_rids.join(";")), - ); - } - - // Send msid based on the configured track if we haven't already - // sent on this sender. If we have sent we must keep the msid line consistent, this - // is handled below. - if sender.initial_track_id().is_none() { - for stream_id in sender.associated_media_stream_ids() { - media = - media.with_property_attribute(format!("msid:{} {}", stream_id, track.id())); - } - - sender.set_initial_track_id(track.id().to_string())?; - break; - } - } - - if let Some(track_id) = sender.initial_track_id() { - // After we have include an msid attribute in an offer it must stay the same for - // all subsequent offer even if the track or transceiver direction changes. - // - // [RFC 8829 Section 5.2.2](https://datatracker.ietf.org/doc/html/rfc8829#section-5.2.2) - // - // For RtpTransceivers that are not stopped, the "a=msid" line or - // lines MUST stay the same if they are present in the current - // description, regardless of changes to the transceiver's direction - // or track. If no "a=msid" line is present in the current - // description, "a=msid" line(s) MUST be generated according to the - // same rules as for an initial offer. - for stream_id in sender.associated_media_stream_ids() { - media = media.with_property_attribute(format!("msid:{stream_id} {track_id}")); - } - - break; - } - } - - let direction = match params.offered_direction { - Some(offered_direction) => { - use RTCRtpTransceiverDirection::*; - let transceiver_direction = t.direction(); - - match offered_direction { - Sendonly | Recvonly => { - // If a stream is offered as sendonly, the corresponding stream MUST be - // marked as recvonly or inactive in the answer. - - // If a media stream is - // listed as recvonly in the offer, the answer MUST be marked as - // sendonly or inactive in the answer. - offered_direction.reverse().intersect(transceiver_direction) - } - // If an offered media stream is - // listed as sendrecv (or if there is no direction attribute at the - // media or session level, in which case the stream is sendrecv by - // default), the corresponding stream in the answer MAY be marked as - // sendonly, recvonly, sendrecv, or inactive - Sendrecv | Unspecified => t.direction(), - // If an offered media - // stream is listed as inactive, it MUST be marked as inactive in the - // answer. - Inactive => Inactive, - } - } - None => { - // If don't have an offered direction to intersect with just use the transceivers - // current direction. - // - // https://datatracker.ietf.org/doc/html/rfc8829#section-4.2.3 - // - // When creating offers, the transceiver direction is directly reflected - // in the output, even for re-offers. - t.direction() - } - }; - media = media.with_property_attribute(direction.to_string()); - - for fingerprint in dtls_fingerprints { - media = media.with_fingerprint( - fingerprint.algorithm.to_owned(), - fingerprint.value.to_uppercase(), - ); - } - - if should_add_candidates { - media = - add_candidates_to_media_descriptions(candidates, media, ice_gathering_state).await?; - } - - Ok((d.with_media(media), true)) -} - -#[derive(thiserror::Error, Debug, PartialEq)] -pub(crate) enum SimulcastRidParseError { - /// SyntaxIdDirSplit indicates rid-syntax could not be parsed. - #[error("RFC8851 mandates rid-syntax = %s\"a=rid:\" rid-id SP rid-dir")] - SyntaxIdDirSplit, - /// UnknownDirection indicates rid-dir was not parsed. Should be "send" or "recv". - #[error("RFC8851 mandates rid-dir = %s\"send\" / %s\"recv\"")] - UnknownDirection, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum SimulcastDirection { - Send, - Recv, -} - -impl TryFrom<&str> for SimulcastDirection { - type Error = SimulcastRidParseError; - fn try_from(value: &str) -> std::result::Result { - match value.to_lowercase().as_str() { - "send" => Ok(SimulcastDirection::Send), - "recv" => Ok(SimulcastDirection::Recv), - _ => Err(SimulcastRidParseError::UnknownDirection), - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct SimulcastRid { - pub(crate) id: String, - pub(crate) direction: SimulcastDirection, - pub(crate) params: String, - pub(crate) paused: bool, -} - -impl TryFrom<&String> for SimulcastRid { - type Error = SimulcastRidParseError; - fn try_from(value: &String) -> std::result::Result { - let mut split = value.split(' '); - let id = split - .next() - .ok_or(SimulcastRidParseError::SyntaxIdDirSplit)? - .to_owned(); - let direction = SimulcastDirection::try_from( - split - .next() - .ok_or(SimulcastRidParseError::SyntaxIdDirSplit)?, - )?; - let params = split.collect(); - - Ok(Self { - id, - direction, - params, - paused: false, - }) - } -} - -fn bundle_match(bundle: Option<&String>, id: &str) -> bool { - match bundle { - None => true, - Some(b) => b.split_whitespace().any(|s| s == id), - } -} - -#[derive(Default)] -pub(crate) struct MediaSection { - pub(crate) id: String, - pub(crate) transceivers: Vec>, - pub(crate) data: bool, - pub(crate) rid_map: Vec, - pub(crate) offered_direction: Option, -} - -pub(crate) struct PopulateSdpParams { - pub(crate) media_description_fingerprint: bool, - pub(crate) is_icelite: bool, - pub(crate) connection_role: ConnectionRole, - pub(crate) ice_gathering_state: RTCIceGatheringState, - pub(crate) match_bundle_group: Option, -} - -/// populate_sdp serializes a PeerConnections state into an SDP -pub(crate) async fn populate_sdp( - mut d: SessionDescription, - dtls_fingerprints: &[RTCDtlsFingerprint], - media_engine: &Arc, - candidates: &[RTCIceCandidate], - ice_params: &RTCIceParameters, - media_sections: &[MediaSection], - params: PopulateSdpParams, -) -> Result { - let media_dtls_fingerprints = if params.media_description_fingerprint { - dtls_fingerprints.to_vec() - } else { - vec![] - }; - - let mut bundle_value = "BUNDLE".to_owned(); - let mut bundle_count = 0; - let append_bundle = |mid_value: &str, value: &mut String, count: &mut i32| { - *value = value.clone() + " " + mid_value; - *count += 1; - }; - - for (i, m) in media_sections.iter().enumerate() { - if m.data && !m.transceivers.is_empty() { - return Err(Error::ErrSDPMediaSectionMediaDataChanInvalid); - } else if m.transceivers.len() > 1 { - return Err(Error::ErrSDPMediaSectionMultipleTrackInvalid); - } - - let should_add_candidates = i == 0; - - let should_add_id = if m.data { - let params = AddDataMediaSectionParams { - should_add_candidates, - mid_value: m.id.clone(), - ice_params: ice_params.clone(), - dtls_role: params.connection_role, - ice_gathering_state: params.ice_gathering_state, - }; - d = add_data_media_section(d, &media_dtls_fingerprints, candidates, params).await?; - true - } else { - let params = AddTransceiverSdpParams { - should_add_candidates, - mid_value: m.id.clone(), - dtls_role: params.connection_role, - ice_gathering_state: params.ice_gathering_state, - offered_direction: m.offered_direction, - }; - let (d1, should_add_id) = add_transceiver_sdp( - d, - &media_dtls_fingerprints, - media_engine, - ice_params, - candidates, - m, - params, - ) - .await?; - d = d1; - should_add_id - }; - - if should_add_id { - if bundle_match(params.match_bundle_group.as_ref(), &m.id) { - append_bundle(&m.id, &mut bundle_value, &mut bundle_count); - } else if let Some(desc) = d.media_descriptions.last_mut() { - desc.media_name.port = RangedPort { - value: 0, - range: None, - } - } - } - } - - if !params.media_description_fingerprint { - for fingerprint in dtls_fingerprints { - d = d.with_fingerprint( - fingerprint.algorithm.clone(), - fingerprint.value.to_uppercase(), - ); - } - } - - if params.is_icelite { - // RFC 5245 S15.3 - d = d.with_value_attribute(ATTR_KEY_ICELITE.to_owned(), ATTR_KEY_ICELITE.to_owned()); - } - - if bundle_count > 0 { - d = d.with_value_attribute(ATTR_KEY_GROUP.to_owned(), bundle_value); - } - - Ok(d) -} - -pub(crate) fn get_mid_value(media: &MediaDescription) -> Option<&String> { - for attr in &media.attributes { - if attr.key == "mid" { - return attr.value.as_ref(); - } - } - None -} - -pub(crate) fn get_peer_direction(media: &MediaDescription) -> RTCRtpTransceiverDirection { - for a in &media.attributes { - let direction = RTCRtpTransceiverDirection::from(a.key.as_str()); - if direction != RTCRtpTransceiverDirection::Unspecified { - return direction; - } - } - RTCRtpTransceiverDirection::Unspecified -} - -pub(crate) fn extract_fingerprint(desc: &SessionDescription) -> Result<(String, String)> { - let mut fingerprints = vec![]; - - if let Some(fingerprint) = desc.attribute("fingerprint") { - fingerprints.push(fingerprint.clone()); - } - - for m in &desc.media_descriptions { - if let Some(fingerprint) = m.attribute("fingerprint").and_then(|o| o) { - fingerprints.push(fingerprint.to_owned()); - } - } - - if fingerprints.is_empty() { - return Err(Error::ErrSessionDescriptionNoFingerprint); - } - - for m in 1..fingerprints.len() { - if fingerprints[m] != fingerprints[0] { - return Err(Error::ErrSessionDescriptionConflictingFingerprints); - } - } - - let parts: Vec<&str> = fingerprints[0].split(' ').collect(); - if parts.len() != 2 { - return Err(Error::ErrSessionDescriptionInvalidFingerprint); - } - - Ok((parts[1].to_owned(), parts[0].to_owned())) -} - -pub(crate) async fn extract_ice_details( - desc: &SessionDescription, -) -> Result<(String, String, Vec)> { - let mut candidates = vec![]; - let mut remote_pwds = vec![]; - let mut remote_ufrags = vec![]; - - if let Some(ufrag) = desc.attribute("ice-ufrag") { - remote_ufrags.push(ufrag.clone()); - } - if let Some(pwd) = desc.attribute("ice-pwd") { - remote_pwds.push(pwd.clone()); - } - - for m in &desc.media_descriptions { - if let Some(ufrag) = m.attribute("ice-ufrag").and_then(|o| o) { - remote_ufrags.push(ufrag.to_owned()); - } - if let Some(pwd) = m.attribute("ice-pwd").and_then(|o| o) { - remote_pwds.push(pwd.to_owned()); - } - - for a in &m.attributes { - if a.is_ice_candidate() { - if let Some(value) = &a.value { - let c: Arc = Arc::new(unmarshal_candidate(value)?); - let candidate = RTCIceCandidate::from(&c); - candidates.push(candidate); - } - } - } - } - - if remote_ufrags.is_empty() { - return Err(Error::ErrSessionDescriptionMissingIceUfrag); - } else if remote_pwds.is_empty() { - return Err(Error::ErrSessionDescriptionMissingIcePwd); - } - - for m in 1..remote_ufrags.len() { - if remote_ufrags[m] != remote_ufrags[0] { - return Err(Error::ErrSessionDescriptionConflictingIceUfrag); - } - } - - for m in 1..remote_pwds.len() { - if remote_pwds[m] != remote_pwds[0] { - return Err(Error::ErrSessionDescriptionConflictingIcePwd); - } - } - - Ok((remote_ufrags[0].clone(), remote_pwds[0].clone(), candidates)) -} - -pub(crate) fn have_application_media_section(desc: &SessionDescription) -> bool { - for m in &desc.media_descriptions { - if m.media_name.media == MEDIA_SECTION_APPLICATION { - return true; - } - } - - false -} - -pub(crate) fn get_by_mid<'a>( - search_mid: &str, - desc: &'a session_description::RTCSessionDescription, -) -> Option<&'a MediaDescription> { - if let Some(parsed) = &desc.parsed { - for m in &parsed.media_descriptions { - if let Some(mid) = m.attribute(ATTR_KEY_MID).flatten() { - if mid == search_mid { - return Some(m); - } - } - } - } - None -} - -/// have_data_channel return MediaDescription with MediaName equal application -pub(crate) fn have_data_channel( - desc: &session_description::RTCSessionDescription, -) -> Option<&MediaDescription> { - if let Some(parsed) = &desc.parsed { - for d in &parsed.media_descriptions { - if d.media_name.media == MEDIA_SECTION_APPLICATION { - return Some(d); - } - } - } - None -} - -pub(crate) fn codecs_from_media_description( - m: &MediaDescription, -) -> Result> { - let s = SessionDescription { - media_descriptions: vec![m.clone()], - ..Default::default() - }; - - let mut out = vec![]; - for payload_str in &m.media_name.formats { - let payload_type: PayloadType = payload_str.parse::()?; - let codec = match s.get_codec_for_payload_type(payload_type) { - Ok(codec) => codec, - Err(err) => { - if payload_type == 0 { - continue; - } - return Err(err.into()); - } - }; - - let channels = codec.encoding_parameters.parse::().unwrap_or(0); - - let mut feedback = vec![]; - for raw in &codec.rtcp_feedback { - let split: Vec<&str> = raw.split(' ').collect(); - - let entry = if split.len() == 2 { - RTCPFeedback { - typ: split[0].to_string(), - parameter: split[1].to_string(), - } - } else { - RTCPFeedback { - typ: split[0].to_string(), - parameter: String::new(), - } - }; - - feedback.push(entry); - } - - out.push(RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: m.media_name.media.clone() + "/" + codec.name.as_str(), - clock_rate: codec.clock_rate, - channels, - sdp_fmtp_line: codec.fmtp.clone(), - rtcp_feedback: feedback, - }, - payload_type, - stats_id: String::new(), - }) - } - - Ok(out) -} - -pub(crate) fn rtp_extensions_from_media_description( - m: &MediaDescription, -) -> Result> { - let mut out = HashMap::new(); - - for a in &m.attributes { - if a.key == ATTR_KEY_EXT_MAP { - let a_str = a.to_string(); - let mut reader = BufReader::new(a_str.as_bytes()); - let e = ExtMap::unmarshal(&mut reader)?; - - if let Some(uri) = e.uri { - out.insert(uri.to_string(), e.value); - } - } - } - - Ok(out) -} - -/// update_sdp_origin saves sdp.Origin in PeerConnection when creating 1st local SDP; -/// for subsequent calling, it updates Origin for SessionDescription from saved one -/// and increments session version by one. -/// -pub(crate) fn update_sdp_origin(origin: &mut Origin, d: &mut SessionDescription) { - //TODO: if atomic.CompareAndSwapUint64(&origin.SessionVersion, 0, d.Origin.SessionVersion) - if origin.session_version == 0 { - // store - origin.session_version = d.origin.session_version; - //atomic.StoreUint64(&origin.SessionID, d.Origin.SessionID) - origin.session_id = d.origin.session_id; - } else { - // load - /*for { // awaiting for saving session id - d.Origin.SessionID = atomic.LoadUint64(&origin.SessionID) - if d.Origin.SessionID != 0 { - break - } - }*/ - d.origin.session_id = origin.session_id; - - //d.Origin.SessionVersion = atomic.AddUint64(&origin.SessionVersion, 1) - origin.session_version += 1; - d.origin.session_version += 1; - } -} diff --git a/webrtc/src/peer_connection/sdp/sdp_test.rs b/webrtc/src/peer_connection/sdp/sdp_test.rs deleted file mode 100644 index ca8cf5b24..000000000 --- a/webrtc/src/peer_connection/sdp/sdp_test.rs +++ /dev/null @@ -1,1378 +0,0 @@ -use rcgen::KeyPair; -use sdp::description::common::Attribute; - -use super::*; -use crate::api::media_engine::{MIME_TYPE_OPUS, MIME_TYPE_VP8}; -use crate::api::setting_engine::SettingEngine; -use crate::api::APIBuilder; -use crate::dtls_transport::dtls_role::DEFAULT_DTLS_ROLE_OFFER; -use crate::dtls_transport::RTCDtlsTransport; -use crate::peer_connection::certificate::RTCCertificate; -use crate::rtp_transceiver::rtp_sender::RTCRtpSender; -use crate::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use crate::track::track_local::TrackLocal; - -#[test] -fn test_extract_fingerprint() -> Result<()> { - //"Good Session Fingerprint" - { - let s = SessionDescription { - attributes: vec![Attribute { - key: "fingerprint".to_owned(), - value: Some("foo bar".to_owned()), - }], - ..Default::default() - }; - - let (fingerprint, hash) = extract_fingerprint(&s)?; - assert_eq!(fingerprint, "bar"); - assert_eq!(hash, "foo"); - } - - //"Good Media Fingerprint" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - attributes: vec![Attribute { - key: "fingerprint".to_owned(), - value: Some("foo bar".to_owned()), - }], - ..Default::default() - }], - ..Default::default() - }; - - let (fingerprint, hash) = extract_fingerprint(&s)?; - assert_eq!(fingerprint, "bar"); - assert_eq!(hash, "foo"); - } - - //"No Fingerprint" - { - let s = SessionDescription::default(); - - assert_eq!( - extract_fingerprint(&s).expect_err("fingerprint absence must be detected"), - Error::ErrSessionDescriptionNoFingerprint - ); - } - - //"Invalid Fingerprint" - { - let s = SessionDescription { - attributes: vec![Attribute { - key: "fingerprint".to_owned(), - value: Some("foo".to_owned()), - }], - ..Default::default() - }; - - assert_eq!( - extract_fingerprint(&s).expect_err("invalid fingerprint text must be detected"), - Error::ErrSessionDescriptionInvalidFingerprint - ); - } - - //"Conflicting Fingerprint" - { - let s = SessionDescription { - attributes: vec![Attribute { - key: "fingerprint".to_owned(), - value: Some("foo".to_owned()), - }], - media_descriptions: vec![MediaDescription { - attributes: vec![Attribute { - key: "fingerprint".to_owned(), - value: Some("foo bar".to_owned()), - }], - ..Default::default() - }], - ..Default::default() - }; - - assert_eq!( - extract_fingerprint(&s).expect_err("mismatching fingerprint texts must be detected"), - Error::ErrSessionDescriptionConflictingFingerprints - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_extract_ice_details() -> Result<()> { - const DEFAULT_UFRAG: &str = "DEFAULT_PWD"; - const DEFAULT_PWD: &str = "DEFAULT_UFRAG"; - - //"Missing ice-pwd" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - attributes: vec![Attribute { - key: "ice-ufrag".to_owned(), - value: Some(DEFAULT_UFRAG.to_owned()), - }], - ..Default::default() - }], - ..Default::default() - }; - - assert_eq!( - extract_ice_details(&s) - .await - .expect_err("ICE requires password for authentication"), - Error::ErrSessionDescriptionMissingIcePwd - ); - } - - //"Missing ice-ufrag" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - attributes: vec![Attribute { - key: "ice-pwd".to_owned(), - value: Some(DEFAULT_PWD.to_owned()), - }], - ..Default::default() - }], - ..Default::default() - }; - - assert_eq!( - extract_ice_details(&s) - .await - .expect_err("ICE requires 'user fragment' for authentication"), - Error::ErrSessionDescriptionMissingIceUfrag - ); - } - - //"ice details at session level" - { - let s = SessionDescription { - attributes: vec![ - Attribute { - key: "ice-ufrag".to_owned(), - value: Some(DEFAULT_UFRAG.to_owned()), - }, - Attribute { - key: "ice-pwd".to_owned(), - value: Some(DEFAULT_PWD.to_owned()), - }, - ], - media_descriptions: vec![], - ..Default::default() - }; - - let (ufrag, pwd, _) = extract_ice_details(&s).await?; - assert_eq!(ufrag, DEFAULT_UFRAG); - assert_eq!(pwd, DEFAULT_PWD); - } - - //"ice details at media level" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - attributes: vec![ - Attribute { - key: "ice-ufrag".to_owned(), - value: Some(DEFAULT_UFRAG.to_owned()), - }, - Attribute { - key: "ice-pwd".to_owned(), - value: Some(DEFAULT_PWD.to_owned()), - }, - ], - ..Default::default() - }], - ..Default::default() - }; - - let (ufrag, pwd, _) = extract_ice_details(&s).await?; - assert_eq!(ufrag, DEFAULT_UFRAG); - assert_eq!(pwd, DEFAULT_PWD); - } - - //"Conflict ufrag" - { - let s = SessionDescription { - attributes: vec![Attribute { - key: "ice-ufrag".to_owned(), - value: Some("invalidUfrag".to_owned()), - }], - media_descriptions: vec![MediaDescription { - attributes: vec![ - Attribute { - key: "ice-ufrag".to_owned(), - value: Some(DEFAULT_UFRAG.to_owned()), - }, - Attribute { - key: "ice-pwd".to_owned(), - value: Some(DEFAULT_PWD.to_owned()), - }, - ], - ..Default::default() - }], - ..Default::default() - }; - - assert_eq!( - extract_ice_details(&s) - .await - .expect_err("mismatching ICE ufrags must be detected"), - Error::ErrSessionDescriptionConflictingIceUfrag - ); - } - - //"Conflict pwd" - { - let s = SessionDescription { - attributes: vec![Attribute { - key: "ice-pwd".to_owned(), - value: Some("invalidPwd".to_owned()), - }], - media_descriptions: vec![MediaDescription { - attributes: vec![ - Attribute { - key: "ice-ufrag".to_owned(), - value: Some(DEFAULT_UFRAG.to_owned()), - }, - Attribute { - key: "ice-pwd".to_owned(), - value: Some(DEFAULT_PWD.to_owned()), - }, - ], - ..Default::default() - }], - ..Default::default() - }; - - assert_eq!( - extract_ice_details(&s) - .await - .expect_err("mismatching ICE passwords must be detected"), - Error::ErrSessionDescriptionConflictingIcePwd - ); - } - - Ok(()) -} - -#[test] -fn test_track_details_from_sdp() -> Result<()> { - //"Tracks unknown, audio and video with RTX" - { - let s = SessionDescription { - media_descriptions: vec![ - MediaDescription { - media_name: MediaName { - media: "foobar".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("0".to_owned()), - }, - Attribute { - key: "sendrecv".to_owned(), - value: None, - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("1000 msid:unknown_trk_label unknown_trk_guid".to_owned()), - }, - ], - ..Default::default() - }, - MediaDescription { - media_name: MediaName { - media: "audio".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("1".to_owned()), - }, - Attribute { - key: "sendrecv".to_owned(), - value: None, - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("2000 msid:audio_trk_label audio_trk_guid".to_owned()), - }, - ], - ..Default::default() - }, - MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("2".to_owned()), - }, - Attribute { - key: "sendrecv".to_owned(), - value: None, - }, - Attribute { - key: "ssrc-group".to_owned(), - value: Some("FID 3000 4000".to_owned()), - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("3000 msid:video_trk_label video_trk_guid".to_owned()), - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("4000 msid:rtx_trk_label rtx_trck_guid".to_owned()), - }, - ], - ..Default::default() - }, - MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("3".to_owned()), - }, - Attribute { - key: "sendonly".to_owned(), - value: None, - }, - Attribute { - key: "msid".to_owned(), - value: Some("video_stream_id video_trk_id".to_owned()), - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("5000".to_owned()), - }, - ], - ..Default::default() - }, - MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "sendonly".to_owned(), - value: None, - }, - Attribute { - key: SDP_ATTRIBUTE_RID.to_owned(), - value: Some("f send pt=97;max-width=1280;max-height=720".to_owned()), - }, - ], - ..Default::default() - }, - ], - ..Default::default() - }; - - let tracks = track_details_from_sdp(&s, true); - assert_eq!(tracks.len(), 3); - if track_details_for_ssrc(&tracks, 1000).is_some() { - panic!("got the unknown track ssrc:1000 which should have been skipped"); - } - if let Some(track) = track_details_for_ssrc(&tracks, 2000) { - assert_eq!(track.kind, RTPCodecType::Audio); - assert_eq!(track.ssrcs[0], 2000); - assert_eq!(track.stream_id, "audio_trk_label"); - } else { - panic!("missing audio track with ssrc:2000"); - } - if let Some(track) = track_details_for_ssrc(&tracks, 3000) { - assert_eq!(track.kind, RTPCodecType::Video); - assert_eq!(track.ssrcs[0], 3000); - assert_eq!(track.stream_id, "video_trk_label"); - } else { - panic!("missing video track with ssrc:3000"); - } - if track_details_for_ssrc(&tracks, 4000).is_some() { - panic!("got the rtx track ssrc:3000 which should have been skipped"); - } - if let Some(track) = track_details_for_ssrc(&tracks, 5000) { - assert_eq!(track.kind, RTPCodecType::Video); - assert_eq!(track.ssrcs[0], 5000); - assert_eq!(track.id, "video_trk_id"); - assert_eq!(track.stream_id, "video_stream_id"); - } else { - panic!("missing video track with ssrc:5000"); - } - } - - { - let s = SessionDescription { - media_descriptions: vec![ - MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("1".to_owned()), - }, - Attribute { - key: "inactive".to_owned(), - value: None, - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("6000".to_owned()), - }, - ], - ..Default::default() - }, - MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "mid".to_owned(), - value: Some("1".to_owned()), - }, - Attribute { - key: "recvonly".to_owned(), - value: None, - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("7000".to_owned()), - }, - ], - ..Default::default() - }, - ], - ..Default::default() - }; - assert_eq!( - track_details_from_sdp(&s, true).len(), - 0, - "inactive and recvonly tracks should be ignored when passing exclude_inactive: true" - ); - assert_eq!( - track_details_from_sdp(&s, false).len(), - 1, - "Inactive tracks should not be ignored when passing exclude_inactive: false" - ); - } - - Ok(()) -} - -#[test] -fn test_have_application_media_section() -> Result<()> { - //"Audio only" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - media_name: MediaName { - media: "audio".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "sendrecv".to_owned(), - value: None, - }, - Attribute { - key: "ssrc".to_owned(), - value: Some("2000".to_owned()), - }, - ], - ..Default::default() - }], - ..Default::default() - }; - - assert!(!have_application_media_section(&s)); - } - - //"Application" - { - let s = SessionDescription { - media_descriptions: vec![MediaDescription { - media_name: MediaName { - media: MEDIA_SECTION_APPLICATION.to_owned(), - ..Default::default() - }, - ..Default::default() - }], - ..Default::default() - }; - - assert!(have_application_media_section(&s)); - } - - Ok(()) -} - -async fn fingerprint_test( - certificate: &RTCCertificate, - engine: &Arc, - media: &[MediaSection], - sdpmedia_description_fingerprints: bool, - expected_fingerprint_count: usize, -) -> Result<()> { - let s = SessionDescription::default(); - - let dtls_fingerprints = certificate.get_fingerprints(); - - let params = PopulateSdpParams { - media_description_fingerprint: sdpmedia_description_fingerprints, - is_icelite: false, - connection_role: ConnectionRole::Active, - ice_gathering_state: RTCIceGatheringState::New, - match_bundle_group: None, - }; - - let s = populate_sdp( - s, - &dtls_fingerprints, - engine, - &[], - &RTCIceParameters::default(), - media, - params, - ) - .await?; - - let sdparray = s.marshal(); - - assert_eq!( - sdparray.matches("sha-256").count(), - expected_fingerprint_count - ); - - Ok(()) -} - -#[tokio::test] -async fn test_media_description_fingerprints() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - let interceptor = api.interceptor_registry.build("")?; - - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let certificate = RTCCertificate::from_key_pair(kp)?; - - let transport = Arc::new(RTCDtlsTransport::default()); - - let video_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - let audio_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Audio, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let video_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let audio_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let media = vec![ - MediaSection { - id: "video".to_owned(), - transceivers: vec![ - RTCRtpTransceiver::new( - video_receiver, - video_sender, - RTCRtpTransceiverDirection::Inactive, - RTPCodecType::Video, - api.media_engine.get_codecs_by_kind(RTPCodecType::Video), - Arc::clone(&api.media_engine), - None, - ) - .await, - ], - ..Default::default() - }, - MediaSection { - id: "audio".to_owned(), - transceivers: vec![ - RTCRtpTransceiver::new( - audio_receiver, - audio_sender, - RTCRtpTransceiverDirection::Inactive, - RTPCodecType::Audio, - api.media_engine.get_codecs_by_kind(RTPCodecType::Audio), - Arc::clone(&api.media_engine), - None, - ) - .await, - ], - ..Default::default() - }, - MediaSection { - id: "application".to_owned(), - data: true, - ..Default::default() - }, - ]; - - #[allow(clippy::needless_range_loop)] - for i in 0..2 { - let track: Arc = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: "video/vp8".to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - media[i].transceivers[0] - .set_sender(Arc::new( - RTCRtpSender::new( - api.setting_engine.get_receive_mtu(), - Some(track), - RTPCodecType::Video, - Arc::new(RTCDtlsTransport::default()), - Arc::clone(&api.media_engine), - Arc::clone(&interceptor), - false, - ) - .await, - )) - .await; - media[i].transceivers[0].set_direction_internal(RTCRtpTransceiverDirection::Sendonly); - } - - //"Per-Media Description Fingerprints", - fingerprint_test(&certificate, &api.media_engine, &media, true, 3).await?; - - //"Per-Session Description Fingerprints", - fingerprint_test(&certificate, &api.media_engine, &media, false, 1).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_populate_sdp() -> Result<()> { - //"Rid" - { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_default_codecs()?; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - - let receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tr = RTCRtpTransceiver::new( - receiver, - sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let rid_map = vec![ - SimulcastRid { - id: "ridkey".to_owned(), - direction: SimulcastDirection::Recv, - params: "some".to_owned(), - paused: false, - }, - SimulcastRid { - id: "ridpaused".to_owned(), - direction: SimulcastDirection::Recv, - params: "some2".to_owned(), - paused: true, - }, - ]; - let media_sections = vec![MediaSection { - id: "video".to_owned(), - transceivers: vec![tr], - data: false, - rid_map, - ..Default::default() - }]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: None, - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - // Test contains rid map keys - let mut found = 0; - for desc in &offer_sdp.media_descriptions { - if desc.media_name.media != "video" { - continue; - } - - let rid_map = get_rids(desc); - if let Some(rid) = rid_map.iter().find(|rid| rid.id == "ridkey") { - assert!(!rid.paused, "Rid should be active"); - assert_eq!( - rid.direction, - SimulcastDirection::Send, - "Rid should be send" - ); - found += 1; - } - if let Some(rid) = rid_map.iter().find(|rid| rid.id == "ridpaused") { - assert!(rid.paused, "Rid should be paused"); - assert_eq!( - rid.direction, - SimulcastDirection::Send, - "Rid should be send" - ); - found += 1; - } - } - assert_eq!(found, 2, "All Rid key should be present"); - } - - //"SetCodecPreferences" - { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_default_codecs()?; - me.push_codecs(me.video_codecs.clone(), RTPCodecType::Video) - .await; - me.push_codecs(me.audio_codecs.clone(), RTPCodecType::Audio) - .await; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - let receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tr = RTCRtpTransceiver::new( - receiver, - sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - tr.set_codec_preferences(vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }]) - .await?; - - let media_sections = vec![MediaSection { - id: "video".to_owned(), - transceivers: vec![tr], - data: false, - rid_map: vec![], - ..Default::default() - }]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: None, - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - // Test codecs - let mut found_vp8 = false; - for desc in &offer_sdp.media_descriptions { - if desc.media_name.media != "video" { - continue; - } - for a in &desc.attributes { - if a.key.contains("rtpmap") { - if let Some(value) = &a.value { - if value == "98 VP9/90000" { - panic!("vp9 should not be present in sdp"); - } else if value == "96 VP8/90000" { - found_vp8 = true; - } - } - } - } - } - assert!(found_vp8, "vp8 should be present in sdp"); - } - - //"Bundle all" - { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_default_codecs()?; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - let receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tr = RTCRtpTransceiver::new( - receiver, - sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let media_sections = vec![MediaSection { - id: "video".to_owned(), - transceivers: vec![tr], - data: false, - rid_map: vec![], - ..Default::default() - }]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: None, - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - assert_eq!( - offer_sdp.attribute(ATTR_KEY_GROUP), - Some(&"BUNDLE video".to_owned()) - ); - } - - //"Bundle matched" - { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_default_codecs()?; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - - let video_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - let audio_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Audio, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let video_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - let audio_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let trv = RTCRtpTransceiver::new( - video_receiver, - video_sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let tra = RTCRtpTransceiver::new( - audio_receiver, - audio_sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Audio, - api.media_engine.audio_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let media_sections = vec![ - MediaSection { - id: "video".to_owned(), - transceivers: vec![trv], - data: false, - rid_map: vec![], - ..Default::default() - }, - MediaSection { - id: "audio".to_owned(), - transceivers: vec![tra], - data: false, - rid_map: vec![], - ..Default::default() - }, - ]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: Some("audio".to_owned()), - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - assert_eq!( - offer_sdp.attribute(ATTR_KEY_GROUP), - Some(&"BUNDLE audio".to_owned()) - ); - } - - //"empty bundle group" - { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_default_codecs()?; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - let receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tr = RTCRtpTransceiver::new( - receiver, - sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let media_sections = vec![MediaSection { - id: "video".to_owned(), - transceivers: vec![tr], - data: false, - rid_map: vec![], - ..Default::default() - }]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: Some("".to_owned()), - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - assert_eq!(offer_sdp.attribute(ATTR_KEY_GROUP), None); - } - - Ok(()) -} - -#[tokio::test] -async fn test_populate_sdp_reject() -> Result<()> { - let se = SettingEngine::default(); - let mut me = MediaEngine::default(); - me.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90_000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 2, - stats_id: "id".to_owned(), - }, - RTPCodecType::Video, - )?; - - let api = APIBuilder::new().with_media_engine(me).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - let video_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let video_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let trv = RTCRtpTransceiver::new( - video_receiver, - video_sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Video, - api.media_engine.video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let audio_receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Audio, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let audio_sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tra = RTCRtpTransceiver::new( - audio_receiver, - audio_sender, - RTCRtpTransceiverDirection::Recvonly, - RTPCodecType::Audio, - api.media_engine.audio_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - let media_sections = vec![ - MediaSection { - id: "video".to_owned(), - transceivers: vec![trv], - data: false, - rid_map: vec![], - ..Default::default() - }, - MediaSection { - id: "audio".to_owned(), - transceivers: vec![tra], - data: false, - rid_map: vec![], - ..Default::default() - }, - ]; - - let d = SessionDescription::default(); - - let params = PopulateSdpParams { - media_description_fingerprint: se.sdp_media_level_fingerprints, - is_icelite: se.candidates.ice_lite, - connection_role: DEFAULT_DTLS_ROLE_OFFER.to_connection_role(), - ice_gathering_state: RTCIceGatheringState::Complete, - match_bundle_group: None, - }; - let offer_sdp = populate_sdp( - d, - &[], - &api.media_engine, - &[], - &RTCIceParameters::default(), - &media_sections, - params, - ) - .await?; - - let mut found_rejected_track = false; - - for desc in offer_sdp.media_descriptions { - if desc.media_name.media != "audio" { - continue; - } - found_rejected_track = true; - - assert!( - desc.connection_information.is_some(), - "connection_information should not be None, even for rejected tracks" - ); - assert_eq!( - desc.media_name.formats, - vec!["0"], - "Format for rejected track should be 0" - ); - assert_eq!( - desc.media_name.port.value, 0, - "Port for rejected track should be 0" - ); - } - - assert!( - found_rejected_track, - "There should've been a rejected track" - ); - - Ok(()) -} - -#[test] -fn test_get_rids() { - let m = vec![MediaDescription { - media_name: MediaName { - media: "video".to_owned(), - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "sendonly".to_owned(), - value: None, - }, - Attribute { - key: SDP_ATTRIBUTE_RID.to_owned(), - value: Some("f send pt=97;max-width=1280;max-height=720".to_owned()), - }, - ], - ..Default::default() - }]; - - let rids = get_rids(&m[0]); - - assert!(!rids.is_empty(), "Rid mapping should be present"); - - let f = rids.iter().find(|rid| rid.id == "f"); - assert!(f.is_some(), "rid values should contain 'f'"); -} - -#[test] -fn test_codecs_from_media_description() -> Result<()> { - //"Codec Only" - { - let codecs = codecs_from_media_description(&MediaDescription { - media_name: MediaName { - media: "audio".to_owned(), - formats: vec!["111".to_owned()], - ..Default::default() - }, - attributes: vec![Attribute { - key: "rtpmap".to_owned(), - value: Some("111 opus/48000/2".to_owned()), - }], - ..Default::default() - })?; - - assert_eq!( - codecs, - vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }], - ); - } - - //"Codec with fmtp/rtcp-fb" - { - let codecs = codecs_from_media_description(&MediaDescription { - media_name: MediaName { - media: "audio".to_owned(), - formats: vec!["111".to_owned()], - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "rtpmap".to_owned(), - value: Some("111 opus/48000/2".to_owned()), - }, - Attribute { - key: "fmtp".to_owned(), - value: Some("111 minptime=10;useinbandfec=1".to_owned()), - }, - Attribute { - key: "rtcp-fb".to_owned(), - value: Some("111 goog-remb".to_owned()), - }, - Attribute { - key: "rtcp-fb".to_owned(), - value: Some("111 ccm fir".to_owned()), - }, - ], - ..Default::default() - })?; - - assert_eq!( - codecs, - vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "minptime=10;useinbandfec=1".to_owned(), - rtcp_feedback: vec![ - RTCPFeedback { - typ: "goog-remb".to_owned(), - parameter: "".to_owned() - }, - RTCPFeedback { - typ: "ccm".to_owned(), - parameter: "fir".to_owned() - } - ] - }, - payload_type: 111, - ..Default::default() - }], - ); - } - - Ok(()) -} - -#[test] -fn test_rtp_extensions_from_media_description() -> Result<()> { - let extensions = rtp_extensions_from_media_description(&MediaDescription { - media_name: MediaName { - media: "audio".to_owned(), - formats: vec!["111".to_owned()], - ..Default::default() - }, - attributes: vec![ - Attribute { - key: "extmap".to_owned(), - value: Some("1 ".to_owned() + sdp::extmap::ABS_SEND_TIME_URI), - }, - Attribute { - key: "extmap".to_owned(), - value: Some("3 ".to_owned() + sdp::extmap::SDES_MID_URI), - }, - ], - ..Default::default() - })?; - - assert_eq!(extensions[sdp::extmap::ABS_SEND_TIME_URI], 1); - assert_eq!(extensions[sdp::extmap::SDES_MID_URI], 3); - - Ok(()) -} diff --git a/webrtc/src/peer_connection/sdp/sdp_type.rs b/webrtc/src/peer_connection/sdp/sdp_type.rs deleted file mode 100644 index 830864f79..000000000 --- a/webrtc/src/peer_connection/sdp/sdp_type.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// SDPType describes the type of an SessionDescription. -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub enum RTCSdpType { - #[default] - Unspecified = 0, - - /// indicates that a description MUST be treated as an SDP offer. - #[serde(rename = "offer")] - Offer, - - /// indicates that a description MUST be treated as an - /// SDP answer, but not a final answer. A description used as an SDP - /// pranswer may be applied as a response to an SDP offer, or an update to - /// a previously sent SDP pranswer. - #[serde(rename = "pranswer")] - Pranswer, - - /// indicates that a description MUST be treated as an SDP - /// final answer, and the offer-answer exchange MUST be considered complete. - /// A description used as an SDP answer may be applied as a response to an - /// SDP offer or as an update to a previously sent SDP pranswer. - #[serde(rename = "answer")] - Answer, - - /// indicates that a description MUST be treated as - /// canceling the current SDP negotiation and moving the SDP offer and - /// answer back to what it was in the previous stable state. Note the - /// local or remote SDP descriptions in the previous stable state could be - /// null if there has not yet been a successful offer-answer negotiation. - #[serde(rename = "rollback")] - Rollback, -} - -const SDP_TYPE_OFFER_STR: &str = "offer"; -const SDP_TYPE_PRANSWER_STR: &str = "pranswer"; -const SDP_TYPE_ANSWER_STR: &str = "answer"; -const SDP_TYPE_ROLLBACK_STR: &str = "rollback"; - -/// creates an SDPType from a string -impl From<&str> for RTCSdpType { - fn from(raw: &str) -> Self { - match raw { - SDP_TYPE_OFFER_STR => RTCSdpType::Offer, - SDP_TYPE_PRANSWER_STR => RTCSdpType::Pranswer, - SDP_TYPE_ANSWER_STR => RTCSdpType::Answer, - SDP_TYPE_ROLLBACK_STR => RTCSdpType::Rollback, - _ => RTCSdpType::Unspecified, - } - } -} - -impl fmt::Display for RTCSdpType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCSdpType::Offer => write!(f, "{SDP_TYPE_OFFER_STR}"), - RTCSdpType::Pranswer => write!(f, "{SDP_TYPE_PRANSWER_STR}"), - RTCSdpType::Answer => write!(f, "{SDP_TYPE_ANSWER_STR}"), - RTCSdpType::Rollback => write!(f, "{SDP_TYPE_ROLLBACK_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_sdp_type() { - let tests = vec![ - ("Unspecified", RTCSdpType::Unspecified), - ("offer", RTCSdpType::Offer), - ("pranswer", RTCSdpType::Pranswer), - ("answer", RTCSdpType::Answer), - ("rollback", RTCSdpType::Rollback), - ]; - - for (sdp_type_string, expected_sdp_type) in tests { - assert_eq!(RTCSdpType::from(sdp_type_string), expected_sdp_type); - } - } - - #[test] - fn test_sdp_type_string() { - let tests = vec![ - (RTCSdpType::Unspecified, "Unspecified"), - (RTCSdpType::Offer, "offer"), - (RTCSdpType::Pranswer, "pranswer"), - (RTCSdpType::Answer, "answer"), - (RTCSdpType::Rollback, "rollback"), - ]; - - for (sdp_type, expected_string) in tests { - assert_eq!(sdp_type.to_string(), expected_string); - } - } -} diff --git a/webrtc/src/peer_connection/sdp/session_description.rs b/webrtc/src/peer_connection/sdp/session_description.rs deleted file mode 100644 index 7085f34dd..000000000 --- a/webrtc/src/peer_connection/sdp/session_description.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::io::Cursor; - -use sdp::description::session::SessionDescription; -use serde::{Deserialize, Serialize}; - -use super::sdp_type::RTCSdpType; -use crate::error::Result; - -/// SessionDescription is used to expose local and remote session descriptions. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RTCSessionDescription { - #[serde(rename = "type")] - pub sdp_type: RTCSdpType, - - pub sdp: String, - - /// This will never be initialized by callers, internal use only - #[serde(skip)] - pub(crate) parsed: Option, -} - -impl RTCSessionDescription { - /// Given SDP representing an answer, wrap it in an RTCSessionDescription - /// that can be given to an RTCPeerConnection. - pub fn answer(sdp: String) -> Result { - let mut desc = RTCSessionDescription { - sdp, - sdp_type: RTCSdpType::Answer, - parsed: None, - }; - - let parsed = desc.unmarshal()?; - desc.parsed = Some(parsed); - - Ok(desc) - } - - /// Given SDP representing an offer, wrap it in an RTCSessionDescription - /// that can be given to an RTCPeerConnection. - pub fn offer(sdp: String) -> Result { - let mut desc = RTCSessionDescription { - sdp, - sdp_type: RTCSdpType::Offer, - parsed: None, - }; - - let parsed = desc.unmarshal()?; - desc.parsed = Some(parsed); - - Ok(desc) - } - - /// Given SDP representing an answer, wrap it in an RTCSessionDescription - /// that can be given to an RTCPeerConnection. `pranswer` is used when the - /// answer may not be final, or when updating a previously sent pranswer. - pub fn pranswer(sdp: String) -> Result { - let mut desc = RTCSessionDescription { - sdp, - sdp_type: RTCSdpType::Pranswer, - parsed: None, - }; - - let parsed = desc.unmarshal()?; - desc.parsed = Some(parsed); - - Ok(desc) - } - - /// Unmarshal is a helper to deserialize the sdp - pub fn unmarshal(&self) -> Result { - let mut reader = Cursor::new(self.sdp.as_bytes()); - let parsed = SessionDescription::unmarshal(&mut reader)?; - Ok(parsed) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::api::media_engine::MediaEngine; - use crate::api::APIBuilder; - use crate::peer_connection::configuration::RTCConfiguration; - - #[test] - fn test_session_description_json() { - let tests = vec![ - ( - RTCSessionDescription { - sdp_type: RTCSdpType::Offer, - sdp: "sdp".to_owned(), - parsed: None, - }, - r#"{"type":"offer","sdp":"sdp"}"#, - ), - ( - RTCSessionDescription { - sdp_type: RTCSdpType::Pranswer, - sdp: "sdp".to_owned(), - parsed: None, - }, - r#"{"type":"pranswer","sdp":"sdp"}"#, - ), - ( - RTCSessionDescription { - sdp_type: RTCSdpType::Answer, - sdp: "sdp".to_owned(), - parsed: None, - }, - r#"{"type":"answer","sdp":"sdp"}"#, - ), - ( - RTCSessionDescription { - sdp_type: RTCSdpType::Rollback, - sdp: "sdp".to_owned(), - parsed: None, - }, - r#"{"type":"rollback","sdp":"sdp"}"#, - ), - ( - RTCSessionDescription { - sdp_type: RTCSdpType::Unspecified, - sdp: "sdp".to_owned(), - parsed: None, - }, - r#"{"type":"Unspecified","sdp":"sdp"}"#, - ), - ]; - - for (desc, expected_string) in tests { - let result = serde_json::to_string(&desc); - assert!(result.is_ok(), "testCase: marshal err: {result:?}"); - let desc_data = result.unwrap(); - assert_eq!(desc_data, expected_string, "string is not expected"); - - let result = serde_json::from_str::(&desc_data); - assert!(result.is_ok(), "testCase: unmarshal err: {result:?}"); - if let Ok(sd) = result { - assert!(sd.sdp == desc.sdp && sd.sdp_type == desc.sdp_type); - } - } - } - - #[tokio::test] - async fn test_session_description_answer() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let offer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - let answer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let _ = offer_pc.create_data_channel("foo", None).await?; - let offer = offer_pc.create_offer(None).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - - let desc = RTCSessionDescription::answer(answer.sdp.clone())?; - - assert!(desc.sdp_type == RTCSdpType::Answer); - assert!(desc.parsed.is_some()); - - assert_eq!(answer.unmarshal()?.marshal(), desc.unmarshal()?.marshal()); - - Ok(()) - } - - #[tokio::test] - async fn test_session_description_offer() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let pc = api.new_peer_connection(RTCConfiguration::default()).await?; - let offer = pc.create_offer(None).await?; - - let desc = RTCSessionDescription::offer(offer.sdp.clone())?; - - assert!(desc.sdp_type == RTCSdpType::Offer); - assert!(desc.parsed.is_some()); - - assert_eq!(offer.unmarshal()?.marshal(), desc.unmarshal()?.marshal()); - - Ok(()) - } - - #[tokio::test] - async fn test_session_description_pranswer() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let offer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - let answer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let _ = offer_pc.create_data_channel("foo", None).await?; - let offer = offer_pc.create_offer(None).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - - let desc = RTCSessionDescription::pranswer(answer.sdp)?; - - assert!(desc.sdp_type == RTCSdpType::Pranswer); - assert!(desc.parsed.is_some()); - - Ok(()) - } - - #[tokio::test] - async fn test_session_description_unmarshal() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let offer = pc.create_offer(None).await?; - - let desc = RTCSessionDescription { - sdp_type: offer.sdp_type, - sdp: offer.sdp, - ..Default::default() - }; - - assert!(desc.parsed.is_none()); - - let parsed1 = desc.unmarshal()?; - let parsed2 = desc.unmarshal()?; - - pc.close().await?; - - // check if the two parsed results _really_ match, could be affected by internal caching - assert_eq!(parsed1.marshal(), parsed2.marshal()); - - Ok(()) - } -} diff --git a/webrtc/src/peer_connection/signaling_state.rs b/webrtc/src/peer_connection/signaling_state.rs deleted file mode 100644 index 2edd613be..000000000 --- a/webrtc/src/peer_connection/signaling_state.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::fmt; - -use crate::error::{Error, Result}; -use crate::peer_connection::sdp::sdp_type::RTCSdpType; - -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum StateChangeOp { - #[default] - SetLocal, - SetRemote, -} - -impl fmt::Display for StateChangeOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - StateChangeOp::SetLocal => write!(f, "SetLocal"), - StateChangeOp::SetRemote => write!(f, "SetRemote"), - //_ => write!(f, UNSPECIFIED_STR), - } - } -} - -/// SignalingState indicates the signaling state of the offer/answer process. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCSignalingState { - #[default] - Unspecified = 0, - - /// SignalingStateStable indicates there is no offer/answer exchange in - /// progress. This is also the initial state, in which case the local and - /// remote descriptions are nil. - Stable, - - /// SignalingStateHaveLocalOffer indicates that a local description, of - /// type "offer", has been successfully applied. - HaveLocalOffer, - - /// SignalingStateHaveRemoteOffer indicates that a remote description, of - /// type "offer", has been successfully applied. - HaveRemoteOffer, - - /// SignalingStateHaveLocalPranswer indicates that a remote description - /// of type "offer" has been successfully applied and a local description - /// of type "pranswer" has been successfully applied. - HaveLocalPranswer, - - /// SignalingStateHaveRemotePranswer indicates that a local description - /// of type "offer" has been successfully applied and a remote description - /// of type "pranswer" has been successfully applied. - HaveRemotePranswer, - - /// SignalingStateClosed indicates The PeerConnection has been closed. - Closed, -} - -const SIGNALING_STATE_STABLE_STR: &str = "stable"; -const SIGNALING_STATE_HAVE_LOCAL_OFFER_STR: &str = "have-local-offer"; -const SIGNALING_STATE_HAVE_REMOTE_OFFER_STR: &str = "have-remote-offer"; -const SIGNALING_STATE_HAVE_LOCAL_PRANSWER_STR: &str = "have-local-pranswer"; -const SIGNALING_STATE_HAVE_REMOTE_PRANSWER_STR: &str = "have-remote-pranswer"; -const SIGNALING_STATE_CLOSED_STR: &str = "closed"; - -impl From<&str> for RTCSignalingState { - fn from(raw: &str) -> Self { - match raw { - SIGNALING_STATE_STABLE_STR => RTCSignalingState::Stable, - SIGNALING_STATE_HAVE_LOCAL_OFFER_STR => RTCSignalingState::HaveLocalOffer, - SIGNALING_STATE_HAVE_REMOTE_OFFER_STR => RTCSignalingState::HaveRemoteOffer, - SIGNALING_STATE_HAVE_LOCAL_PRANSWER_STR => RTCSignalingState::HaveLocalPranswer, - SIGNALING_STATE_HAVE_REMOTE_PRANSWER_STR => RTCSignalingState::HaveRemotePranswer, - SIGNALING_STATE_CLOSED_STR => RTCSignalingState::Closed, - _ => RTCSignalingState::Unspecified, - } - } -} - -impl fmt::Display for RTCSignalingState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCSignalingState::Stable => write!(f, "{SIGNALING_STATE_STABLE_STR}"), - RTCSignalingState::HaveLocalOffer => { - write!(f, "{SIGNALING_STATE_HAVE_LOCAL_OFFER_STR}") - } - RTCSignalingState::HaveRemoteOffer => { - write!(f, "{SIGNALING_STATE_HAVE_REMOTE_OFFER_STR}") - } - RTCSignalingState::HaveLocalPranswer => { - write!(f, "{SIGNALING_STATE_HAVE_LOCAL_PRANSWER_STR}") - } - RTCSignalingState::HaveRemotePranswer => { - write!(f, "{SIGNALING_STATE_HAVE_REMOTE_PRANSWER_STR}") - } - RTCSignalingState::Closed => write!(f, "{SIGNALING_STATE_CLOSED_STR}"), - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -impl From for RTCSignalingState { - fn from(v: u8) -> Self { - match v { - 1 => RTCSignalingState::Stable, - 2 => RTCSignalingState::HaveLocalOffer, - 3 => RTCSignalingState::HaveRemoteOffer, - 4 => RTCSignalingState::HaveLocalPranswer, - 5 => RTCSignalingState::HaveRemotePranswer, - 6 => RTCSignalingState::Closed, - _ => RTCSignalingState::Unspecified, - } - } -} - -pub(crate) fn check_next_signaling_state( - cur: RTCSignalingState, - next: RTCSignalingState, - op: StateChangeOp, - sdp_type: RTCSdpType, -) -> Result { - // Special case for rollbacks - if sdp_type == RTCSdpType::Rollback && cur == RTCSignalingState::Stable { - return Err(Error::ErrSignalingStateCannotRollback); - } - - // 4.3.1 valid state transitions - match cur { - RTCSignalingState::Stable => { - match op { - StateChangeOp::SetLocal => { - // stable->SetLocal(offer)->have-local-offer - if sdp_type == RTCSdpType::Offer && next == RTCSignalingState::HaveLocalOffer { - return Ok(next); - } - } - StateChangeOp::SetRemote => { - // stable->SetRemote(offer)->have-remote-offer - if sdp_type == RTCSdpType::Offer && next == RTCSignalingState::HaveRemoteOffer { - return Ok(next); - } - } - } - } - RTCSignalingState::HaveLocalOffer => { - if op == StateChangeOp::SetRemote { - match sdp_type { - // have-local-offer->SetRemote(answer)->stable - RTCSdpType::Answer => { - if next == RTCSignalingState::Stable { - return Ok(next); - } - } - // have-local-offer->SetRemote(pranswer)->have-remote-pranswer - RTCSdpType::Pranswer => { - if next == RTCSignalingState::HaveRemotePranswer { - return Ok(next); - } - } - _ => {} - } - } else if op == StateChangeOp::SetLocal - && sdp_type == RTCSdpType::Offer - && next == RTCSignalingState::HaveLocalOffer - { - return Ok(next); - } - } - RTCSignalingState::HaveRemotePranswer => { - if op == StateChangeOp::SetRemote && sdp_type == RTCSdpType::Answer { - // have-remote-pranswer->SetRemote(answer)->stable - if next == RTCSignalingState::Stable { - return Ok(next); - } - } - } - RTCSignalingState::HaveRemoteOffer => { - if op == StateChangeOp::SetLocal { - match sdp_type { - // have-remote-offer->SetLocal(answer)->stable - RTCSdpType::Answer => { - if next == RTCSignalingState::Stable { - return Ok(next); - } - } - // have-remote-offer->SetLocal(pranswer)->have-local-pranswer - RTCSdpType::Pranswer => { - if next == RTCSignalingState::HaveLocalPranswer { - return Ok(next); - } - } - _ => {} - } - } - } - RTCSignalingState::HaveLocalPranswer => { - if op == StateChangeOp::SetLocal && sdp_type == RTCSdpType::Answer { - // have-local-pranswer->SetLocal(answer)->stable - if next == RTCSignalingState::Stable { - return Ok(next); - } - } - } - _ => { - return Err(Error::ErrSignalingStateProposedTransitionInvalid { - from: cur, - applying: sdp_type, - is_local: op == StateChangeOp::SetLocal, - }); - } - }; - - Err(Error::ErrSignalingStateProposedTransitionInvalid { - from: cur, - is_local: op == StateChangeOp::SetLocal, - applying: sdp_type, - }) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_signaling_state() { - let tests = vec![ - ("Unspecified", RTCSignalingState::Unspecified), - ("stable", RTCSignalingState::Stable), - ("have-local-offer", RTCSignalingState::HaveLocalOffer), - ("have-remote-offer", RTCSignalingState::HaveRemoteOffer), - ("have-local-pranswer", RTCSignalingState::HaveLocalPranswer), - ( - "have-remote-pranswer", - RTCSignalingState::HaveRemotePranswer, - ), - ("closed", RTCSignalingState::Closed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!(RTCSignalingState::from(state_string), expected_state); - } - } - - #[test] - fn test_signaling_state_string() { - let tests = vec![ - (RTCSignalingState::Unspecified, "Unspecified"), - (RTCSignalingState::Stable, "stable"), - (RTCSignalingState::HaveLocalOffer, "have-local-offer"), - (RTCSignalingState::HaveRemoteOffer, "have-remote-offer"), - (RTCSignalingState::HaveLocalPranswer, "have-local-pranswer"), - ( - RTCSignalingState::HaveRemotePranswer, - "have-remote-pranswer", - ), - (RTCSignalingState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string); - } - } - - #[test] - fn test_signaling_state_transitions() { - let tests = vec![ - ( - "stable->SetLocal(offer)->have-local-offer", - RTCSignalingState::Stable, - RTCSignalingState::HaveLocalOffer, - StateChangeOp::SetLocal, - RTCSdpType::Offer, - None, - ), - ( - "stable->SetRemote(offer)->have-remote-offer", - RTCSignalingState::Stable, - RTCSignalingState::HaveRemoteOffer, - StateChangeOp::SetRemote, - RTCSdpType::Offer, - None, - ), - ( - "have-local-offer->SetRemote(answer)->stable", - RTCSignalingState::HaveLocalOffer, - RTCSignalingState::Stable, - StateChangeOp::SetRemote, - RTCSdpType::Answer, - None, - ), - ( - "have-local-offer->SetRemote(pranswer)->have-remote-pranswer", - RTCSignalingState::HaveLocalOffer, - RTCSignalingState::HaveRemotePranswer, - StateChangeOp::SetRemote, - RTCSdpType::Pranswer, - None, - ), - ( - "have-remote-pranswer->SetRemote(answer)->stable", - RTCSignalingState::HaveRemotePranswer, - RTCSignalingState::Stable, - StateChangeOp::SetRemote, - RTCSdpType::Answer, - None, - ), - ( - "have-remote-offer->SetLocal(answer)->stable", - RTCSignalingState::HaveRemoteOffer, - RTCSignalingState::Stable, - StateChangeOp::SetLocal, - RTCSdpType::Answer, - None, - ), - ( - "have-remote-offer->SetLocal(pranswer)->have-local-pranswer", - RTCSignalingState::HaveRemoteOffer, - RTCSignalingState::HaveLocalPranswer, - StateChangeOp::SetLocal, - RTCSdpType::Pranswer, - None, - ), - ( - "have-local-pranswer->SetLocal(answer)->stable", - RTCSignalingState::HaveLocalPranswer, - RTCSignalingState::Stable, - StateChangeOp::SetLocal, - RTCSdpType::Answer, - None, - ), - ( - "(invalid) stable->SetRemote(pranswer)->have-remote-pranswer", - RTCSignalingState::Stable, - RTCSignalingState::HaveRemotePranswer, - StateChangeOp::SetRemote, - RTCSdpType::Pranswer, - Some(Error::ErrSignalingStateProposedTransitionInvalid { - from: RTCSignalingState::Stable, - is_local: false, - applying: RTCSdpType::Pranswer, - }), - ), - ( - "(invalid) stable->SetRemote(rollback)->have-local-offer", - RTCSignalingState::Stable, - RTCSignalingState::HaveLocalOffer, - StateChangeOp::SetRemote, - RTCSdpType::Rollback, - Some(Error::ErrSignalingStateCannotRollback), - ), - ]; - - for (desc, cur, next, op, sdp_type, expected_err) in tests { - let result = check_next_signaling_state(cur, next, op, sdp_type); - match (&result, &expected_err) { - (Ok(got), None) => { - assert_eq!(*got, next, "{desc} state mismatch"); - } - (Err(got), Some(err)) => { - assert_eq!(got.to_string(), err.to_string(), "{desc} error mismatch"); - } - _ => { - panic!("{desc}: expected {expected_err:?}, but got {result:?}"); - } - }; - } - } -} diff --git a/webrtc/src/rtp_transceiver/fmtp/generic/generic_test.rs b/webrtc/src/rtp_transceiver/fmtp/generic/generic_test.rs deleted file mode 100644 index f37c4af5c..000000000 --- a/webrtc/src/rtp_transceiver/fmtp/generic/generic_test.rs +++ /dev/null @@ -1,160 +0,0 @@ -use super::*; - -#[test] -fn test_generic_fmtp_parse() { - let tests: Vec<(&str, &str, Box)> = vec![ - ( - "OneParam", - "key-name=value", - Box::new(GenericFmtp { - mime_type: "generic".to_owned(), - parameters: [("key-name".to_owned(), "value".to_owned())] - .iter() - .cloned() - .collect(), - }), - ), - ( - "OneParamWithWhiteSpeces", - "\tkey-name=value ", - Box::new(GenericFmtp { - mime_type: "generic".to_owned(), - parameters: [("key-name".to_owned(), "value".to_owned())] - .iter() - .cloned() - .collect(), - }), - ), - ( - "TwoParams", - "key-name=value;key2=value2", - Box::new(GenericFmtp { - mime_type: "generic".to_owned(), - parameters: [ - ("key-name".to_owned(), "value".to_owned()), - ("key2".to_owned(), "value2".to_owned()), - ] - .iter() - .cloned() - .collect(), - }), - ), - ( - "TwoParamsWithWhiteSpeces", - "key-name=value; \n\tkey2=value2 ", - Box::new(GenericFmtp { - mime_type: "generic".to_owned(), - parameters: [ - ("key-name".to_owned(), "value".to_owned()), - ("key2".to_owned(), "value2".to_owned()), - ] - .iter() - .cloned() - .collect(), - }), - ), - ]; - - for (name, input, expected) in tests { - let f = parse("generic", input); - assert_eq!(&f, &expected, "{name} failed"); - - assert_eq!(f.mime_type(), "generic"); - } -} - -#[test] -fn test_generic_fmtp_compare() { - let consist_string: HashMap = [ - (true, "consist".to_owned()), - (false, "inconsist".to_owned()), - ] - .iter() - .cloned() - .collect(); - - let tests = vec![ - ( - "Equal", - "key1=value1;key2=value2;key3=value3", - "key1=value1;key2=value2;key3=value3", - true, - ), - ( - "EqualWithWhitespaceVariants", - "key1=value1;key2=value2;key3=value3", - " key1=value1; \nkey2=value2;\t\nkey3=value3", - true, - ), - ( - "EqualWithCase", - "key1=value1;key2=value2;key3=value3", - "key1=value1;key2=Value2;Key3=value3", - true, - ), - ( - "OneHasExtraParam", - "key1=value1;key2=value2;key3=value3", - "key1=value1;key2=value2;key3=value3;key4=value4", - true, - ), - ( - "Inconsistent", - "key1=value1;key2=value2;key3=value3", - "key1=value1;key2=different_value;key3=value3", - false, - ), - ( - "Inconsistent_OneHasExtraParam", - "key1=value1;key2=value2;key3=value3;key4=value4", - "key1=value1;key2=different_value;key3=value3", - false, - ), - ]; - - for (name, a, b, consist) in tests { - let check = |a, b| { - let aa = parse("", a); - let bb = parse("", b); - - // test forward case here - let c = aa.match_fmtp(&*bb); - assert_eq!( - c, - consist, - "{}: '{}' and '{}' are expected to be {:?}, but treated as {:?}", - name, - a, - b, - consist_string.get(&consist), - consist_string.get(&c), - ); - - // test reverse case here - let c = bb.match_fmtp(&*aa); - assert_eq!( - c, - consist, - "{}: '{}' and '{}' are expected to be {:?}, but treated as {:?}", - name, - a, - b, - consist_string.get(&consist), - consist_string.get(&c), - ); - }; - - check(a, b); - } -} - -#[test] -fn test_generic_fmtp_compare_mime_type_case_mismatch() { - let a = parse("video/vp8", ""); - let b = parse("video/VP8", ""); - - assert!( - b.match_fmtp(&*a), - "fmtp lines should match even if they use different casing" - ); -} diff --git a/webrtc/src/rtp_transceiver/fmtp/generic/mod.rs b/webrtc/src/rtp_transceiver/fmtp/generic/mod.rs deleted file mode 100644 index 4b75ce350..000000000 --- a/webrtc/src/rtp_transceiver/fmtp/generic/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -#[cfg(test)] -mod generic_test; - -use super::*; - -/// fmtp_consist checks that two FMTP parameters are not inconsistent. -fn fmtp_consist(a: &HashMap, b: &HashMap) -> bool { - //TODO: add unicode case-folding equal support - for (k, v) in a { - if let Some(vb) = b.get(k) { - if vb.to_uppercase() != v.to_uppercase() { - return false; - } - } - } - for (k, v) in b { - if let Some(va) = a.get(k) { - if va.to_uppercase() != v.to_uppercase() { - return false; - } - } - } - true -} - -#[derive(Debug, PartialEq)] -pub(crate) struct GenericFmtp { - pub(crate) mime_type: String, - pub(crate) parameters: HashMap, -} - -impl Fmtp for GenericFmtp { - fn mime_type(&self) -> &str { - self.mime_type.as_str() - } - - /// Match returns true if g and b are compatible fmtp descriptions - /// The generic implementation is used for MimeTypes that are not defined - fn match_fmtp(&self, f: &(dyn Fmtp)) -> bool { - if let Some(c) = f.as_any().downcast_ref::() { - if self.mime_type.to_lowercase() != c.mime_type().to_lowercase() { - return false; - } - - fmtp_consist(&self.parameters, &c.parameters) - } else { - false - } - } - - fn parameter(&self, key: &str) -> Option<&String> { - self.parameters.get(key) - } - - fn equal(&self, other: &(dyn Fmtp)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn as_any(&self) -> &(dyn Any) { - self - } -} diff --git a/webrtc/src/rtp_transceiver/fmtp/h264/h264_test.rs b/webrtc/src/rtp_transceiver/fmtp/h264/h264_test.rs deleted file mode 100644 index fe97dbe05..000000000 --- a/webrtc/src/rtp_transceiver/fmtp/h264/h264_test.rs +++ /dev/null @@ -1,163 +0,0 @@ -use super::*; - -#[test] -fn test_h264_fmtp_parse() { - let tests: Vec<(&str, &str, Box)> = vec![ - ( - "OneParam", - "key-name=value", - Box::new(H264Fmtp { - parameters: [("key-name".to_owned(), "value".to_owned())] - .iter() - .cloned() - .collect(), - }), - ), - ( - "OneParamWithWhiteSpeces", - "\tkey-name=value ", - Box::new(H264Fmtp { - parameters: [("key-name".to_owned(), "value".to_owned())] - .iter() - .cloned() - .collect(), - }), - ), - ( - "TwoParams", - "key-name=value;key2=value2", - Box::new(H264Fmtp { - parameters: [ - ("key-name".to_owned(), "value".to_owned()), - ("key2".to_owned(), "value2".to_owned()), - ] - .iter() - .cloned() - .collect(), - }), - ), - ( - "TwoParamsWithWhiteSpeces", - "key-name=value; \n\tkey2=value2 ", - Box::new(H264Fmtp { - parameters: [ - ("key-name".to_owned(), "value".to_owned()), - ("key2".to_owned(), "value2".to_owned()), - ] - .iter() - .cloned() - .collect(), - }), - ), - ]; - - for (name, input, expected) in tests { - let f = parse("video/h264", input); - assert_eq!(&f, &expected, "{name} failed"); - - assert_eq!(f.mime_type(), "video/h264"); - } -} - -#[test] -fn test_h264_fmtp_compare() { - let consist_string: HashMap = [ - (true, "consist".to_owned()), - (false, "inconsist".to_owned()), - ] - .iter() - .cloned() - .collect(); - - let tests = vec![ - ( - "Equal", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - true, - ), - ( - "EqualWithWhitespaceVariants", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - " level-asymmetry-allowed=1; \npacketization-mode=1;\t\nprofile-level-id=42e01f", - true, - ), - ( - "EqualWithCase", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - "level-asymmetry-allowed=1;packetization-mode=1;PROFILE-LEVEL-ID=42e01f", - true, - ), - ( - "OneHasExtraParam", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - "packetization-mode=1;profile-level-id=42e01f", - true, - ), - ( - "DifferentProfileLevelIDVersions", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - "packetization-mode=1;profile-level-id=42e029", - true, - ), - ( - "Inconsistent", - "packetization-mode=1;profile-level-id=42e029", - "packetization-mode=0;profile-level-id=42e029", - false, - ), - ( - "Inconsistent_MissingPacketizationMode", - "packetization-mode=1;profile-level-id=42e029", - "profile-level-id=42e029", - false, - ), - ( - "Inconsistent_MissingProfileLevelID", - "packetization-mode=1;profile-level-id=42e029", - "packetization-mode=1", - false, - ), - ( - "Inconsistent_InvalidProfileLevelID", - "packetization-mode=1;profile-level-id=42e029", - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=41e029", - false, - ), - ]; - - for (name, a, b, consist) in tests { - let check = |a, b| { - let aa = parse("video/h264", a); - let bb = parse("video/h264", b); - - // test forward case here - let c = aa.match_fmtp(&*bb); - assert_eq!( - c, - consist, - "{}: '{}' and '{}' are expected to be {:?}, but treated as {:?}", - name, - a, - b, - consist_string.get(&consist), - consist_string.get(&c), - ); - - // test reverse case here - let c = bb.match_fmtp(&*aa); - assert_eq!( - c, - consist, - "{}: '{}' and '{}' are expected to be {:?}, but treated as {:?}", - name, - a, - b, - consist_string.get(&consist), - consist_string.get(&c), - ); - }; - - check(a, b); - } -} diff --git a/webrtc/src/rtp_transceiver/fmtp/h264/mod.rs b/webrtc/src/rtp_transceiver/fmtp/h264/mod.rs deleted file mode 100644 index 22a5bf594..000000000 --- a/webrtc/src/rtp_transceiver/fmtp/h264/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -#[cfg(test)] -mod h264_test; - -use super::*; - -fn profile_level_id_matches(a: &str, b: &str) -> bool { - let aa = match hex::decode(a) { - Ok(aa) => { - if aa.len() < 2 { - return false; - } - aa - } - Err(_) => return false, - }; - - let bb = match hex::decode(b) { - Ok(bb) => { - if bb.len() < 2 { - return false; - } - bb - } - Err(_) => return false, - }; - - aa[0] == bb[0] && aa[1] == bb[1] -} - -#[derive(Debug, PartialEq)] -pub(crate) struct H264Fmtp { - pub(crate) parameters: HashMap, -} - -impl Fmtp for H264Fmtp { - fn mime_type(&self) -> &str { - "video/h264" - } - - /// Match returns true if h and b are compatible fmtp descriptions - /// Based on RFC6184 Section 8.2.2: - /// The parameters identifying a media format configuration for H.264 - /// are profile-level-id and packetization-mode. These media format - /// configuration parameters (except for the level part of profile- - /// level-id) MUST be used symmetrically; that is, the answerer MUST - /// either maintain all configuration parameters or remove the media - /// format (payload type) completely if one or more of the parameter - /// values are not supported. - /// Informative note: The requirement for symmetric use does not - /// apply for the level part of profile-level-id and does not apply - /// for the other stream properties and capability parameters. - fn match_fmtp(&self, f: &(dyn Fmtp)) -> bool { - if let Some(c) = f.as_any().downcast_ref::() { - // test packetization-mode - let hpmode = match self.parameters.get("packetization-mode") { - Some(s) => s, - None => return false, - }; - let cpmode = match c.parameters.get("packetization-mode") { - Some(s) => s, - None => return false, - }; - - if hpmode != cpmode { - return false; - } - - // test profile-level-id - let hplid = match self.parameters.get("profile-level-id") { - Some(s) => s, - None => return false, - }; - let cplid = match c.parameters.get("profile-level-id") { - Some(s) => s, - None => return false, - }; - - if !profile_level_id_matches(hplid, cplid) { - return false; - } - - true - } else { - false - } - } - - fn parameter(&self, key: &str) -> Option<&String> { - self.parameters.get(key) - } - - fn equal(&self, other: &(dyn Fmtp)) -> bool { - other - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn as_any(&self) -> &(dyn Any) { - self - } -} diff --git a/webrtc/src/rtp_transceiver/fmtp/mod.rs b/webrtc/src/rtp_transceiver/fmtp/mod.rs deleted file mode 100644 index ea6e83b39..000000000 --- a/webrtc/src/rtp_transceiver/fmtp/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -pub(crate) mod generic; -pub(crate) mod h264; - -use std::any::Any; -use std::collections::HashMap; -use std::fmt; - -use crate::rtp_transceiver::fmtp::generic::GenericFmtp; -use crate::rtp_transceiver::fmtp::h264::H264Fmtp; - -/// Fmtp interface for implementing custom -/// Fmtp parsers based on mime_type -pub trait Fmtp: fmt::Debug { - /// mime_type returns the mime_type associated with - /// the fmtp - fn mime_type(&self) -> &str; - - /// match_fmtp compares two fmtp descriptions for - /// compatibility based on the mime_type - fn match_fmtp(&self, f: &(dyn Fmtp)) -> bool; - - /// parameter returns a value for the associated key - /// if contained in the parsed fmtp string - fn parameter(&self, key: &str) -> Option<&String>; - - fn equal(&self, other: &(dyn Fmtp)) -> bool; - fn as_any(&self) -> &(dyn Any); -} - -impl PartialEq for dyn Fmtp { - fn eq(&self, other: &Self) -> bool { - self.equal(other) - } -} - -/// parse parses an fmtp string based on the MimeType -pub fn parse(mime_type: &str, line: &str) -> Box { - let mut parameters = HashMap::new(); - for p in line.split(';').collect::>() { - let pp: Vec<&str> = p.trim().splitn(2, '=').collect(); - let key = pp[0].to_lowercase(); - let value = if pp.len() > 1 { - pp[1].to_owned() - } else { - String::new() - }; - parameters.insert(key, value); - } - - if mime_type.to_uppercase() == "video/h264".to_uppercase() { - Box::new(H264Fmtp { parameters }) - } else { - Box::new(GenericFmtp { - mime_type: mime_type.to_owned(), - parameters, - }) - } -} diff --git a/webrtc/src/rtp_transceiver/mod.rs b/webrtc/src/rtp_transceiver/mod.rs deleted file mode 100644 index e634cc2d4..000000000 --- a/webrtc/src/rtp_transceiver/mod.rs +++ /dev/null @@ -1,561 +0,0 @@ -#[cfg(test)] -mod rtp_transceiver_test; - -use std::fmt; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use interceptor::stream_info::{RTPHeaderExtension, StreamInfo}; -use interceptor::Attributes; -use log::trace; -use portable_atomic::{AtomicBool, AtomicU8}; -use serde::{Deserialize, Serialize}; -use smol_str::SmolStr; -use tokio::sync::{Mutex, OnceCell}; -use util::Unmarshal; - -use crate::api::media_engine::MediaEngine; -use crate::error::{Error, Result}; -use crate::rtp_transceiver::rtp_codec::*; -use crate::rtp_transceiver::rtp_receiver::{RTCRtpReceiver, RTPReceiverInternal}; -use crate::rtp_transceiver::rtp_sender::RTCRtpSender; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::track::track_local::TrackLocal; - -pub(crate) mod fmtp; -pub mod rtp_codec; -pub mod rtp_receiver; -pub mod rtp_sender; -pub mod rtp_transceiver_direction; -pub(crate) mod srtp_writer_future; - -/// SSRC represents a synchronization source -/// A synchronization source is a randomly chosen -/// value meant to be globally unique within a particular -/// RTP session. Used to identify a single stream of media. -/// -#[allow(clippy::upper_case_acronyms)] -pub type SSRC = u32; - -/// PayloadType identifies the format of the RTP payload and determines -/// its interpretation by the application. Each codec in a RTP Session -/// will have a different PayloadType -/// -pub type PayloadType = u8; - -/// TYPE_RTCP_FBT_RANSPORT_CC .. -pub const TYPE_RTCP_FB_TRANSPORT_CC: &str = "transport-cc"; - -/// TYPE_RTCP_FB_GOOG_REMB .. -pub const TYPE_RTCP_FB_GOOG_REMB: &str = "goog-remb"; - -/// TYPE_RTCP_FB_ACK .. -pub const TYPE_RTCP_FB_ACK: &str = "ack"; - -/// TYPE_RTCP_FB_CCM .. -pub const TYPE_RTCP_FB_CCM: &str = "ccm"; - -/// TYPE_RTCP_FB_NACK .. -pub const TYPE_RTCP_FB_NACK: &str = "nack"; - -/// rtcpfeedback signals the connection to use additional RTCP packet types. -/// -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RTCPFeedback { - /// Type is the type of feedback. - /// see: - /// valid: ack, ccm, nack, goog-remb, transport-cc - pub typ: String, - - /// The parameter value depends on the type. - /// For example, type="nack" parameter="pli" will send Picture Loss Indicator packets. - pub parameter: String, -} - -/// RTPCapabilities represents the capabilities of a transceiver -/// -#[derive(Default, Debug, Clone)] -pub struct RTCRtpCapabilities { - pub codecs: Vec, - pub header_extensions: Vec, -} - -/// RTPRtxParameters dictionary contains information relating to retransmission (RTX) settings. -/// -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RTCRtpRtxParameters { - pub ssrc: SSRC, -} - -/// RTPCodingParameters provides information relating to both encoding and decoding. -/// This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself -/// -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RTCRtpCodingParameters { - pub rid: SmolStr, - pub ssrc: SSRC, - pub payload_type: PayloadType, - pub rtx: RTCRtpRtxParameters, -} - -/// RTPDecodingParameters provides information relating to both encoding and decoding. -/// This is a subset of the RFC since Pion WebRTC doesn't implement decoding itself -/// -pub type RTCRtpDecodingParameters = RTCRtpCodingParameters; - -/// RTPEncodingParameters provides information relating to both encoding and decoding. -/// This is a subset of the RFC since Pion WebRTC doesn't implement encoding itself -/// -pub type RTCRtpEncodingParameters = RTCRtpCodingParameters; - -/// RTPReceiveParameters contains the RTP stack settings used by receivers -#[derive(Debug)] -pub struct RTCRtpReceiveParameters { - pub encodings: Vec, -} - -/// RTPSendParameters contains the RTP stack settings used by receivers -#[derive(Debug)] -pub struct RTCRtpSendParameters { - pub rtp_parameters: RTCRtpParameters, - pub encodings: Vec, -} - -/// RTPTransceiverInit dictionary is used when calling the WebRTC function addTransceiver() to provide configuration options for the new transceiver. -pub struct RTCRtpTransceiverInit { - pub direction: RTCRtpTransceiverDirection, - pub send_encodings: Vec, - // Streams []*Track -} - -pub(crate) fn create_stream_info( - id: String, - ssrc: SSRC, - payload_type: PayloadType, - codec: RTCRtpCodecCapability, - webrtc_header_extensions: &[RTCRtpHeaderExtensionParameters], -) -> StreamInfo { - let header_extensions: Vec = webrtc_header_extensions - .iter() - .map(|h| RTPHeaderExtension { - id: h.id, - uri: h.uri.clone(), - }) - .collect(); - - let feedbacks: Vec<_> = codec - .rtcp_feedback - .iter() - .map(|f| interceptor::stream_info::RTCPFeedback { - typ: f.typ.clone(), - parameter: f.parameter.clone(), - }) - .collect(); - - StreamInfo { - id, - attributes: Attributes::new(), - ssrc, - payload_type, - rtp_header_extensions: header_extensions, - mime_type: codec.mime_type, - clock_rate: codec.clock_rate, - channels: codec.channels, - sdp_fmtp_line: codec.sdp_fmtp_line, - rtcp_feedback: feedbacks, - } -} - -pub type TriggerNegotiationNeededFnOption = - Option Pin + Send + Sync>> + Send + Sync>>; - -/// RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid. -pub struct RTCRtpTransceiver { - mid: OnceCell, //atomic.Value - sender: Mutex>, //atomic.Value - receiver: Mutex>, //atomic.Value - - direction: AtomicU8, //RTPTransceiverDirection - current_direction: AtomicU8, //RTPTransceiverDirection - - codecs: Arc>>, // User provided codecs via set_codec_preferences - - pub(crate) stopped: AtomicBool, - pub(crate) kind: RTPCodecType, - - media_engine: Arc, - - trigger_negotiation_needed: Mutex, -} - -impl RTCRtpTransceiver { - pub async fn new( - receiver: Arc, - sender: Arc, - direction: RTCRtpTransceiverDirection, - kind: RTPCodecType, - codecs: Vec, - media_engine: Arc, - trigger_negotiation_needed: TriggerNegotiationNeededFnOption, - ) -> Arc { - let codecs = Arc::new(Mutex::new(codecs)); - receiver.set_transceiver_codecs(Some(Arc::clone(&codecs))); - - let t = Arc::new(RTCRtpTransceiver { - mid: OnceCell::new(), - sender: Mutex::new(sender), - receiver: Mutex::new(receiver), - - direction: AtomicU8::new(direction as u8), - current_direction: AtomicU8::new(RTCRtpTransceiverDirection::Unspecified as u8), - - codecs, - stopped: AtomicBool::new(false), - kind, - media_engine, - trigger_negotiation_needed: Mutex::new(trigger_negotiation_needed), - }); - t.sender() - .await - .set_rtp_transceiver(Some(Arc::downgrade(&t))); - - t - } - - /// set_codec_preferences sets preferred list of supported codecs - /// if codecs is empty or nil we reset to default from MediaEngine - pub async fn set_codec_preferences(&self, codecs: Vec) -> Result<()> { - for codec in &codecs { - let media_engine_codecs = self.media_engine.get_codecs_by_kind(self.kind); - let (_, match_type) = codec_parameters_fuzzy_search(codec, &media_engine_codecs); - if match_type == CodecMatch::None { - return Err(Error::ErrRTPTransceiverCodecUnsupported); - } - } - - { - let mut c = self.codecs.lock().await; - *c = codecs; - } - Ok(()) - } - - /// Codecs returns list of supported codecs - pub(crate) async fn get_codecs(&self) -> Vec { - let mut codecs = self.codecs.lock().await; - RTPReceiverInternal::get_codecs(&mut codecs, self.kind, &self.media_engine) - } - - /// sender returns the RTPTransceiver's RTPSender if it has one - pub async fn sender(&self) -> Arc { - let sender = self.sender.lock().await; - sender.clone() - } - - /// set_sender_track sets the RTPSender and Track to current transceiver - pub async fn set_sender_track( - self: &Arc, - sender: Arc, - track: Option>, - ) -> Result<()> { - self.set_sender(sender).await; - self.set_sending_track(track).await - } - - pub async fn set_sender(self: &Arc, s: Arc) { - s.set_rtp_transceiver(Some(Arc::downgrade(self))); - - let prev_sender = self.sender().await; - prev_sender.set_rtp_transceiver(None); - - { - let mut sender = self.sender.lock().await; - *sender = s; - } - } - - /// receiver returns the RTPTransceiver's RTPReceiver if it has one - pub async fn receiver(&self) -> Arc { - let receiver = self.receiver.lock().await; - receiver.clone() - } - - pub(crate) async fn set_receiver(&self, r: Arc) { - r.set_transceiver_codecs(Some(Arc::clone(&self.codecs))); - - { - let mut receiver = self.receiver.lock().await; - (*receiver).set_transceiver_codecs(None); - - *receiver = r; - } - } - - /// set_mid sets the RTPTransceiver's mid. If it was already set, will return an error. - pub(crate) fn set_mid(&self, mid: SmolStr) -> Result<()> { - self.mid - .set(mid) - .map_err(|_| Error::ErrRTPTransceiverCannotChangeMid) - } - - /// mid gets the Transceiver's mid value. When not already set, this value will be set in CreateOffer or create_answer. - pub fn mid(&self) -> Option { - self.mid.get().cloned() - } - - /// kind returns RTPTransceiver's kind. - pub fn kind(&self) -> RTPCodecType { - self.kind - } - - /// direction returns the RTPTransceiver's desired direction. - pub fn direction(&self) -> RTCRtpTransceiverDirection { - self.direction.load(Ordering::SeqCst).into() - } - - /// Set the direction of this transceiver. This might trigger a renegotiation. - pub async fn set_direction(&self, d: RTCRtpTransceiverDirection) { - let changed = self.set_direction_internal(d); - - if changed { - let lock = self.trigger_negotiation_needed.lock().await; - if let Some(trigger) = &*lock { - (trigger)().await; - } - } - } - - pub(crate) fn set_direction_internal(&self, d: RTCRtpTransceiverDirection) -> bool { - let previous: RTCRtpTransceiverDirection = - self.direction.swap(d as u8, Ordering::SeqCst).into(); - - let changed = d != previous; - - if changed { - trace!( - "Changing direction of transceiver from {} to {}", - previous, - d - ); - } - - changed - } - - /// current_direction returns the RTPTransceiver's current direction as negotiated. - /// - /// If this transceiver has never been negotiated or if it's stopped this returns [`RTCRtpTransceiverDirection::Unspecified`]. - pub fn current_direction(&self) -> RTCRtpTransceiverDirection { - if self.stopped.load(Ordering::SeqCst) { - return RTCRtpTransceiverDirection::Unspecified; - } - - self.current_direction.load(Ordering::SeqCst).into() - } - - pub(crate) fn set_current_direction(&self, d: RTCRtpTransceiverDirection) { - let previous: RTCRtpTransceiverDirection = self - .current_direction - .swap(d as u8, Ordering::SeqCst) - .into(); - - if d != previous { - trace!( - "Changing current direction of transceiver from {} to {}", - previous, - d, - ); - } - } - - /// Perform any subsequent actions after altering the transceiver's direction. - /// - /// After changing the transceiver's direction this method should be called to perform any - /// side-effects that results from the new direction, such as pausing/resuming the RTP receiver. - pub(crate) async fn process_new_current_direction( - &self, - previous_direction: RTCRtpTransceiverDirection, - ) -> Result<()> { - if self.stopped.load(Ordering::SeqCst) { - return Ok(()); - } - - let current_direction = self.current_direction(); - if previous_direction != current_direction { - let mid = self.mid(); - trace!( - "Processing transceiver({:?}) direction change from {} to {}", - mid, - previous_direction, - current_direction - ); - } else { - // no change. - return Ok(()); - } - - { - let receiver = self.receiver.lock().await; - let pause_receiver = !current_direction.has_recv(); - - if pause_receiver { - receiver.pause().await?; - } else { - receiver.resume().await?; - } - } - - let pause_sender = !current_direction.has_send(); - { - let sender = &*self.sender.lock().await; - sender.set_paused(pause_sender); - } - - Ok(()) - } - - /// stop irreversibly stops the RTPTransceiver - pub async fn stop(&self) -> Result<()> { - if self.stopped.load(Ordering::SeqCst) { - return Ok(()); - } - - self.stopped.store(true, Ordering::SeqCst); - - { - let sender = self.sender.lock().await; - sender.stop().await?; - } - { - let r = self.receiver.lock().await; - r.stop().await?; - } - - self.set_direction_internal(RTCRtpTransceiverDirection::Inactive); - - Ok(()) - } - - pub(crate) async fn set_sending_track( - &self, - track: Option>, - ) -> Result<()> { - let track_is_none = track.is_none(); - { - let sender = self.sender.lock().await; - sender.replace_track(track).await?; - } - - let direction = self.direction(); - let should_send = !track_is_none; - let should_recv = direction.has_recv(); - self.set_direction_internal(RTCRtpTransceiverDirection::from_send_recv( - should_send, - should_recv, - )); - - Ok(()) - } -} - -impl fmt::Debug for RTCRtpTransceiver { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RTCRtpTransceiver") - .field("mid", &self.mid) - .field("sender", &self.sender) - .field("receiver", &self.receiver) - .field("direction", &self.direction) - .field("current_direction", &self.current_direction) - .field("codecs", &self.codecs) - .field("stopped", &self.stopped) - .field("kind", &self.kind) - .finish() - } -} - -pub(crate) async fn find_by_mid( - mid: &str, - local_transceivers: &mut Vec>, -) -> Option> { - for (i, t) in local_transceivers.iter().enumerate() { - if t.mid() == Some(SmolStr::from(mid)) { - return Some(local_transceivers.remove(i)); - } - } - - None -} - -/// Given a direction+type pluck a transceiver from the passed list -/// if no entry satisfies the requested type+direction return a inactive Transceiver -pub(crate) async fn satisfy_type_and_direction( - remote_kind: RTPCodecType, - remote_direction: RTCRtpTransceiverDirection, - local_transceivers: &mut Vec>, -) -> Option> { - // Get direction order from most preferred to least - let get_preferred_directions = || -> Vec { - match remote_direction { - RTCRtpTransceiverDirection::Sendrecv => vec![ - RTCRtpTransceiverDirection::Recvonly, - RTCRtpTransceiverDirection::Sendrecv, - ], - RTCRtpTransceiverDirection::Sendonly => vec![RTCRtpTransceiverDirection::Recvonly], - RTCRtpTransceiverDirection::Recvonly => vec![ - RTCRtpTransceiverDirection::Sendonly, - RTCRtpTransceiverDirection::Sendrecv, - ], - _ => vec![], - } - }; - - for possible_direction in get_preferred_directions() { - for (i, t) in local_transceivers.iter().enumerate() { - if t.mid().is_none() && t.kind == remote_kind && possible_direction == t.direction() { - return Some(local_transceivers.remove(i)); - } - } - } - - None -} - -/// handle_unknown_rtp_packet consumes a single RTP Packet and returns information that is helpful -/// for demuxing and handling an unknown SSRC (usually for Simulcast) -pub(crate) fn handle_unknown_rtp_packet( - buf: &[u8], - mid_extension_id: u8, - sid_extension_id: u8, - rsid_extension_id: u8, -) -> Result<(String, String, String, PayloadType)> { - let mut reader = buf; - let rp = rtp::packet::Packet::unmarshal(&mut reader)?; - - if !rp.header.extension { - return Ok((String::new(), String::new(), String::new(), 0)); - } - - let payload_type = rp.header.payload_type; - - let mid = if let Some(payload) = rp.header.get_extension(mid_extension_id) { - String::from_utf8(payload.to_vec())? - } else { - String::new() - }; - - let rid = if let Some(payload) = rp.header.get_extension(sid_extension_id) { - String::from_utf8(payload.to_vec())? - } else { - String::new() - }; - - let srid = if let Some(payload) = rp.header.get_extension(rsid_extension_id) { - String::from_utf8(payload.to_vec())? - } else { - String::new() - }; - - Ok((mid, rid, srid, payload_type)) -} diff --git a/webrtc/src/rtp_transceiver/rtp_codec.rs b/webrtc/src/rtp_transceiver/rtp_codec.rs deleted file mode 100644 index 5bf57501c..000000000 --- a/webrtc/src/rtp_transceiver/rtp_codec.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::fmt; - -use super::*; -use crate::api::media_engine::*; -use crate::error::{Error, Result}; -use crate::rtp_transceiver::fmtp; - -/// RTPCodecType determines the type of a codec -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTPCodecType { - #[default] - Unspecified = 0, - - /// RTPCodecTypeAudio indicates this is an audio codec - Audio = 1, - - /// RTPCodecTypeVideo indicates this is a video codec - Video = 2, -} - -impl From<&str> for RTPCodecType { - fn from(raw: &str) -> Self { - match raw { - "audio" => RTPCodecType::Audio, - "video" => RTPCodecType::Video, - _ => RTPCodecType::Unspecified, - } - } -} - -impl From for RTPCodecType { - fn from(v: u8) -> Self { - match v { - 1 => RTPCodecType::Audio, - 2 => RTPCodecType::Video, - _ => RTPCodecType::Unspecified, - } - } -} - -impl fmt::Display for RTPCodecType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTPCodecType::Audio => "audio", - RTPCodecType::Video => "video", - RTPCodecType::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -/// RTPCodecCapability provides information about codec capabilities. -/// -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RTCRtpCodecCapability { - pub mime_type: String, - pub clock_rate: u32, - pub channels: u16, - pub sdp_fmtp_line: String, - pub rtcp_feedback: Vec, -} - -impl RTCRtpCodecCapability { - /// Turn codec capability into a `packetizer::Payloader` - pub fn payloader_for_codec(&self) -> Result> { - let mime_type = self.mime_type.to_lowercase(); - if mime_type == MIME_TYPE_H264.to_lowercase() { - Ok(Box::::default()) - } else if mime_type == MIME_TYPE_HEVC.to_lowercase() { - Ok(Box::::default()) - } else if mime_type == MIME_TYPE_VP8.to_lowercase() { - let mut vp8_payloader = rtp::codecs::vp8::Vp8Payloader::default(); - vp8_payloader.enable_picture_id = true; - Ok(Box::new(vp8_payloader)) - } else if mime_type == MIME_TYPE_VP9.to_lowercase() { - Ok(Box::::default()) - } else if mime_type == MIME_TYPE_OPUS.to_lowercase() { - Ok(Box::::default()) - } else if mime_type == MIME_TYPE_G722.to_lowercase() - || mime_type == MIME_TYPE_PCMU.to_lowercase() - || mime_type == MIME_TYPE_PCMA.to_lowercase() - || mime_type == MIME_TYPE_TELEPHONE_EVENT.to_lowercase() - { - Ok(Box::::default()) - } else if mime_type == MIME_TYPE_AV1.to_lowercase() { - Ok(Box::::default()) - } else { - Err(Error::ErrNoPayloaderForCodec) - } - } -} - -/// RTPHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec. -/// -#[derive(Default, Debug, Clone)] -pub struct RTCRtpHeaderExtensionCapability { - pub uri: String, -} - -/// RTPHeaderExtensionParameter represents a negotiated RFC5285 RTP header extension. -/// -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RTCRtpHeaderExtensionParameters { - pub uri: String, - pub id: isize, -} - -/// RTPCodecParameters is a sequence containing the media codecs that an RtpSender -/// will choose from, as well as entries for RTX, RED and FEC mechanisms. This also -/// includes the PayloadType that has been negotiated -/// -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RTCRtpCodecParameters { - pub capability: RTCRtpCodecCapability, - pub payload_type: PayloadType, - pub stats_id: String, -} - -/// RTPParameters is a list of negotiated codecs and header extensions -/// -#[derive(Default, Debug, Clone)] -pub struct RTCRtpParameters { - pub header_extensions: Vec, - pub codecs: Vec, -} - -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub(crate) enum CodecMatch { - #[default] - None = 0, - Partial = 1, - Exact = 2, -} - -/// Do a fuzzy find for a codec in the list of codecs -/// Used for lookup up a codec in an existing list to find a match -/// Returns codecMatchExact, codecMatchPartial, or codecMatchNone -pub(crate) fn codec_parameters_fuzzy_search( - needle: &RTCRtpCodecParameters, - haystack: &[RTCRtpCodecParameters], -) -> (RTCRtpCodecParameters, CodecMatch) { - let needle_fmtp = fmtp::parse( - &needle.capability.mime_type, - &needle.capability.sdp_fmtp_line, - ); - - //TODO: add unicode case-folding equal support - - // First attempt to match on mime_type + sdpfmtp_line - for c in haystack { - let cfmpt = fmtp::parse(&c.capability.mime_type, &c.capability.sdp_fmtp_line); - if needle_fmtp.match_fmtp(&*cfmpt) { - return (c.clone(), CodecMatch::Exact); - } - } - - // Fallback to just mime_type - for c in haystack { - if c.capability.mime_type.to_uppercase() == needle.capability.mime_type.to_uppercase() { - return (c.clone(), CodecMatch::Partial); - } - } - - (RTCRtpCodecParameters::default(), CodecMatch::None) -} diff --git a/webrtc/src/rtp_transceiver/rtp_receiver/mod.rs b/webrtc/src/rtp_transceiver/rtp_receiver/mod.rs deleted file mode 100644 index e589ee575..000000000 --- a/webrtc/src/rtp_transceiver/rtp_receiver/mod.rs +++ /dev/null @@ -1,853 +0,0 @@ -#[cfg(test)] -mod rtp_receiver_test; - -use std::fmt; -use std::sync::Arc; - -use arc_swap::ArcSwapOption; -use interceptor::stream_info::RTPHeaderExtension; -use interceptor::{Attributes, Interceptor}; -use log::trace; -use smol_str::SmolStr; -use tokio::sync::{watch, Mutex, RwLock}; - -use crate::api::media_engine::MediaEngine; -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::{flatten_errs, Error, Result}; -use crate::peer_connection::sdp::TrackDetails; -use crate::rtp_transceiver::rtp_codec::{ - codec_parameters_fuzzy_search, CodecMatch, RTCRtpCodecCapability, RTCRtpCodecParameters, - RTCRtpParameters, RTPCodecType, -}; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::rtp_transceiver::{ - create_stream_info, RTCRtpDecodingParameters, RTCRtpReceiveParameters, SSRC, -}; -use crate::track::track_remote::TrackRemote; -use crate::track::{TrackStream, TrackStreams}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[repr(u8)] -pub enum State { - /// We haven't started yet. - Unstarted = 0, - /// We haven't started yet and additionally we've been paused. - UnstartedPaused = 1, - - /// We have started and are running. - Started = 2, - - /// We have been paused after starting. - Paused = 3, - - /// We have been stopped. - Stopped = 4, -} - -impl From for State { - fn from(value: u8) -> Self { - match value { - v if v == State::Unstarted as u8 => State::Unstarted, - v if v == State::UnstartedPaused as u8 => State::UnstartedPaused, - v if v == State::Started as u8 => State::Started, - v if v == State::Paused as u8 => State::Paused, - v if v == State::Stopped as u8 => State::Stopped, - _ => unreachable!( - "Invalid serialization of {}: {}", - std::any::type_name::(), - value - ), - } - } -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - State::Unstarted => write!(f, "Unstarted"), - State::UnstartedPaused => write!(f, "UnstartedPaused"), - State::Started => write!(f, "Running"), - State::Paused => write!(f, "Paused"), - State::Stopped => write!(f, "Closed"), - } - } -} - -impl State { - fn transition(to: Self, tx: &watch::Sender) -> Result<()> { - let current = *tx.borrow(); - if current == to { - // Already in this state - return Ok(()); - } - - match current { - Self::Unstarted - if matches!(to, Self::Started | Self::Stopped | Self::UnstartedPaused) => - { - let _ = tx.send(to); - return Ok(()); - } - Self::UnstartedPaused - if matches!(to, Self::Unstarted | Self::Stopped | Self::Paused) => - { - let _ = tx.send(to); - return Ok(()); - } - State::Started if matches!(to, Self::Paused | Self::Stopped) => { - let _ = tx.send(to); - return Ok(()); - } - State::Paused if matches!(to, Self::Started | Self::Stopped) => { - let _ = tx.send(to); - return Ok(()); - } - _ => {} - } - - Err(Error::ErrRTPReceiverStateChangeInvalid { from: current, to }) - } - - async fn wait_for(rx: &mut watch::Receiver, states: &[State]) -> Result<()> { - loop { - let state = *rx.borrow(); - - match state { - _ if states.contains(&state) => return Ok(()), - State::Stopped => { - return Err(Error::ErrClosedPipe); - } - _ => {} - } - - if rx.changed().await.is_err() { - return Err(Error::ErrClosedPipe); - } - } - } - - async fn error_on_close(rx: &mut watch::Receiver) -> Result<()> { - if rx.changed().await.is_err() { - return Err(Error::ErrClosedPipe); - } - - let state = *rx.borrow(); - if state == State::Stopped { - return Err(Error::ErrClosedPipe); - } - - Ok(()) - } - - fn is_started(&self) -> bool { - matches!(self, Self::Started | Self::Paused) - } -} - -pub struct RTPReceiverInternal { - pub(crate) kind: RTPCodecType, - - // State is stored within the channel - state_tx: watch::Sender, - state_rx: watch::Receiver, - - tracks: RwLock>, - - transceiver_codecs: ArcSwapOption>>, - - transport: Arc, - media_engine: Arc, - interceptor: Arc, -} - -impl RTPReceiverInternal { - /// read reads incoming RTCP for this RTPReceiver - async fn read( - &self, - b: &mut [u8], - ) -> Result<(Vec>, Attributes)> { - let mut state_watch_rx = self.state_tx.subscribe(); - // Ensure we are running or paused. When paused we still receive RTCP even if RTP traffic - // isn't flowing. - State::wait_for(&mut state_watch_rx, &[State::Started, State::Paused]).await?; - - let tracks = self.tracks.read().await; - if let Some(t) = tracks.first() { - if let Some(rtcp_interceptor) = &t.stream.rtcp_interceptor { - let a = Attributes::new(); - loop { - tokio::select! { - res = State::error_on_close(&mut state_watch_rx) => { - res? - } - result = rtcp_interceptor.read(b, &a) => { - return Ok(result?) - } - } - } - } else { - Err(Error::ErrInterceptorNotBind) - } - } else { - Err(Error::ErrExistingTrack) - } - } - - /// read_simulcast reads incoming RTCP for this RTPReceiver for given rid - async fn read_simulcast( - &self, - b: &mut [u8], - rid: &str, - ) -> Result<(Vec>, Attributes)> { - let mut state_watch_rx = self.state_tx.subscribe(); - - // Ensure we are running or paused. When paused we still receive RTCP even if RTP traffic - // isn't flowing. - State::wait_for(&mut state_watch_rx, &[State::Started, State::Paused]).await?; - - let tracks = self.tracks.read().await; - for t in &*tracks { - if t.track.rid() == rid { - if let Some(rtcp_interceptor) = &t.stream.rtcp_interceptor { - let a = Attributes::new(); - - loop { - tokio::select! { - res = State::error_on_close(&mut state_watch_rx) => { - res? - } - result = rtcp_interceptor.read(b, &a) => { - return Ok(result?); - } - } - } - } else { - return Err(Error::ErrInterceptorNotBind); - } - } - } - Err(Error::ErrRTPReceiverForRIDTrackStreamNotFound) - } - - /// read_rtcp is a convenience method that wraps Read and unmarshal for you. - /// It also runs any configured interceptors. - async fn read_rtcp( - &self, - receive_mtu: usize, - ) -> Result<(Vec>, Attributes)> { - let mut b = vec![0u8; receive_mtu]; - let (pkts, attributes) = self.read(&mut b).await?; - - Ok((pkts, attributes)) - } - - /// read_simulcast_rtcp is a convenience method that wraps ReadSimulcast and unmarshal for you - async fn read_simulcast_rtcp( - &self, - rid: &str, - receive_mtu: usize, - ) -> Result<(Vec>, Attributes)> { - let mut b = vec![0u8; receive_mtu]; - let (pkts, attributes) = self.read_simulcast(&mut b, rid).await?; - - Ok((pkts, attributes)) - } - - pub(crate) async fn read_rtp( - &self, - b: &mut [u8], - tid: usize, - ) -> Result<(rtp::packet::Packet, Attributes)> { - let mut state_watch_rx = self.state_tx.subscribe(); - - // Ensure we are running. - State::wait_for(&mut state_watch_rx, &[State::Started]).await?; - - //log::debug!("read_rtp enter tracks tid {}", tid); - let mut rtp_interceptor = None; - //let mut ssrc = 0; - { - let tracks = self.tracks.read().await; - for t in &*tracks { - if t.track.tid() == tid { - rtp_interceptor.clone_from(&t.stream.rtp_interceptor); - //ssrc = t.track.ssrc(); - break; - } - } - }; - /*log::debug!( - "read_rtp exit tracks with rtp_interceptor {} with tid {}", - rtp_interceptor.is_some(), - tid, - );*/ - - if let Some(rtp_interceptor) = rtp_interceptor { - let a = Attributes::new(); - //println!( - // "read_rtp rtp_interceptor.read enter with tid {} ssrc {}", - // tid, ssrc - //); - let mut current_state = *state_watch_rx.borrow(); - loop { - tokio::select! { - _ = state_watch_rx.changed() => { - let new_state = *state_watch_rx.borrow(); - - if new_state == State::Stopped { - return Err(Error::ErrClosedPipe); - } - current_state = new_state; - } - result = rtp_interceptor.read(b, &a) => { - let result = result?; - - if current_state == State::Paused { - trace!("Dropping {} read bytes received while RTPReceiver was paused", result.0); - continue; - } - return Ok(result); - } - } - } - } else { - //log::debug!("read_rtp exit tracks with ErrRTPReceiverWithSSRCTrackStreamNotFound"); - Err(Error::ErrRTPReceiverWithSSRCTrackStreamNotFound) - } - } - - async fn get_parameters(&self) -> RTCRtpParameters { - let mut parameters = self - .media_engine - .get_rtp_parameters_by_kind(self.kind, RTCRtpTransceiverDirection::Recvonly); - - let transceiver_codecs = self.transceiver_codecs.load(); - if let Some(codecs) = &*transceiver_codecs { - let mut c = codecs.lock().await; - parameters.codecs = - RTPReceiverInternal::get_codecs(&mut c, self.kind, &self.media_engine); - } - - parameters - } - - pub(crate) fn get_codecs( - codecs: &mut [RTCRtpCodecParameters], - kind: RTPCodecType, - media_engine: &Arc, - ) -> Vec { - let media_engine_codecs = media_engine.get_codecs_by_kind(kind); - if codecs.is_empty() { - return media_engine_codecs; - } - let mut filtered_codecs = vec![]; - for codec in codecs { - let (c, match_type) = codec_parameters_fuzzy_search(codec, &media_engine_codecs); - if match_type != CodecMatch::None { - if codec.payload_type == 0 { - codec.payload_type = c.payload_type; - } - filtered_codecs.push(codec.clone()); - } - } - - filtered_codecs - } - - // State - - /// Get the current state and a receiver for the next state change. - pub(crate) fn current_state(&self) -> State { - *self.state_rx.borrow() - } - - pub(crate) fn start(&self) -> Result<()> { - State::transition(State::Started, &self.state_tx) - } - - pub(crate) fn pause(&self) -> Result<()> { - let current = self.current_state(); - - match current { - State::Unstarted => State::transition(State::UnstartedPaused, &self.state_tx), - State::Started => State::transition(State::Paused, &self.state_tx), - _ => Ok(()), - } - } - - pub(crate) fn resume(&self) -> Result<()> { - let current = self.current_state(); - - match current { - State::UnstartedPaused => State::transition(State::Unstarted, &self.state_tx), - State::Paused => State::transition(State::Started, &self.state_tx), - _ => Ok(()), - } - } - - pub(crate) fn close(&self) -> Result<()> { - State::transition(State::Stopped, &self.state_tx) - } -} - -/// RTPReceiver allows an application to inspect the receipt of a TrackRemote -pub struct RTCRtpReceiver { - receive_mtu: usize, - kind: RTPCodecType, - transport: Arc, - - pub internal: Arc, -} - -impl std::fmt::Debug for RTCRtpReceiver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RTCRtpReceiver") - .field("kind", &self.kind) - .finish() - } -} - -impl RTCRtpReceiver { - pub fn new( - receive_mtu: usize, - kind: RTPCodecType, - transport: Arc, - media_engine: Arc, - interceptor: Arc, - ) -> Self { - let (state_tx, state_rx) = watch::channel(State::Unstarted); - - RTCRtpReceiver { - receive_mtu, - kind, - transport: Arc::clone(&transport), - - internal: Arc::new(RTPReceiverInternal { - kind, - - tracks: RwLock::new(vec![]), - transport, - media_engine, - interceptor, - - state_tx, - state_rx, - - transceiver_codecs: ArcSwapOption::new(None), - }), - } - } - - pub fn kind(&self) -> RTPCodecType { - self.kind - } - - pub(crate) fn set_transceiver_codecs( - &self, - codecs: Option>>>, - ) { - self.internal.transceiver_codecs.store(codecs); - } - - /// transport returns the currently-configured *DTLSTransport or nil - /// if one has not yet been configured - pub fn transport(&self) -> Arc { - Arc::clone(&self.transport) - } - - /// get_parameters describes the current configuration for the encoding and - /// transmission of media on the receiver's track. - pub async fn get_parameters(&self) -> RTCRtpParameters { - self.internal.get_parameters().await - } - - /// SetRTPParameters applies provided RTPParameters the RTPReceiver's tracks. - /// This method is part of the ORTC API. It is not - /// meant to be used together with the basic WebRTC API. - /// The amount of provided codecs must match the number of tracks on the receiver. - pub async fn set_rtp_parameters(&self, params: RTCRtpParameters) { - let mut header_extensions = vec![]; - for h in ¶ms.header_extensions { - header_extensions.push(RTPHeaderExtension { - id: h.id, - uri: h.uri.clone(), - }); - } - - let mut tracks = self.internal.tracks.write().await; - for (idx, codec) in params.codecs.iter().enumerate() { - let t = &mut tracks[idx]; - if let Some(stream_info) = &mut t.stream.stream_info { - stream_info - .rtp_header_extensions - .clone_from(&header_extensions); - } - - let current_track = &t.track; - current_track.set_codec(codec.clone()); - current_track.set_params(params.clone()); - } - } - - /// tracks returns the RtpTransceiver traclockks - /// A RTPReceiver to support Simulcast may now have multiple tracks - pub async fn tracks(&self) -> Vec> { - let tracks = self.internal.tracks.read().await; - tracks.iter().map(|t| Arc::clone(&t.track)).collect() - } - - /// receive initialize the track and starts all the transports - pub async fn receive(&self, parameters: &RTCRtpReceiveParameters) -> Result<()> { - let receiver = Arc::downgrade(&self.internal); - - let current_state = self.internal.current_state(); - if current_state.is_started() { - return Err(Error::ErrRTPReceiverReceiveAlreadyCalled); - } - self.internal.start()?; - - let (global_params, interceptor, media_engine) = { - ( - self.internal.get_parameters().await, - Arc::clone(&self.internal.interceptor), - Arc::clone(&self.internal.media_engine), - ) - }; - - let codec = if let Some(codec) = global_params.codecs.first() { - codec.capability.clone() - } else { - RTCRtpCodecCapability::default() - }; - - for encoding in ¶meters.encodings { - let (stream_info, rtp_read_stream, rtp_interceptor, rtcp_read_stream, rtcp_interceptor) = - if encoding.ssrc != 0 { - let stream_info = create_stream_info( - "".to_owned(), - encoding.ssrc, - 0, - codec.clone(), - &global_params.header_extensions, - ); - let (rtp_read_stream, rtp_interceptor, rtcp_read_stream, rtcp_interceptor) = - self.transport - .streams_for_ssrc(encoding.ssrc, &stream_info, &interceptor) - .await?; - - ( - Some(stream_info), - Some(rtp_read_stream), - Some(rtp_interceptor), - Some(rtcp_read_stream), - Some(rtcp_interceptor), - ) - } else { - (None, None, None, None, None) - }; - - let t = TrackStreams { - track: Arc::new(TrackRemote::new( - self.receive_mtu, - self.kind, - encoding.ssrc, - encoding.rid.clone(), - receiver.clone(), - Arc::clone(&media_engine), - Arc::clone(&interceptor), - )), - stream: TrackStream { - stream_info, - rtp_read_stream, - rtp_interceptor, - rtcp_read_stream, - rtcp_interceptor, - }, - - repair_stream: TrackStream { - stream_info: None, - rtp_read_stream: None, - rtp_interceptor: None, - rtcp_read_stream: None, - rtcp_interceptor: None, - }, - }; - - { - let mut tracks = self.internal.tracks.write().await; - tracks.push(t); - }; - - let rtx_ssrc = encoding.rtx.ssrc; - if rtx_ssrc != 0 { - let stream_info = create_stream_info( - "".to_owned(), - rtx_ssrc, - 0, - codec.clone(), - &global_params.header_extensions, - ); - let (rtp_read_stream, rtp_interceptor, rtcp_read_stream, rtcp_interceptor) = self - .transport - .streams_for_ssrc(rtx_ssrc, &stream_info, &interceptor) - .await?; - - self.receive_for_rtx( - rtx_ssrc, - "".to_owned(), - TrackStream { - stream_info: Some(stream_info), - rtp_read_stream: Some(rtp_read_stream), - rtp_interceptor: Some(rtp_interceptor), - rtcp_read_stream: Some(rtcp_read_stream), - rtcp_interceptor: Some(rtcp_interceptor), - }, - ) - .await?; - } - } - - Ok(()) - } - - /// read reads incoming RTCP for this RTPReceiver - pub async fn read( - &self, - b: &mut [u8], - ) -> Result<(Vec>, Attributes)> { - self.internal.read(b).await - } - - /// read_simulcast reads incoming RTCP for this RTPReceiver for given rid - pub async fn read_simulcast( - &self, - b: &mut [u8], - rid: &str, - ) -> Result<(Vec>, Attributes)> { - self.internal.read_simulcast(b, rid).await - } - - /// read_rtcp is a convenience method that wraps Read and unmarshal for you. - /// It also runs any configured interceptors. - pub async fn read_rtcp( - &self, - ) -> Result<(Vec>, Attributes)> { - self.internal.read_rtcp(self.receive_mtu).await - } - - /// read_simulcast_rtcp is a convenience method that wraps ReadSimulcast and unmarshal for you - pub async fn read_simulcast_rtcp( - &self, - rid: &str, - ) -> Result<(Vec>, Attributes)> { - self.internal - .read_simulcast_rtcp(rid, self.receive_mtu) - .await - } - - pub(crate) async fn have_received(&self) -> bool { - self.internal.current_state().is_started() - } - - pub(crate) async fn start(&self, incoming: &TrackDetails) { - let mut encoding_size = incoming.ssrcs.len(); - if incoming.rids.len() >= encoding_size { - encoding_size = incoming.rids.len(); - }; - - let mut encodings = vec![RTCRtpDecodingParameters::default(); encoding_size]; - for (i, encoding) in encodings.iter_mut().enumerate() { - if incoming.rids.len() > i { - encoding.rid = incoming.rids[i].clone(); - } - if incoming.ssrcs.len() > i { - encoding.ssrc = incoming.ssrcs[i]; - } - - encoding.rtx.ssrc = incoming.repair_ssrc; - } - - if let Err(err) = self.receive(&RTCRtpReceiveParameters { encodings }).await { - log::warn!("RTPReceiver Receive failed {}", err); - return; - } - - // set track id and label early so they can be set as new track information - // is received from the SDP. - let is_unpaused = self.current_state() == State::Started; - for track_remote in &self.tracks().await { - track_remote.set_id(incoming.id.clone()); - track_remote.set_stream_id(incoming.stream_id.clone()); - - if is_unpaused { - track_remote.fire_onunmute().await; - } - } - } - - /// Stop irreversibly stops the RTPReceiver - pub async fn stop(&self) -> Result<()> { - let previous_state = self.internal.current_state(); - self.internal.close()?; - - let mut errs = vec![]; - let was_ever_started = previous_state.is_started(); - if was_ever_started { - let tracks = self.internal.tracks.write().await; - for t in &*tracks { - if let Some(rtcp_read_stream) = &t.stream.rtcp_read_stream { - if let Err(err) = rtcp_read_stream.close().await { - errs.push(err); - } - } - - if let Some(rtp_read_stream) = &t.stream.rtp_read_stream { - if let Err(err) = rtp_read_stream.close().await { - errs.push(err); - } - } - - if let Some(repair_rtcp_read_stream) = &t.repair_stream.rtcp_read_stream { - if let Err(err) = repair_rtcp_read_stream.close().await { - errs.push(err); - } - } - - if let Some(repair_rtp_read_stream) = &t.repair_stream.rtp_read_stream { - if let Err(err) = repair_rtp_read_stream.close().await { - errs.push(err); - } - } - - if let Some(stream_info) = &t.stream.stream_info { - self.internal - .interceptor - .unbind_remote_stream(stream_info) - .await; - } - - if let Some(repair_stream_info) = &t.repair_stream.stream_info { - self.internal - .interceptor - .unbind_remote_stream(repair_stream_info) - .await; - } - } - } - - flatten_errs(errs) - } - - /// read_rtp should only be called by a track, this only exists so we can keep state in one place - pub(crate) async fn read_rtp( - &self, - b: &mut [u8], - tid: usize, - ) -> Result<(rtp::packet::Packet, Attributes)> { - self.internal.read_rtp(b, tid).await - } - - /// receive_for_rid is the sibling of Receive expect for RIDs instead of SSRCs - /// It populates all the internal state for the given RID - pub(crate) async fn receive_for_rid( - &self, - rid: SmolStr, - params: RTCRtpParameters, - stream: TrackStream, - ) -> Result> { - let mut tracks = self.internal.tracks.write().await; - for t in &mut *tracks { - if *t.track.rid() == rid { - t.track.set_kind(self.kind); - if let Some(codec) = params.codecs.first() { - t.track.set_codec(codec.clone()); - } - t.track.set_params(params.clone()); - t.track - .set_ssrc(stream.stream_info.as_ref().map_or(0, |s| s.ssrc)); - t.stream = stream; - return Ok(Arc::clone(&t.track)); - } - } - - Err(Error::ErrRTPReceiverForRIDTrackStreamNotFound) - } - - /// receiveForRtx starts a routine that processes the repair stream - /// These packets aren't exposed to the user yet, but we need to process them for - /// TWCC - pub(crate) async fn receive_for_rtx( - &self, - ssrc: SSRC, - rsid: String, - repair_stream: TrackStream, - ) -> Result<()> { - let mut tracks = self.internal.tracks.write().await; - let l = tracks.len(); - for t in &mut *tracks { - if (ssrc != 0 && l == 1) || t.track.rid() == rsid { - t.repair_stream = repair_stream; - - let receive_mtu = self.receive_mtu; - let track = t.clone(); - tokio::spawn(async move { - let a = Attributes::new(); - let mut b = vec![0u8; receive_mtu]; - while let Some(repair_rtp_interceptor) = &track.repair_stream.rtp_interceptor { - //TODO: cancel repair_rtp_interceptor.read gracefully - //println!("repair_rtp_interceptor read begin with ssrc={}", ssrc); - if repair_rtp_interceptor.read(&mut b, &a).await.is_err() { - break; - } - } - }); - - return Ok(()); - } - } - - Err(Error::ErrRTPReceiverForRIDTrackStreamNotFound) - } - - // State - - pub(crate) fn current_state(&self) -> State { - self.internal.current_state() - } - - pub(crate) async fn pause(&self) -> Result<()> { - self.internal.pause()?; - - if !self.internal.current_state().is_started() { - return Ok(()); - } - - let streams = self.internal.tracks.read().await; - - for stream in streams.iter() { - // TODO: If we introduce futures as a direct dependency this and other futures could be - // ran concurrently with [`join_all`](https://docs.rs/futures/0.3.21/futures/future/fn.join_all.html) - stream.track.fire_onmute().await; - } - - Ok(()) - } - - pub(crate) async fn resume(&self) -> Result<()> { - self.internal.resume()?; - - if !self.internal.current_state().is_started() { - return Ok(()); - } - - let streams = self.internal.tracks.read().await; - - for stream in streams.iter() { - // TODO: If we introduce futures as a direct dependency this and other futures could be - // ran concurrently with [`join_all`](https://docs.rs/futures/0.3.21/futures/future/fn.join_all.html) - stream.track.fire_onunmute().await; - } - - Ok(()) - } -} diff --git a/webrtc/src/rtp_transceiver/rtp_receiver/rtp_receiver_test.rs b/webrtc/src/rtp_transceiver/rtp_receiver/rtp_receiver_test.rs deleted file mode 100644 index 7520667db..000000000 --- a/webrtc/src/rtp_transceiver/rtp_receiver/rtp_receiver_test.rs +++ /dev/null @@ -1,233 +0,0 @@ -use bytes::Bytes; -use media::Sample; -use tokio::sync::mpsc; -use tokio::time::Duration; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::media_engine::{MIME_TYPE_OPUS, MIME_TYPE_VP8}; -use crate::error::Result; -use crate::peer_connection::peer_connection_state::RTCPeerConnectionState; -use crate::peer_connection::peer_connection_test::{ - close_pair_now, create_vnet_pair, signal_pair, until_connection_state, -}; -use crate::rtp_transceiver::rtp_codec::RTCRtpHeaderExtensionParameters; -use crate::rtp_transceiver::RTCPFeedback; -use crate::track::track_local::track_local_static_sample::TrackLocalStaticSample; -use crate::track::track_local::TrackLocal; - -lazy_static! { - static ref P: RTCRtpParameters = RTCRtpParameters { - codecs: vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_string(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "minptime=10;useinbandfec=1".to_string(), - rtcp_feedback: vec![RTCPFeedback { - typ: "nack".to_owned(), - parameter: "".to_owned(), - }], - }, - payload_type: 111, - ..Default::default() - }], - header_extensions: vec![ - RTCRtpHeaderExtensionParameters { - uri: "urn:ietf:params:rtp-hdrext:sdes:mid".to_owned(), - ..Default::default() - }, - RTCRtpHeaderExtensionParameters { - uri: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id".to_owned(), - ..Default::default() - }, - RTCRtpHeaderExtensionParameters { - uri: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id".to_owned(), - ..Default::default() - }, - ], - }; -} - -//use log::LevelFilter; -//use std::io::Write; - -#[tokio::test] -async fn test_set_rtp_parameters() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let (mut sender, mut receiver, wan) = create_vnet_pair().await?; - - let outgoing_track: Arc = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - sender.add_track(Arc::clone(&outgoing_track)).await?; - - // Those parameters wouldn't make sense in a real application, - // but for the sake of the test we just need different values. - - let (seen_packet_tx, mut seen_packet_rx) = mpsc::channel::<()>(1); - let seen_packet_tx = Arc::new(Mutex::new(Some(seen_packet_tx))); - receiver.on_track(Box::new(move |_, receiver, _| { - let seen_packet_tx2 = Arc::clone(&seen_packet_tx); - Box::pin(async move { - receiver.set_rtp_parameters(P.clone()).await; - - let tracks = receiver.tracks().await; - assert_eq!(tracks.len(), 1); - let t = tracks.first().unwrap(); - - let incoming_track_codecs = t.codec(); - - assert_eq!(P.header_extensions, t.params().header_extensions); - assert_eq!( - P.codecs[0].capability.mime_type, - incoming_track_codecs.capability.mime_type - ); - assert_eq!( - P.codecs[0].capability.clock_rate, - incoming_track_codecs.capability.clock_rate - ); - assert_eq!( - P.codecs[0].capability.channels, - incoming_track_codecs.capability.channels - ); - assert_eq!( - P.codecs[0].capability.sdp_fmtp_line, - incoming_track_codecs.capability.sdp_fmtp_line - ); - assert_eq!( - P.codecs[0].capability.rtcp_feedback, - incoming_track_codecs.capability.rtcp_feedback - ); - assert_eq!(P.codecs[0].payload_type, incoming_track_codecs.payload_type); - - { - let mut done = seen_packet_tx2.lock().await; - done.take(); - } - }) - })); - - let wg = WaitGroup::new(); - - until_connection_state(&mut sender, &wg, RTCPeerConnectionState::Connected).await; - until_connection_state(&mut receiver, &wg, RTCPeerConnectionState::Connected).await; - - signal_pair(&mut sender, &mut receiver).await?; - - wg.wait().await; - - if let Some(v) = outgoing_track - .as_any() - .downcast_ref::() - { - v.write_sample(&Sample { - data: Bytes::from_static(&[0xAA]), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - } else { - panic!(); - } - - let _ = seen_packet_rx.recv().await; - { - let mut w = wan.lock().await; - w.stop().await?; - } - close_pair_now(&sender, &receiver).await; - - Ok(()) -} - -// Assert that SetReadDeadline works as expected -// This test uses VNet since we must have zero loss -#[tokio::test] -async fn test_rtp_receiver_set_read_deadline() -> Result<()> { - let (mut sender, mut receiver, wan) = create_vnet_pair().await?; - - let track: Arc = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - sender.add_track(Arc::clone(&track)).await?; - - let (seen_packet_tx, mut seen_packet_rx) = mpsc::channel::<()>(1); - let seen_packet_tx = Arc::new(Mutex::new(Some(seen_packet_tx))); - receiver.on_track(Box::new(move |track, receiver, _| { - let seen_packet_tx2 = Arc::clone(&seen_packet_tx); - Box::pin(async move { - // First call will not error because we cache for probing - let result = tokio::time::timeout(Duration::from_secs(1), track.read_rtp()).await; - assert!( - result.is_ok(), - " First call will not error because we cache for probing" - ); - - let result = tokio::time::timeout(Duration::from_secs(1), track.read_rtp()).await; - assert!(result.is_err()); - - let result = tokio::time::timeout(Duration::from_secs(1), receiver.read_rtcp()).await; - assert!(result.is_err()); - - { - let mut done = seen_packet_tx2.lock().await; - done.take(); - } - }) - })); - - let wg = WaitGroup::new(); - until_connection_state(&mut sender, &wg, RTCPeerConnectionState::Connected).await; - until_connection_state(&mut receiver, &wg, RTCPeerConnectionState::Connected).await; - - signal_pair(&mut sender, &mut receiver).await?; - - wg.wait().await; - - if let Some(v) = track.as_any().downcast_ref::() { - v.write_sample(&Sample { - data: Bytes::from_static(&[0xAA]), - duration: Duration::from_secs(1), - ..Default::default() - }) - .await?; - } else { - panic!(); - } - - let _ = seen_packet_rx.recv().await; - { - let mut w = wan.lock().await; - w.stop().await?; - } - close_pair_now(&sender, &receiver).await; - - Ok(()) -} diff --git a/webrtc/src/rtp_transceiver/rtp_sender/mod.rs b/webrtc/src/rtp_transceiver/rtp_sender/mod.rs deleted file mode 100644 index 9ae813e06..000000000 --- a/webrtc/src/rtp_transceiver/rtp_sender/mod.rs +++ /dev/null @@ -1,593 +0,0 @@ -#[cfg(test)] -mod rtp_sender_test; - -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; - -use ice::rand::generate_crypto_random_string; -use interceptor::stream_info::StreamInfo; -use interceptor::{Attributes, Interceptor, RTCPReader, RTPWriter}; -use portable_atomic::AtomicBool; -use tokio::sync::{mpsc, Mutex, Notify}; -use util::sync::Mutex as SyncMutex; - -use super::srtp_writer_future::SequenceTransformer; -use crate::api::media_engine::MediaEngine; -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::{Error, Result}; -use crate::rtp_transceiver::rtp_codec::RTPCodecType; -use crate::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; -use crate::rtp_transceiver::srtp_writer_future::SrtpWriterFuture; -use crate::rtp_transceiver::{ - create_stream_info, PayloadType, RTCRtpEncodingParameters, RTCRtpSendParameters, - RTCRtpTransceiver, SSRC, -}; -use crate::track::track_local::{ - InterceptorToTrackLocalWriter, TrackLocal, TrackLocalContext, TrackLocalWriter, -}; - -pub(crate) struct RTPSenderInternal { - pub(crate) send_called_rx: Mutex>, - pub(crate) stop_called_rx: Arc, - pub(crate) stop_called_signal: Arc, -} - -pub(crate) struct TrackEncoding { - pub(crate) track: Arc, - pub(crate) srtp_stream: Arc, - pub(crate) rtcp_interceptor: Arc, - pub(crate) stream_info: Mutex, - pub(crate) context: Mutex, - - pub(crate) ssrc: SSRC, -} - -/// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer -pub struct RTCRtpSender { - pub(crate) track_encodings: Mutex>, - - seq_trans: Arc, - - pub(crate) transport: Arc, - - pub(crate) kind: RTPCodecType, - pub(crate) payload_type: PayloadType, - receive_mtu: usize, - - /// a transceiver sender since we can just check the - /// transceiver negotiation status - pub(crate) negotiated: AtomicBool, - - pub(crate) media_engine: Arc, - pub(crate) interceptor: Arc, - - pub(crate) id: String, - - /// The id of the initial track, even if we later change to a different - /// track id should be use when negotiating. - pub(crate) initial_track_id: std::sync::Mutex>, - /// AssociatedMediaStreamIds from the WebRTC specifications - pub(crate) associated_media_stream_ids: std::sync::Mutex>, - - rtp_transceiver: SyncMutex>>, - - send_called_tx: SyncMutex>>, - stop_called_tx: Arc, - stop_called_signal: Arc, - - pub(crate) paused: Arc, - - internal: Arc, -} - -impl std::fmt::Debug for RTCRtpSender { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RTCRtpSender") - .field("id", &self.id) - .finish() - } -} - -impl RTCRtpSender { - pub async fn new( - receive_mtu: usize, - track: Option>, - kind: RTPCodecType, - transport: Arc, - media_engine: Arc, - interceptor: Arc, - start_paused: bool, - ) -> Self { - let id = generate_crypto_random_string( - 32, - b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", - ); - let (send_called_tx, send_called_rx) = mpsc::channel(1); - let stop_called_tx = Arc::new(Notify::new()); - let stop_called_rx = stop_called_tx.clone(); - let stop_called_signal = Arc::new(AtomicBool::new(false)); - - let internal = Arc::new(RTPSenderInternal { - send_called_rx: Mutex::new(send_called_rx), - stop_called_rx, - stop_called_signal: Arc::clone(&stop_called_signal), - }); - - let seq_trans = Arc::new(SequenceTransformer::new()); - - let stream_ids = track - .as_ref() - .map(|track| vec![track.stream_id().to_string()]) - .unwrap_or_default(); - let ret = Self { - track_encodings: Mutex::new(vec![]), - - seq_trans, - - transport, - - kind, - payload_type: 0, - receive_mtu, - - negotiated: AtomicBool::new(false), - - media_engine, - interceptor, - - id, - initial_track_id: std::sync::Mutex::new(None), - associated_media_stream_ids: std::sync::Mutex::new(stream_ids), - - rtp_transceiver: SyncMutex::new(None), - - send_called_tx: SyncMutex::new(Some(send_called_tx)), - stop_called_tx, - stop_called_signal, - - paused: Arc::new(AtomicBool::new(start_paused)), - - internal, - }; - - if let Some(track) = track { - let mut track_encodings = ret.track_encodings.lock().await; - let _ = ret.add_encoding_internal(&mut track_encodings, track).await; - } - - ret - } - - /// AddEncoding adds an encoding to RTPSender. Used by simulcast senders. - pub async fn add_encoding(&self, track: Arc) -> Result<()> { - let mut track_encodings = self.track_encodings.lock().await; - - if track.rid().is_none() { - return Err(Error::ErrRTPSenderRidNil); - } - - if self.has_stopped().await { - return Err(Error::ErrRTPSenderStopped); - } - - if self.has_sent() { - return Err(Error::ErrRTPSenderSendAlreadyCalled); - } - - let base_track = track_encodings - .first() - .map(|e| &e.track) - .ok_or(Error::ErrRTPSenderNoBaseEncoding)?; - if base_track.rid().is_none() { - return Err(Error::ErrRTPSenderNoBaseEncoding); - } - - if base_track.id() != track.id() - || base_track.stream_id() != track.stream_id() - || base_track.kind() != track.kind() - { - return Err(Error::ErrRTPSenderBaseEncodingMismatch); - } - - if track_encodings.iter().any(|e| e.track.rid() == track.rid()) { - return Err(Error::ErrRTPSenderRIDCollision); - } - - self.add_encoding_internal(&mut track_encodings, track) - .await - } - - async fn add_encoding_internal( - &self, - track_encodings: &mut Vec, - track: Arc, - ) -> Result<()> { - let ssrc = rand::random::(); - let srtp_stream = Arc::new(SrtpWriterFuture { - closed: AtomicBool::new(false), - ssrc, - rtp_sender: Arc::downgrade(&self.internal), - rtp_transport: Arc::clone(&self.transport), - rtcp_read_stream: Mutex::new(None), - rtp_write_session: Mutex::new(None), - seq_trans: Arc::clone(&self.seq_trans), - }); - - let srtp_rtcp_reader = Arc::clone(&srtp_stream) as Arc; - let rtcp_interceptor = self.interceptor.bind_rtcp_reader(srtp_rtcp_reader).await; - - let encoding = TrackEncoding { - track, - srtp_stream, - rtcp_interceptor, - stream_info: Mutex::new(StreamInfo::default()), - context: Mutex::new(TrackLocalContext::default()), - ssrc, - }; - - track_encodings.push(encoding); - - Ok(()) - } - - pub(crate) fn is_negotiated(&self) -> bool { - self.negotiated.load(Ordering::SeqCst) - } - - pub(crate) fn set_negotiated(&self) { - self.negotiated.store(true, Ordering::SeqCst); - } - - pub(crate) fn set_rtp_transceiver(&self, rtp_transceiver: Option>) { - if let Some(t) = rtp_transceiver.as_ref().and_then(|t| t.upgrade()) { - self.set_paused(!t.direction().has_send()); - } - let mut tr = self.rtp_transceiver.lock(); - *tr = rtp_transceiver; - } - - pub(crate) fn set_paused(&self, paused: bool) { - self.paused.store(paused, Ordering::SeqCst); - } - - /// transport returns the currently-configured DTLSTransport - /// if one has not yet been configured - pub fn transport(&self) -> Arc { - Arc::clone(&self.transport) - } - - /// get_parameters describes the current configuration for the encoding and - /// transmission of media on the sender's track. - pub async fn get_parameters(&self) -> RTCRtpSendParameters { - let encodings = { - let track_encodings = self.track_encodings.lock().await; - let mut encodings = Vec::with_capacity(track_encodings.len()); - for e in track_encodings.iter() { - encodings.push(RTCRtpEncodingParameters { - rid: e.track.rid().unwrap_or_default().into(), - ssrc: e.ssrc, - payload_type: self.payload_type, - ..Default::default() - }); - } - - encodings - }; - - let mut rtp_parameters = self - .media_engine - .get_rtp_parameters_by_kind(self.kind, RTCRtpTransceiverDirection::Sendonly); - rtp_parameters.codecs = { - let tr = self - .rtp_transceiver - .lock() - .clone() - .and_then(|t| t.upgrade()); - if let Some(t) = &tr { - t.get_codecs().await - } else { - self.media_engine.get_codecs_by_kind(self.kind) - } - }; - - RTCRtpSendParameters { - rtp_parameters, - encodings, - } - } - - /// track returns the RTCRtpTransceiver track, or nil - pub async fn track(&self) -> Option> { - self.track_encodings - .lock() - .await - .first() - .map(|e| Arc::clone(&e.track)) - } - - /// replace_track replaces the track currently being used as the sender's source with a new TrackLocal. - /// The new track must be of the same media kind (audio, video, etc) and switching the track should not - /// require negotiation. - pub async fn replace_track( - &self, - track: Option>, - ) -> Result<()> { - let mut track_encodings = self.track_encodings.lock().await; - - if let Some(t) = &track { - if self.kind != t.kind() { - return Err(Error::ErrRTPSenderNewTrackHasIncorrectKind); - } - - // cannot replace simulcast envelope - if track_encodings.len() > 1 { - return Err(Error::ErrRTPSenderNewTrackHasIncorrectEnvelope); - } - - let encoding = track_encodings - .first_mut() - .ok_or(Error::ErrRTPSenderNewTrackHasIncorrectEnvelope)?; - - let mut context = encoding.context.lock().await; - if self.has_sent() { - encoding.track.unbind(&context).await?; - } - - self.seq_trans.reset_offset(); - - let mid = self - .rtp_transceiver - .lock() - .clone() - .and_then(|t| t.upgrade()) - .and_then(|t| t.mid()); - - let new_context = TrackLocalContext { - id: context.id.clone(), - params: self - .media_engine - .get_rtp_parameters_by_kind(t.kind(), RTCRtpTransceiverDirection::Sendonly), - ssrc: context.ssrc, - write_stream: context.write_stream.clone(), - paused: self.paused.clone(), - mid, - }; - - match t.bind(&new_context).await { - Err(err) => { - // Re-bind the original track - encoding.track.bind(&context).await?; - - Err(err) - } - Ok(codec) => { - // Codec has changed - context.params.codecs = vec![codec]; - encoding.track = Arc::clone(t); - Ok(()) - } - } - } else { - if self.has_sent() { - for encoding in track_encodings.drain(..) { - let context = encoding.context.lock().await; - encoding.track.unbind(&context).await?; - } - } else { - track_encodings.clear(); - } - - Ok(()) - } - } - - /// send Attempts to set the parameters controlling the sending of media. - pub async fn send(&self, parameters: &RTCRtpSendParameters) -> Result<()> { - if self.has_sent() { - return Err(Error::ErrRTPSenderSendAlreadyCalled); - } - let track_encodings = self.track_encodings.lock().await; - if track_encodings.is_empty() { - return Err(Error::ErrRTPSenderTrackRemoved); - } - - let mid = self - .rtp_transceiver - .lock() - .clone() - .and_then(|t| t.upgrade()) - .and_then(|t| t.mid()); - - for (idx, encoding) in track_encodings.iter().enumerate() { - let write_stream = Arc::new(InterceptorToTrackLocalWriter::new(self.paused.clone())); - let mut context = TrackLocalContext { - id: self.id.clone(), - params: self.media_engine.get_rtp_parameters_by_kind( - encoding.track.kind(), - RTCRtpTransceiverDirection::Sendonly, - ), - ssrc: parameters.encodings[idx].ssrc, - write_stream: Some( - Arc::clone(&write_stream) as Arc - ), - paused: self.paused.clone(), - mid: mid.to_owned(), - }; - - let codec = encoding.track.bind(&context).await?; - let stream_info = create_stream_info( - self.id.clone(), - parameters.encodings[idx].ssrc, - codec.payload_type, - codec.capability.clone(), - ¶meters.rtp_parameters.header_extensions, - ); - context.params.codecs = vec![codec]; - - let srtp_writer = Arc::clone(&encoding.srtp_stream) as Arc; - let rtp_writer = self - .interceptor - .bind_local_stream(&stream_info, srtp_writer) - .await; - - *encoding.context.lock().await = context; - *encoding.stream_info.lock().await = stream_info; - *write_stream.interceptor_rtp_writer.lock().await = Some(rtp_writer); - } - - self.send_called_tx.lock().take(); - Ok(()) - } - - /// stop irreversibly stops the RTPSender - pub async fn stop(&self) -> Result<()> { - if self.stop_called_signal.load(Ordering::SeqCst) { - return Ok(()); - } - self.stop_called_signal.store(true, Ordering::SeqCst); - self.stop_called_tx.notify_waiters(); - - if !self.has_sent() { - return Ok(()); - } - - self.replace_track(None).await?; - - let track_encodings = self.track_encodings.lock().await; - for encoding in track_encodings.iter() { - let stream_info = encoding.stream_info.lock().await; - self.interceptor.unbind_local_stream(&stream_info).await; - - encoding.srtp_stream.close().await?; - } - - Ok(()) - } - - /// read reads incoming RTCP for this RTPReceiver - pub async fn read( - &self, - b: &mut [u8], - ) -> Result<(Vec>, Attributes)> { - let mut send_called_rx = self.internal.send_called_rx.lock().await; - - tokio::select! { - _ = send_called_rx.recv() => { - let rtcp_interceptor = { - let track_encodings = self.track_encodings.lock().await; - track_encodings.first().map(|e|e.rtcp_interceptor.clone()) - }.ok_or(Error::ErrInterceptorNotBind)?; - let a = Attributes::new(); - tokio::select! { - _ = self.internal.stop_called_rx.notified() => Err(Error::ErrClosedPipe), - result = rtcp_interceptor.read(b, &a) => Ok(result?), - } - } - _ = self.internal.stop_called_rx.notified() => Err(Error::ErrClosedPipe), - } - } - - /// read_rtcp is a convenience method that wraps Read and unmarshals for you. - pub async fn read_rtcp( - &self, - ) -> Result<(Vec>, Attributes)> { - let mut b = vec![0u8; self.receive_mtu]; - let (pkts, attributes) = self.read(&mut b).await?; - - Ok((pkts, attributes)) - } - - /// ReadSimulcast reads incoming RTCP for this RTPSender for given rid - pub async fn read_simulcast( - &self, - b: &mut [u8], - rid: &str, - ) -> Result<(Vec>, Attributes)> { - let mut send_called_rx = self.internal.send_called_rx.lock().await; - - tokio::select! { - _ = send_called_rx.recv() => { - let rtcp_interceptor = { - let track_encodings = self.track_encodings.lock().await; - track_encodings.iter().find(|e| e.track.rid() == Some(rid)).map(|e| e.rtcp_interceptor.clone()) - }.ok_or(Error::ErrRTPSenderNoTrackForRID)?; - let a = Attributes::new(); - tokio::select! { - _ = self.internal.stop_called_rx.notified() => Err(Error::ErrClosedPipe), - result = rtcp_interceptor.read(b, &a) => Ok(result?), - } - } - _ = self.internal.stop_called_rx.notified() => Err(Error::ErrClosedPipe), - } - } - - /// ReadSimulcastRTCP is a convenience method that wraps ReadSimulcast and unmarshal for you - pub async fn read_rtcp_simulcast( - &self, - rid: &str, - ) -> Result<(Vec>, Attributes)> { - let mut b = vec![0u8; self.receive_mtu]; - let (pkts, attributes) = self.read_simulcast(&mut b, rid).await?; - - Ok((pkts, attributes)) - } - - /// Enables overriding outgoing `RTP` packets' `sequence number`s. - /// - /// Must be called once before any data sent or never called at all. - /// - /// # Errors - /// - /// Errors if this [`RTCRtpSender`] has started to send data or sequence - /// transforming has been already enabled. - pub fn enable_seq_transformer(&self) -> Result<()> { - self.seq_trans.enable() - } - - /// has_sent tells if data has been ever sent for this instance - pub(crate) fn has_sent(&self) -> bool { - let send_called_tx = self.send_called_tx.lock(); - send_called_tx.is_none() - } - - /// has_stopped tells if stop has been called - pub(crate) async fn has_stopped(&self) -> bool { - self.stop_called_signal.load(Ordering::SeqCst) - } - - pub(crate) fn initial_track_id(&self) -> Option { - let lock = self.initial_track_id.lock().unwrap(); - - lock.clone() - } - - pub(crate) fn set_initial_track_id(&self, id: String) -> Result<()> { - let mut lock = self.initial_track_id.lock().unwrap(); - - if lock.is_some() { - return Err(Error::ErrSenderInitialTrackIdAlreadySet); - } - - *lock = Some(id); - - Ok(()) - } - - pub(crate) fn associate_media_stream_id(&self, id: String) -> bool { - let mut lock = self.associated_media_stream_ids.lock().unwrap(); - - if lock.contains(&id) { - return false; - } - - lock.push(id); - - true - } - - pub(crate) fn associated_media_stream_ids(&self) -> Vec { - let lock = self.associated_media_stream_ids.lock().unwrap(); - - lock.clone() - } -} diff --git a/webrtc/src/rtp_transceiver/rtp_sender/rtp_sender_test.rs b/webrtc/src/rtp_transceiver/rtp_sender/rtp_sender_test.rs deleted file mode 100644 index f65cdd24d..000000000 --- a/webrtc/src/rtp_transceiver/rtp_sender/rtp_sender_test.rs +++ /dev/null @@ -1,622 +0,0 @@ -use bytes::Bytes; -use portable_atomic::AtomicU64; -use tokio::time::Duration; -use waitgroup::WaitGroup; - -use super::*; -use crate::api::media_engine::{MIME_TYPE_H264, MIME_TYPE_OPUS, MIME_TYPE_VP8, MIME_TYPE_VP9}; -use crate::api::setting_engine::SettingEngine; -use crate::api::APIBuilder; -use crate::error::Result; -use crate::peer_connection::peer_connection_state::RTCPeerConnectionState; -use crate::peer_connection::peer_connection_test::{ - close_pair_now, create_vnet_pair, new_pair, send_video_until_done, signal_pair, - until_connection_state, -}; -use crate::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; -use crate::rtp_transceiver::RTCRtpCodecParameters; -use crate::track::track_local::track_local_static_sample::TrackLocalStaticSample; - -#[tokio::test] -async fn test_rtp_sender_replace_track() -> Result<()> { - let mut s = SettingEngine::default(); - s.disable_srtp_replay_protection(true); - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - - let api = APIBuilder::new() - .with_setting_engine(s) - .with_media_engine(m) - .build(); - - let (mut sender, mut receiver) = new_pair(&api).await?; - - let track_a = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let track_b = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_H264.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let rtp_sender = sender - .add_track(Arc::clone(&track_a) as Arc) - .await?; - - let (seen_packet_a_tx, seen_packet_a_rx) = mpsc::channel::<()>(1); - let (seen_packet_b_tx, seen_packet_b_rx) = mpsc::channel::<()>(1); - - let seen_packet_a_tx = Arc::new(seen_packet_a_tx); - let seen_packet_b_tx = Arc::new(seen_packet_b_tx); - let on_track_count = Arc::new(AtomicU64::new(0)); - receiver.on_track(Box::new(move |track, _, _| { - assert_eq!(on_track_count.fetch_add(1, Ordering::SeqCst), 0); - let seen_packet_a_tx2 = Arc::clone(&seen_packet_a_tx); - let seen_packet_b_tx2 = Arc::clone(&seen_packet_b_tx); - Box::pin(async move { - let pkt = match track.read_rtp().await { - Ok((pkt, _)) => pkt, - Err(err) => { - //assert!(errors.Is(io.EOF, err)) - log::debug!("{}", err); - return; - } - }; - - let last = pkt.payload[pkt.payload.len() - 1]; - if last == 0xAA { - assert_eq!(track.codec().capability.mime_type, MIME_TYPE_VP8); - let _ = seen_packet_a_tx2.send(()).await; - } else if last == 0xBB { - assert_eq!(track.codec().capability.mime_type, MIME_TYPE_H264); - let _ = seen_packet_b_tx2.send(()).await; - } else { - panic!("Unexpected RTP Data {last:02x}"); - } - }) - })); - - signal_pair(&mut sender, &mut receiver).await?; - - // Block Until packet with 0xAA has been seen - tokio::spawn(async move { - send_video_until_done( - seen_packet_a_rx, - vec![track_a], - Bytes::from_static(&[0xAA]), - None, - ) - .await; - }); - - rtp_sender - .replace_track(Some( - Arc::clone(&track_b) as Arc - )) - .await?; - - // Block Until packet with 0xBB has been seen - tokio::spawn(async move { - send_video_until_done( - seen_packet_b_rx, - vec![track_b], - Bytes::from_static(&[0xBB]), - None, - ) - .await; - }); - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_get_parameters() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offerer, mut answerer) = new_pair(&api).await?; - - let rtp_transceiver = offerer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - signal_pair(&mut offerer, &mut answerer).await?; - - let sender = rtp_transceiver.sender().await; - assert!(sender.track().await.is_some()); - let parameters = sender.get_parameters().await; - assert_ne!(0, parameters.rtp_parameters.codecs.len()); - assert_eq!(1, parameters.encodings.len()); - assert_eq!( - sender.track_encodings.lock().await[0].ssrc, - parameters.encodings[0].ssrc - ); - assert!(parameters.encodings[0].rid.is_empty()); - - close_pair_now(&offerer, &answerer).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_get_parameters_with_rid() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut offerer, mut answerer) = new_pair(&api).await?; - - let rtp_transceiver = offerer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - signal_pair(&mut offerer, &mut answerer).await?; - - let rid = "moo"; - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - rid.to_owned(), - "webrtc-rs".to_owned(), - )); - rtp_transceiver.set_sending_track(Some(track)).await?; - - let sender = rtp_transceiver.sender().await; - assert!(sender.track().await.is_some()); - let parameters = sender.get_parameters().await; - assert_ne!(0, parameters.rtp_parameters.codecs.len()); - assert_eq!(1, parameters.encodings.len()); - assert_eq!( - sender.track_encodings.lock().await[0].ssrc, - parameters.encodings[0].ssrc - ); - assert_eq!(rid, parameters.encodings[0].rid); - - close_pair_now(&offerer, &answerer).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_set_read_deadline() -> Result<()> { - let (mut sender, mut receiver, wan) = create_vnet_pair().await?; - - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let rtp_sender = sender - .add_track(Arc::clone(&track) as Arc) - .await?; - - let peer_connections_connected = WaitGroup::new(); - until_connection_state( - &mut sender, - &peer_connections_connected, - RTCPeerConnectionState::Connected, - ) - .await; - until_connection_state( - &mut receiver, - &peer_connections_connected, - RTCPeerConnectionState::Connected, - ) - .await; - - signal_pair(&mut sender, &mut receiver).await?; - - peer_connections_connected.wait().await; - - let result = tokio::time::timeout(Duration::from_secs(1), rtp_sender.read_rtcp()).await; - assert!(result.is_err()); - - { - let mut w = wan.lock().await; - w.stop().await?; - } - close_pair_now(&sender, &receiver).await; - - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_replace_track_invalid_track_kind_change() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut sender, mut receiver) = new_pair(&api).await?; - - let track_a = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let track_b = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - "audio".to_owned(), - "webrtc-rs".to_owned(), - )); - - let rtp_sender = sender - .add_track(Arc::clone(&track_a) as Arc) - .await?; - - signal_pair(&mut sender, &mut receiver).await?; - - let (seen_packet_tx, seen_packet_rx) = mpsc::channel::<()>(1); - let seen_packet_tx = Arc::new(seen_packet_tx); - receiver.on_track(Box::new(move |_, _, _| { - let seen_packet_tx2 = Arc::clone(&seen_packet_tx); - Box::pin(async move { - let _ = seen_packet_tx2.send(()).await; - }) - })); - - tokio::spawn(async move { - send_video_until_done( - seen_packet_rx, - vec![track_a], - Bytes::from_static(&[0xAA]), - None, - ) - .await; - }); - - if let Err(err) = rtp_sender.replace_track(Some(track_b)).await { - assert_eq!(err, Error::ErrRTPSenderNewTrackHasIncorrectKind); - } else { - panic!(); - } - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_replace_track_invalid_codec_change() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut sender, mut receiver) = new_pair(&api).await?; - - let track_a = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let track_b = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let rtp_sender = sender - .add_track(Arc::clone(&track_a) as Arc) - .await?; - - { - let tr = rtp_sender.rtp_transceiver.lock(); - let t = tr - .as_ref() - .and_then(|t| t.upgrade()) - .expect("Weak transceiver valid"); - t.set_codec_preferences(vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - payload_type: 96, - ..Default::default() - }]) - .await?; - } - - signal_pair(&mut sender, &mut receiver).await?; - - let (seen_packet_tx, seen_packet_rx) = mpsc::channel::<()>(1); - let seen_packet_tx = Arc::new(seen_packet_tx); - receiver.on_track(Box::new(move |_, _, _| { - let seen_packet_tx2 = Arc::clone(&seen_packet_tx); - Box::pin(async move { - let _ = seen_packet_tx2.send(()).await; - }) - })); - - tokio::spawn(async move { - send_video_until_done( - seen_packet_rx, - vec![track_a], - Bytes::from_static(&[0xAA]), - None, - ) - .await; - }); - - if let Err(err) = rtp_sender.replace_track(Some(track_b)).await { - assert_eq!(err, Error::ErrUnsupportedCodec); - } else { - panic!(); - } - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_get_parameters_replaced() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (sender, receiver) = new_pair(&api).await?; - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let rtp_sender = sender.add_track(track).await?; - let param = rtp_sender.get_parameters().await; - assert_eq!(1, param.encodings.len()); - - rtp_sender.replace_track(None).await?; - let param = rtp_sender.get_parameters().await; - assert_eq!(0, param.encodings.len()); - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_send() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (sender, receiver) = new_pair(&api).await?; - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let rtp_sender = sender.add_track(track).await?; - let param = rtp_sender.get_parameters().await; - assert_eq!(1, param.encodings.len()); - - rtp_sender.send(¶m).await?; - - assert_eq!( - Error::ErrRTPSenderSendAlreadyCalled, - rtp_sender.send(¶m).await.unwrap_err() - ); - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_send_track_removed() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (sender, receiver) = new_pair(&api).await?; - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let rtp_sender = sender.add_track(track).await?; - let param = rtp_sender.get_parameters().await; - assert_eq!(1, param.encodings.len()); - - sender.remove_track(&rtp_sender).await?; - assert_eq!( - Error::ErrRTPSenderTrackRemoved, - rtp_sender.send(¶m).await.unwrap_err() - ); - - close_pair_now(&sender, &receiver).await; - Ok(()) -} - -#[tokio::test] -async fn test_rtp_sender_add_encoding() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (sender, receiver) = new_pair(&api).await?; - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - let rtp_sender = sender.add_track(track).await?; - - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderRidNil, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "h".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderNoBaseEncoding, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "f".to_owned(), - "webrtc-rs".to_owned(), - )); - let rtp_sender = sender.add_track(track).await?; - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video-foobar".to_owned(), - "h".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderBaseEncodingMismatch, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "h".to_owned(), - "webrtc-rs-foobar".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderBaseEncodingMismatch, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "h".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderBaseEncodingMismatch, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "f".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderRIDCollision, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "h".to_owned(), - "webrtc-rs".to_owned(), - )); - rtp_sender.add_encoding(track).await?; - - rtp_sender.send(&rtp_sender.get_parameters().await).await?; - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "f".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderSendAlreadyCalled, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - rtp_sender.stop().await?; - - let track = Arc::new(TrackLocalStaticSample::new_with_rid( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "f".to_owned(), - "webrtc-rs".to_owned(), - )); - assert_eq!( - Error::ErrRTPSenderStopped, - rtp_sender.add_encoding(track).await.unwrap_err() - ); - - close_pair_now(&sender, &receiver).await; - Ok(()) -} diff --git a/webrtc/src/rtp_transceiver/rtp_transceiver_direction.rs b/webrtc/src/rtp_transceiver/rtp_transceiver_direction.rs deleted file mode 100644 index 756731ede..000000000 --- a/webrtc/src/rtp_transceiver/rtp_transceiver_direction.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::fmt; - -/// RTPTransceiverDirection indicates the direction of the RTPTransceiver. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum RTCRtpTransceiverDirection { - Unspecified, - - /// Sendrecv indicates the RTPSender will offer - /// to send RTP and RTPReceiver the will offer to receive RTP. - Sendrecv, - - /// Sendonly indicates the RTPSender will offer to send RTP. - Sendonly, - - /// Recvonly indicates the RTPReceiver the will offer to receive RTP. - Recvonly, - - /// Inactive indicates the RTPSender won't offer - /// to send RTP and RTPReceiver the won't offer to receive RTP. - Inactive, -} - -const RTP_TRANSCEIVER_DIRECTION_SENDRECV_STR: &str = "sendrecv"; -const RTP_TRANSCEIVER_DIRECTION_SENDONLY_STR: &str = "sendonly"; -const RTP_TRANSCEIVER_DIRECTION_RECVONLY_STR: &str = "recvonly"; -const RTP_TRANSCEIVER_DIRECTION_INACTIVE_STR: &str = "inactive"; - -/// defines a procedure for creating a new -/// RTPTransceiverDirection from a raw string naming the transceiver direction. -impl From<&str> for RTCRtpTransceiverDirection { - fn from(raw: &str) -> Self { - match raw { - RTP_TRANSCEIVER_DIRECTION_SENDRECV_STR => RTCRtpTransceiverDirection::Sendrecv, - RTP_TRANSCEIVER_DIRECTION_SENDONLY_STR => RTCRtpTransceiverDirection::Sendonly, - RTP_TRANSCEIVER_DIRECTION_RECVONLY_STR => RTCRtpTransceiverDirection::Recvonly, - RTP_TRANSCEIVER_DIRECTION_INACTIVE_STR => RTCRtpTransceiverDirection::Inactive, - _ => RTCRtpTransceiverDirection::Unspecified, - } - } -} - -impl From for RTCRtpTransceiverDirection { - fn from(v: u8) -> Self { - match v { - 1 => RTCRtpTransceiverDirection::Sendrecv, - 2 => RTCRtpTransceiverDirection::Sendonly, - 3 => RTCRtpTransceiverDirection::Recvonly, - 4 => RTCRtpTransceiverDirection::Inactive, - _ => RTCRtpTransceiverDirection::Unspecified, - } - } -} - -impl fmt::Display for RTCRtpTransceiverDirection { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - RTCRtpTransceiverDirection::Sendrecv => { - write!(f, "{RTP_TRANSCEIVER_DIRECTION_SENDRECV_STR}") - } - RTCRtpTransceiverDirection::Sendonly => { - write!(f, "{RTP_TRANSCEIVER_DIRECTION_SENDONLY_STR}") - } - RTCRtpTransceiverDirection::Recvonly => { - write!(f, "{RTP_TRANSCEIVER_DIRECTION_RECVONLY_STR}") - } - RTCRtpTransceiverDirection::Inactive => { - write!(f, "{RTP_TRANSCEIVER_DIRECTION_INACTIVE_STR}") - } - _ => write!(f, "{}", crate::UNSPECIFIED_STR), - } - } -} - -impl RTCRtpTransceiverDirection { - /// reverse indicate the opposite direction - pub fn reverse(&self) -> RTCRtpTransceiverDirection { - match *self { - RTCRtpTransceiverDirection::Sendonly => RTCRtpTransceiverDirection::Recvonly, - RTCRtpTransceiverDirection::Recvonly => RTCRtpTransceiverDirection::Sendonly, - _ => *self, - } - } - - pub fn intersect(&self, other: RTCRtpTransceiverDirection) -> RTCRtpTransceiverDirection { - Self::from_send_recv( - self.has_send() && other.has_send(), - self.has_recv() && other.has_recv(), - ) - } - - pub fn from_send_recv(send: bool, recv: bool) -> RTCRtpTransceiverDirection { - match (send, recv) { - (true, true) => Self::Sendrecv, - (true, false) => Self::Sendonly, - (false, true) => Self::Recvonly, - (false, false) => Self::Inactive, - } - } - - pub fn has_send(&self) -> bool { - matches!(self, Self::Sendrecv | Self::Sendonly) - } - - pub fn has_recv(&self) -> bool { - matches!(self, Self::Sendrecv | Self::Recvonly) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_rtp_transceiver_direction() { - let tests = vec![ - ("Unspecified", RTCRtpTransceiverDirection::Unspecified), - ("sendrecv", RTCRtpTransceiverDirection::Sendrecv), - ("sendonly", RTCRtpTransceiverDirection::Sendonly), - ("recvonly", RTCRtpTransceiverDirection::Recvonly), - ("inactive", RTCRtpTransceiverDirection::Inactive), - ]; - - for (ct_str, expected_type) in tests { - assert_eq!(RTCRtpTransceiverDirection::from(ct_str), expected_type); - } - } - - #[test] - fn test_rtp_transceiver_direction_string() { - let tests = vec![ - (RTCRtpTransceiverDirection::Unspecified, "Unspecified"), - (RTCRtpTransceiverDirection::Sendrecv, "sendrecv"), - (RTCRtpTransceiverDirection::Sendonly, "sendonly"), - (RTCRtpTransceiverDirection::Recvonly, "recvonly"), - (RTCRtpTransceiverDirection::Inactive, "inactive"), - ]; - - for (d, expected_string) in tests { - assert_eq!(d.to_string(), expected_string); - } - } - - #[test] - fn test_rtp_transceiver_has_send() { - let tests = vec![ - (RTCRtpTransceiverDirection::Unspecified, false), - (RTCRtpTransceiverDirection::Sendrecv, true), - (RTCRtpTransceiverDirection::Sendonly, true), - (RTCRtpTransceiverDirection::Recvonly, false), - (RTCRtpTransceiverDirection::Inactive, false), - ]; - - for (d, expected_value) in tests { - assert_eq!(d.has_send(), expected_value); - } - } - - #[test] - fn test_rtp_transceiver_has_recv() { - let tests = vec![ - (RTCRtpTransceiverDirection::Unspecified, false), - (RTCRtpTransceiverDirection::Sendrecv, true), - (RTCRtpTransceiverDirection::Sendonly, false), - (RTCRtpTransceiverDirection::Recvonly, true), - (RTCRtpTransceiverDirection::Inactive, false), - ]; - - for (d, expected_value) in tests { - assert_eq!(d.has_recv(), expected_value); - } - } - - #[test] - fn test_rtp_transceiver_from_send_recv() { - let tests = vec![ - (RTCRtpTransceiverDirection::Sendrecv, (true, true)), - (RTCRtpTransceiverDirection::Sendonly, (true, false)), - (RTCRtpTransceiverDirection::Recvonly, (false, true)), - (RTCRtpTransceiverDirection::Inactive, (false, false)), - ]; - - for (expected_value, (send, recv)) in tests { - assert_eq!( - RTCRtpTransceiverDirection::from_send_recv(send, recv), - expected_value - ); - } - } - - #[test] - fn test_rtp_transceiver_intersect() { - use RTCRtpTransceiverDirection::*; - - let tests = vec![ - ((Sendrecv, Recvonly), Recvonly), - ((Sendrecv, Sendonly), Sendonly), - ((Sendrecv, Inactive), Inactive), - ((Sendonly, Inactive), Inactive), - ((Recvonly, Inactive), Inactive), - ((Recvonly, Sendrecv), Recvonly), - ((Sendonly, Sendrecv), Sendonly), - ((Sendonly, Recvonly), Inactive), - ((Recvonly, Recvonly), Recvonly), - ]; - - for ((a, b), expected_direction) in tests { - assert_eq!(a.intersect(b), expected_direction); - } - } -} diff --git a/webrtc/src/rtp_transceiver/rtp_transceiver_test.rs b/webrtc/src/rtp_transceiver/rtp_transceiver_test.rs deleted file mode 100644 index 6e6cc75a9..000000000 --- a/webrtc/src/rtp_transceiver/rtp_transceiver_test.rs +++ /dev/null @@ -1,356 +0,0 @@ -use portable_atomic::AtomicUsize; - -use super::*; -use crate::api::media_engine::{MIME_TYPE_OPUS, MIME_TYPE_VP8, MIME_TYPE_VP9}; -use crate::api::APIBuilder; -use crate::dtls_transport::RTCDtlsTransport; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::peer_connection_test::{close_pair_now, create_vnet_pair}; - -#[tokio::test] -async fn test_rtp_transceiver_set_codec_preferences() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.push_codecs(m.video_codecs.clone(), RTPCodecType::Video) - .await; - m.push_codecs(m.audio_codecs.clone(), RTPCodecType::Audio) - .await; - - let media_video_codecs = m.video_codecs.clone(); - - let api = APIBuilder::new().with_media_engine(m).build(); - let interceptor = api.interceptor_registry.build("")?; - let transport = Arc::new(RTCDtlsTransport::default()); - let receiver = Arc::new(api.new_rtp_receiver( - RTPCodecType::Video, - Arc::clone(&transport), - Arc::clone(&interceptor), - )); - - let sender = Arc::new( - api.new_rtp_sender(None, Arc::clone(&transport), Arc::clone(&interceptor)) - .await, - ); - - let tr = RTCRtpTransceiver::new( - receiver, - sender, - RTCRtpTransceiverDirection::Unspecified, - RTPCodecType::Video, - media_video_codecs.clone(), - Arc::clone(&api.media_engine), - None, - ) - .await; - - assert_eq!(&tr.get_codecs().await, &media_video_codecs); - - let fail_test_cases = vec![ - vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_string(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "minptime=10;useinbandfec=1".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }], - vec![ - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_OPUS.to_string(), - clock_rate: 48000, - channels: 2, - sdp_fmtp_line: "minptime=10;useinbandfec=1".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 111, - ..Default::default() - }, - ], - ]; - - for test_case in fail_test_cases { - if let Err(err) = tr.set_codec_preferences(test_case).await { - assert_eq!(err, Error::ErrRTPTransceiverCodecUnsupported); - } else { - panic!(); - } - } - - let success_test_cases = vec![ - vec![RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }], - vec![ - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP9.to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "profile-id=0".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 98, - ..Default::default() - }, - ], - ]; - - for test_case in success_test_cases { - tr.set_codec_preferences(test_case).await?; - } - - tr.set_codec_preferences(vec![]).await?; - assert_ne!(0, tr.get_codecs().await.len()); - - Ok(()) -} - -// Assert that SetCodecPreferences properly filters codecs and PayloadTypes are respected -#[tokio::test] -async fn test_rtp_transceiver_set_codec_preferences_payload_type() -> Result<()> { - let test_codec = RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: "video/test_codec".to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 50, - ..Default::default() - }; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - let offer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - m.register_codec(test_codec.clone(), RTPCodecType::Video)?; - let api = APIBuilder::new().with_media_engine(m).build(); - let answer_pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let _ = offer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let answer_transceiver = answer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - answer_transceiver - .set_codec_preferences(vec![ - test_codec, - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_string(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_string(), - rtcp_feedback: vec![], - }, - payload_type: 51, - ..Default::default() - }, - ]) - .await?; - - let offer = offer_pc.create_offer(None).await?; - - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - - // VP8 with proper PayloadType - assert!( - answer.sdp.contains("a=rtpmap:51 VP8/90000"), - "{}", - answer.sdp - ); - - // test_codec is ignored since offerer doesn't support - assert!(!answer.sdp.contains("test_codec")); - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} - -#[tokio::test] -async fn test_rtp_transceiver_direction_change() -> Result<()> { - let (offer_pc, answer_pc, _) = create_vnet_pair().await?; - - let offer_transceiver = offer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let _ = answer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let offer = offer_pc.create_offer(None).await?; - - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - assert!(answer.sdp.contains("a=sendrecv"),); - answer_pc.set_local_description(answer.clone()).await?; - offer_pc.set_remote_description(answer).await?; - - offer_transceiver - .set_direction(RTCRtpTransceiverDirection::Inactive) - .await; - - let offer = offer_pc.create_offer(None).await?; - assert!(offer.sdp.contains("a=inactive"),); - - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - assert!(answer.sdp.contains("a=inactive"),); - offer_pc.set_remote_description(answer).await?; - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} - -#[tokio::test] -async fn test_rtp_transceiver_set_direction_causing_negotiation() -> Result<()> { - let (offer_pc, answer_pc, _) = create_vnet_pair().await?; - - let count = Arc::new(AtomicUsize::new(0)); - - { - let count = count.clone(); - offer_pc.on_negotiation_needed(Box::new(move || { - let count = count.clone(); - Box::pin(async move { - count.fetch_add(1, Ordering::SeqCst); - }) - })); - } - - let offer_transceiver = offer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let _ = answer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let offer = offer_pc.create_offer(None).await?; - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - answer_pc.set_local_description(answer.clone()).await?; - offer_pc.set_remote_description(answer).await?; - - assert_eq!(count.load(Ordering::SeqCst), 0); - - let offer = offer_pc.create_offer(None).await?; - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - answer_pc.set_local_description(answer.clone()).await?; - offer_pc.set_remote_description(answer).await?; - - assert_eq!(count.load(Ordering::SeqCst), 0); - - offer_transceiver - .set_direction(RTCRtpTransceiverDirection::Inactive) - .await; - - // wait for negotiation ops queue to finish. - offer_pc.internal.ops.done().await; - - assert_eq!(count.load(Ordering::SeqCst), 1); - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn test_rtp_transceiver_stopping() -> Result<()> { - let (offer_pc, answer_pc, _) = create_vnet_pair().await?; - - let offer_transceiver = offer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let _ = answer_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let offer = offer_pc.create_offer(None).await?; - - offer_pc.set_local_description(offer.clone()).await?; - answer_pc.set_remote_description(offer).await?; - - let answer = answer_pc.create_answer(None).await?; - assert!(answer.sdp.contains("a=sendrecv"),); - answer_pc.set_local_description(answer.clone()).await?; - offer_pc.set_remote_description(answer).await?; - - assert!( - offer_transceiver.mid().is_some(), - "A mid should have been associated with the transceiver when applying the answer" - ); - // Stop the transceiver - offer_transceiver.stop().await?; - - let offer = offer_pc.create_offer(None).await?; - assert!(offer.sdp.contains("a=inactive"),); - let parsed = offer.parsed.unwrap(); - let m = &parsed.media_descriptions[0]; - assert_eq!( - m.media_name.port.value, 0, - "After stopping a transceiver it should be rejected in offers" - ); - - close_pair_now(&offer_pc, &answer_pc).await; - - Ok(()) -} diff --git a/webrtc/src/rtp_transceiver/srtp_writer_future.rs b/webrtc/src/rtp_transceiver/srtp_writer_future.rs deleted file mode 100644 index 5ceadc828..000000000 --- a/webrtc/src/rtp_transceiver/srtp_writer_future.rs +++ /dev/null @@ -1,290 +0,0 @@ -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; - -use async_trait::async_trait; -use bytes::Bytes; -use interceptor::{Attributes, RTCPReader, RTPWriter}; -use portable_atomic::AtomicBool; -use srtp::session::Session; -use srtp::stream::Stream; -use tokio::sync::Mutex; -use util; - -use crate::dtls_transport::RTCDtlsTransport; -use crate::error::{Error, Result}; -use crate::rtp_transceiver::rtp_sender::RTPSenderInternal; -use crate::rtp_transceiver::SSRC; - -/// `RTP` packet sequence number manager. -/// -/// Used to override outgoing `RTP` packets' sequence numbers. On creating it is -/// unabled and can be enabled before sending data beginning. Once data sending -/// began it can not be enabled any more. -pub(crate) struct SequenceTransformer(util::sync::Mutex); - -/// [`SequenceTransformer`] inner. -struct SequenceTransformerInner { - offset: u16, - last_sq: u16, - reset_needed: bool, - enabled: bool, - data_sent: bool, -} - -impl SequenceTransformer { - /// Creates a new [`SequenceTransformer`]. - pub(crate) fn new() -> Self { - Self(util::sync::Mutex::new(SequenceTransformerInner { - offset: 0, - last_sq: rand::random(), - reset_needed: false, - enabled: false, - data_sent: false, - })) - } - - /// Enables this [`SequenceTransformer`]. - /// - /// # Errors - /// - /// With [`Error::ErrRTPSenderSeqTransEnabled`] on trying to enable already - /// enabled [`SequenceTransformer`]. - /// - /// With [`Error::ErrRTPSenderSeqTransEnabled`] on trying to enable - /// [`SequenceTransformer`] after data sending began. - pub(crate) fn enable(&self) -> Result<()> { - let mut guard = self.0.lock(); - - if guard.enabled { - return Err(Error::ErrRTPSenderSeqTransEnabled); - } - - (!guard.data_sent) - .then(|| { - guard.enabled = true; - }) - .ok_or(Error::ErrRTPSenderDataSent) - } - - /// Indicates [`SequenceTransformer`] about necessity of recalculating - /// `offset`. - pub(crate) fn reset_offset(&self) { - self.0.lock().reset_needed = true; - } - - /// Gets [`Some`] consistent `sequence number` if this [`SequenceTransformer`] is - /// enabled or [`None`] if it is not. - /// - /// Once this method is called, considers data sending began. - fn seq_number(&self, raw_sn: u16) -> Option { - let mut guard = self.0.lock(); - guard.data_sent = true; - - if !guard.enabled { - return None; - } - - let offset = guard - .reset_needed - .then(|| { - guard.reset_needed = false; - let offset = guard.last_sq.overflowing_sub(raw_sn.overflowing_sub(1).0).0; - guard.offset = offset; - offset - }) - .unwrap_or(guard.offset); - let next = raw_sn.overflowing_add(offset).0; - guard.last_sq = next; - - Some(next) - } -} - -/// SrtpWriterFuture blocks Read/Write calls until -/// the SRTP Session is available -pub(crate) struct SrtpWriterFuture { - pub(crate) closed: AtomicBool, - pub(crate) ssrc: SSRC, - pub(crate) rtp_sender: Weak, - pub(crate) rtp_transport: Arc, - pub(crate) rtcp_read_stream: Mutex>>, // atomic.Value // * - pub(crate) rtp_write_session: Mutex>>, // atomic.Value // * - pub(crate) seq_trans: Arc, -} - -impl SrtpWriterFuture { - async fn init(&self, return_when_no_srtp: bool) -> Result<()> { - if return_when_no_srtp { - { - if let Some(rtp_sender) = self.rtp_sender.upgrade() { - if rtp_sender.stop_called_signal.load(Ordering::SeqCst) { - return Err(Error::ErrClosedPipe); - } - } else { - return Err(Error::ErrClosedPipe); - } - } - - if !self.rtp_transport.srtp_ready_signal.load(Ordering::SeqCst) { - return Ok(()); - } - } else { - let mut rx = self.rtp_transport.srtp_ready_rx.lock().await; - if let Some(srtp_ready_rx) = &mut *rx { - if let Some(rtp_sender) = self.rtp_sender.upgrade() { - tokio::select! { - _ = rtp_sender.stop_called_rx.notified()=> return Err(Error::ErrClosedPipe), - _ = srtp_ready_rx.recv() =>{} - } - } else { - return Err(Error::ErrClosedPipe); - } - } - } - - if self.closed.load(Ordering::SeqCst) { - return Err(Error::ErrClosedPipe); - } - - if let Some(srtcp_session) = self.rtp_transport.get_srtcp_session().await { - let rtcp_read_stream = srtcp_session.open(self.ssrc).await; - let mut stream = self.rtcp_read_stream.lock().await; - *stream = Some(rtcp_read_stream); - } - - { - let srtp_session = self.rtp_transport.get_srtp_session().await; - let mut session = self.rtp_write_session.lock().await; - *session = srtp_session; - } - - Ok(()) - } - - pub async fn close(&self) -> Result<()> { - if self.closed.load(Ordering::SeqCst) { - return Ok(()); - } - self.closed.store(true, Ordering::SeqCst); - - let stream = { - let mut stream = self.rtcp_read_stream.lock().await; - stream.take() - }; - if let Some(rtcp_read_stream) = stream { - Ok(rtcp_read_stream.close().await?) - } else { - Ok(()) - } - } - - pub async fn read(&self, b: &mut [u8]) -> Result { - { - let stream = { - let stream = self.rtcp_read_stream.lock().await; - stream.clone() - }; - if let Some(rtcp_read_stream) = stream { - return Ok(rtcp_read_stream.read(b).await?); - } - } - - self.init(false).await?; - - { - let stream = { - let stream = self.rtcp_read_stream.lock().await; - stream.clone() - }; - if let Some(rtcp_read_stream) = stream { - return Ok(rtcp_read_stream.read(b).await?); - } - } - - Ok(0) - } - - pub async fn write_rtp(&self, pkt: &rtp::packet::Packet) -> Result { - { - let session = { - let session = self.rtp_write_session.lock().await; - session.clone() - }; - if let Some(rtp_write_session) = session { - return Ok(rtp_write_session.write_rtp(pkt).await?); - } - } - - self.init(true).await?; - - { - let session = { - let session = self.rtp_write_session.lock().await; - session.clone() - }; - if let Some(rtp_write_session) = session { - return Ok(rtp_write_session.write_rtp(pkt).await?); - } - } - - Ok(0) - } - - pub async fn write(&self, b: &Bytes) -> Result { - { - let session = { - let session = self.rtp_write_session.lock().await; - session.clone() - }; - if let Some(rtp_write_session) = session { - return Ok(rtp_write_session.write(b, true).await?); - } - } - - self.init(true).await?; - - { - let session = { - let session = self.rtp_write_session.lock().await; - session.clone() - }; - if let Some(rtp_write_session) = session { - return Ok(rtp_write_session.write(b, true).await?); - } - } - - Ok(0) - } -} - -type IResult = std::result::Result; - -#[async_trait] -impl RTCPReader for SrtpWriterFuture { - async fn read( - &self, - buf: &mut [u8], - a: &Attributes, - ) -> IResult<(Vec>, Attributes)> { - let read = self.read(buf).await?; - let pkt = rtcp::packet::unmarshal(&mut &buf[..read])?; - - Ok((pkt, a.clone())) - } -} - -#[async_trait] -impl RTPWriter for SrtpWriterFuture { - async fn write(&self, pkt: &rtp::packet::Packet, _a: &Attributes) -> IResult { - Ok( - match self.seq_trans.seq_number(pkt.header.sequence_number) { - Some(seq_num) => { - let mut new_pkt = pkt.clone(); - new_pkt.header.sequence_number = seq_num; - self.write_rtp(&new_pkt).await? - } - None => self.write_rtp(pkt).await?, - }, - ) - } -} diff --git a/webrtc/src/sctp_transport/mod.rs b/webrtc/src/sctp_transport/mod.rs deleted file mode 100644 index f4914c301..000000000 --- a/webrtc/src/sctp_transport/mod.rs +++ /dev/null @@ -1,442 +0,0 @@ -#[cfg(test)] -mod sctp_transport_test; - -pub mod sctp_transport_capabilities; -pub mod sctp_transport_state; - -use std::collections::{HashMap, HashSet}; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use arc_swap::ArcSwapOption; -use data::data_channel::DataChannel; -use data::message::message_channel_open::ChannelType; -use portable_atomic::{AtomicBool, AtomicU32, AtomicU8}; -use sctp::association::Association; -use sctp_transport_state::RTCSctpTransportState; -use tokio::sync::{Mutex, Notify}; -use util::Conn; - -use crate::api::setting_engine::SettingEngine; -use crate::data_channel::data_channel_parameters::DataChannelParameters; -use crate::data_channel::data_channel_state::RTCDataChannelState; -use crate::data_channel::RTCDataChannel; -use crate::dtls_transport::dtls_role::DTLSRole; -use crate::dtls_transport::*; -use crate::error::*; -use crate::sctp_transport::sctp_transport_capabilities::SCTPTransportCapabilities; -use crate::stats::stats_collector::StatsCollector; -use crate::stats::StatsReportType::{PeerConnection, SCTPTransport}; -use crate::stats::{ICETransportStats, PeerConnectionStats}; - -const SCTP_MAX_CHANNELS: u16 = u16::MAX; - -pub type OnDataChannelHdlrFn = Box< - dyn (FnMut(Arc) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -pub type OnDataChannelOpenedHdlrFn = Box< - dyn (FnMut(Arc) -> Pin + Send + 'static>>) - + Send - + Sync, ->; - -struct AcceptDataChannelParams { - notify_rx: Arc, - sctp_association: Arc, - data_channels: Arc>>>, - on_error_handler: Arc>>, - on_data_channel_handler: Arc>>, - on_data_channel_opened_handler: Arc>>, - data_channels_opened: Arc, - data_channels_accepted: Arc, - setting_engine: Arc, -} - -/// SCTPTransport provides details about the SCTP transport. -#[derive(Default)] -pub struct RTCSctpTransport { - pub(crate) dtls_transport: Arc, - - // State represents the current state of the SCTP transport. - state: AtomicU8, // RTCSctpTransportState - - // SCTPTransportState doesn't have an enum to distinguish between New/Connecting - // so we need a dedicated field - is_started: AtomicBool, - - // max_message_size represents the maximum size of data that can be passed to - // DataChannel's send() method. - max_message_size: usize, - - // max_channels represents the maximum amount of DataChannel's that can - // be used simultaneously. - max_channels: u16, - - sctp_association: Mutex>>, - - on_error_handler: Arc>>, - on_data_channel_handler: Arc>>, - on_data_channel_opened_handler: Arc>>, - - // DataChannels - pub(crate) data_channels: Arc>>>, - pub(crate) data_channels_opened: Arc, - pub(crate) data_channels_requested: Arc, - data_channels_accepted: Arc, - - notify_tx: Arc, - - setting_engine: Arc, -} - -impl RTCSctpTransport { - pub(crate) fn new( - dtls_transport: Arc, - setting_engine: Arc, - ) -> Self { - RTCSctpTransport { - dtls_transport, - state: AtomicU8::new(RTCSctpTransportState::Connecting as u8), - is_started: AtomicBool::new(false), - max_message_size: RTCSctpTransport::calc_message_size(65536, 65536), - max_channels: SCTP_MAX_CHANNELS, - sctp_association: Mutex::new(None), - on_error_handler: Arc::new(ArcSwapOption::empty()), - on_data_channel_handler: Arc::new(ArcSwapOption::empty()), - on_data_channel_opened_handler: Arc::new(ArcSwapOption::empty()), - - data_channels: Arc::new(Mutex::new(vec![])), - data_channels_opened: Arc::new(AtomicU32::new(0)), - data_channels_requested: Arc::new(AtomicU32::new(0)), - data_channels_accepted: Arc::new(AtomicU32::new(0)), - - notify_tx: Arc::new(Notify::new()), - - setting_engine, - } - } - - /// transport returns the DTLSTransport instance the SCTPTransport is sending over. - pub fn transport(&self) -> Arc { - Arc::clone(&self.dtls_transport) - } - - /// get_capabilities returns the SCTPCapabilities of the SCTPTransport. - pub fn get_capabilities(&self) -> SCTPTransportCapabilities { - SCTPTransportCapabilities { - max_message_size: 0, - } - } - - /// Start the SCTPTransport. Since both local and remote parties must mutually - /// create an SCTPTransport, SCTP SO (Simultaneous Open) is used to establish - /// a connection over SCTP. - pub async fn start(&self, _remote_caps: SCTPTransportCapabilities) -> Result<()> { - if self.is_started.load(Ordering::SeqCst) { - return Ok(()); - } - self.is_started.store(true, Ordering::SeqCst); - - let dtls_transport = self.transport(); - if let Some(net_conn) = &dtls_transport.conn().await { - let sctp_association = loop { - tokio::select! { - _ = self.notify_tx.notified() => { - // It seems like notify_tx is only notified on Stop so perhaps this check - // is redundant. - // TODO: Consider renaming notify_tx to shutdown_tx. - if self.state.load(Ordering::SeqCst) == RTCSctpTransportState::Closed as u8 { - return Err(Error::ErrSCTPTransportDTLS); - } - }, - association = sctp::association::Association::client(sctp::association::Config { - net_conn: Arc::clone(net_conn) as Arc, - max_receive_buffer_size: 0, - max_message_size: 0, - name: String::new(), - }) => { - break Arc::new(association?); - } - }; - }; - - { - let mut sa = self.sctp_association.lock().await; - *sa = Some(Arc::clone(&sctp_association)); - } - self.state - .store(RTCSctpTransportState::Connected as u8, Ordering::SeqCst); - - let param = AcceptDataChannelParams { - notify_rx: self.notify_tx.clone(), - sctp_association, - data_channels: Arc::clone(&self.data_channels), - on_error_handler: Arc::clone(&self.on_error_handler), - on_data_channel_handler: Arc::clone(&self.on_data_channel_handler), - on_data_channel_opened_handler: Arc::clone(&self.on_data_channel_opened_handler), - data_channels_opened: Arc::clone(&self.data_channels_opened), - data_channels_accepted: Arc::clone(&self.data_channels_accepted), - setting_engine: Arc::clone(&self.setting_engine), - }; - tokio::spawn(async move { - RTCSctpTransport::accept_data_channels(param).await; - }); - - Ok(()) - } else { - Err(Error::ErrSCTPTransportDTLS) - } - } - - /// Stop stops the SCTPTransport - pub async fn stop(&self) -> Result<()> { - { - let mut sctp_association = self.sctp_association.lock().await; - if let Some(sa) = sctp_association.take() { - sa.close().await?; - } - } - - self.state - .store(RTCSctpTransportState::Closed as u8, Ordering::SeqCst); - - self.notify_tx.notify_waiters(); - - Ok(()) - } - - async fn accept_data_channels(param: AcceptDataChannelParams) { - let dcs = param.data_channels.lock().await; - let mut existing_data_channels = Vec::new(); - for dc in dcs.iter() { - if let Some(dc) = dc.data_channel.lock().await.clone() { - existing_data_channels.push(dc); - } - } - drop(dcs); - - loop { - let dc = tokio::select! { - _ = param.notify_rx.notified() => break, - result = DataChannel::accept( - ¶m.sctp_association, - data::data_channel::Config::default(), - &existing_data_channels, - ) => { - match result { - Ok(dc) => dc, - Err(err) => { - if data::Error::ErrStreamClosed == err { - log::error!("Failed to accept data channel: {}", err); - if let Some(handler) = &*param.on_error_handler.load() { - let mut f = handler.lock().await; - f(err.into()).await; - } - } - break; - } - } - } - }; - - let mut max_retransmits = 0; - let mut max_packet_lifetime = 0; - let val = dc.config.reliability_parameter as u16; - let ordered; - - match dc.config.channel_type { - ChannelType::Reliable => { - ordered = true; - } - ChannelType::ReliableUnordered => { - ordered = false; - } - ChannelType::PartialReliableRexmit => { - ordered = true; - max_retransmits = val; - } - ChannelType::PartialReliableRexmitUnordered => { - ordered = false; - max_retransmits = val; - } - ChannelType::PartialReliableTimed => { - ordered = true; - max_packet_lifetime = val; - } - ChannelType::PartialReliableTimedUnordered => { - ordered = false; - max_packet_lifetime = val; - } - }; - - let negotiated = if dc.config.negotiated { - Some(dc.stream_identifier()) - } else { - None - }; - let rtc_dc = Arc::new(RTCDataChannel::new( - DataChannelParameters { - label: dc.config.label.clone(), - protocol: dc.config.protocol.clone(), - negotiated, - ordered, - max_packet_life_time: max_packet_lifetime, - max_retransmits, - }, - Arc::clone(¶m.setting_engine), - )); - - if let Some(handler) = &*param.on_data_channel_handler.load() { - let mut f = handler.lock().await; - f(Arc::clone(&rtc_dc)).await; - - param.data_channels_accepted.fetch_add(1, Ordering::SeqCst); - - let mut dcs = param.data_channels.lock().await; - dcs.push(Arc::clone(&rtc_dc)); - } - - rtc_dc.handle_open(Arc::new(dc)).await; - - if let Some(handler) = &*param.on_data_channel_opened_handler.load() { - let mut f = handler.lock().await; - f(rtc_dc).await; - param.data_channels_opened.fetch_add(1, Ordering::SeqCst); - } - } - } - - /// on_error sets an event handler which is invoked when - /// the SCTP connection error occurs. - pub fn on_error(&self, f: OnErrorHdlrFn) { - self.on_error_handler.store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_data_channel sets an event handler which is invoked when a data - /// channel message arrives from a remote peer. - pub fn on_data_channel(&self, f: OnDataChannelHdlrFn) { - self.on_data_channel_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - /// on_data_channel_opened sets an event handler which is invoked when a data - /// channel is opened - pub fn on_data_channel_opened(&self, f: OnDataChannelOpenedHdlrFn) { - self.on_data_channel_opened_handler - .store(Some(Arc::new(Mutex::new(f)))); - } - - fn calc_message_size(remote_max_message_size: usize, can_send_size: usize) -> usize { - if remote_max_message_size == 0 && can_send_size == 0 { - usize::MAX - } else if remote_max_message_size == 0 { - can_send_size - } else if can_send_size == 0 || can_send_size > remote_max_message_size { - remote_max_message_size - } else { - can_send_size - } - } - - /// max_channels is the maximum number of RTCDataChannels that can be open simultaneously. - pub fn max_channels(&self) -> u16 { - if self.max_channels == 0 { - SCTP_MAX_CHANNELS - } else { - self.max_channels - } - } - - /// state returns the current state of the SCTPTransport - pub fn state(&self) -> RTCSctpTransportState { - self.state.load(Ordering::SeqCst).into() - } - - pub(crate) async fn collect_stats( - &self, - collector: &StatsCollector, - peer_connection_id: String, - ) { - let dtls_transport = self.transport(); - - // TODO: should this be collected? - dtls_transport.collect_stats(collector).await; - - // data channels - let mut data_channels_closed = 0; - let data_channels = self.data_channels.lock().await; - for data_channel in &*data_channels { - match data_channel.ready_state() { - RTCDataChannelState::Connecting => (), - RTCDataChannelState::Open => (), - _ => data_channels_closed += 1, - } - data_channel.collect_stats(collector).await; - } - - let mut reports = HashMap::new(); - let peer_connection_stats = - PeerConnectionStats::new(self, peer_connection_id.clone(), data_channels_closed); - reports.insert(peer_connection_id, PeerConnection(peer_connection_stats)); - - // conn - if let Some(agent) = dtls_transport.ice_transport.gatherer.get_agent().await { - let stats = ICETransportStats::new("sctp_transport".to_owned(), agent); - reports.insert(stats.id.clone(), SCTPTransport(stats)); - } - - collector.merge(reports); - } - - pub(crate) async fn generate_and_set_data_channel_id( - &self, - dtls_role: DTLSRole, - ) -> Result { - let mut id = 0u16; - if dtls_role != DTLSRole::Client { - id += 1; - } - - // Create map of ids so we can compare without double-looping each time. - let mut ids_map = HashSet::new(); - { - let data_channels = self.data_channels.lock().await; - for dc in &*data_channels { - ids_map.insert(dc.id()); - } - } - - let max = self.max_channels(); - while id < max - 1 { - if ids_map.contains(&id) { - id += 2; - } else { - return Ok(id); - } - } - - Err(Error::ErrMaxDataChannelID) - } - - pub(crate) async fn association(&self) -> Option> { - let sctp_association = self.sctp_association.lock().await; - sctp_association.clone() - } - - pub(crate) fn data_channels_accepted(&self) -> u32 { - self.data_channels_accepted.load(Ordering::SeqCst) - } - - pub(crate) fn data_channels_opened(&self) -> u32 { - self.data_channels_opened.load(Ordering::SeqCst) - } - - pub(crate) fn data_channels_requested(&self) -> u32 { - self.data_channels_requested.load(Ordering::SeqCst) - } -} diff --git a/webrtc/src/sctp_transport/sctp_transport_capabilities.rs b/webrtc/src/sctp_transport/sctp_transport_capabilities.rs deleted file mode 100644 index ee4b2a7cc..000000000 --- a/webrtc/src/sctp_transport/sctp_transport_capabilities.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// SCTPTransportCapabilities indicates the capabilities of the SCTPTransport. -#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] -pub struct SCTPTransportCapabilities { - pub max_message_size: u32, -} diff --git a/webrtc/src/sctp_transport/sctp_transport_state.rs b/webrtc/src/sctp_transport/sctp_transport_state.rs deleted file mode 100644 index 310b814c5..000000000 --- a/webrtc/src/sctp_transport/sctp_transport_state.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::fmt; - -/// SCTPTransportState indicates the state of the SCTP transport. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[repr(u8)] -pub enum RTCSctpTransportState { - #[default] - Unspecified, - - /// SCTPTransportStateConnecting indicates the SCTPTransport is in the - /// process of negotiating an association. This is the initial state of the - /// SCTPTransportState when an SCTPTransport is created. - Connecting, - - /// SCTPTransportStateConnected indicates the negotiation of an - /// association is completed. - Connected, - - /// SCTPTransportStateClosed indicates a SHUTDOWN or ABORT chunk is - /// received or when the SCTP association has been closed intentionally, - /// such as by closing the peer connection or applying a remote description - /// that rejects data or changes the SCTP port. - Closed, -} - -const SCTP_TRANSPORT_STATE_CONNECTING_STR: &str = "connecting"; -const SCTP_TRANSPORT_STATE_CONNECTED_STR: &str = "connected"; -const SCTP_TRANSPORT_STATE_CLOSED_STR: &str = "closed"; - -impl From<&str> for RTCSctpTransportState { - fn from(raw: &str) -> Self { - match raw { - SCTP_TRANSPORT_STATE_CONNECTING_STR => RTCSctpTransportState::Connecting, - SCTP_TRANSPORT_STATE_CONNECTED_STR => RTCSctpTransportState::Connected, - SCTP_TRANSPORT_STATE_CLOSED_STR => RTCSctpTransportState::Closed, - _ => RTCSctpTransportState::Unspecified, - } - } -} - -impl From for RTCSctpTransportState { - fn from(v: u8) -> Self { - match v { - 1 => RTCSctpTransportState::Connecting, - 2 => RTCSctpTransportState::Connected, - 3 => RTCSctpTransportState::Closed, - _ => RTCSctpTransportState::Unspecified, - } - } -} - -impl fmt::Display for RTCSctpTransportState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - RTCSctpTransportState::Connecting => SCTP_TRANSPORT_STATE_CONNECTING_STR, - RTCSctpTransportState::Connected => SCTP_TRANSPORT_STATE_CONNECTED_STR, - RTCSctpTransportState::Closed => SCTP_TRANSPORT_STATE_CLOSED_STR, - RTCSctpTransportState::Unspecified => crate::UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_new_sctp_transport_state() { - let tests = vec![ - (crate::UNSPECIFIED_STR, RTCSctpTransportState::Unspecified), - ("connecting", RTCSctpTransportState::Connecting), - ("connected", RTCSctpTransportState::Connected), - ("closed", RTCSctpTransportState::Closed), - ]; - - for (state_string, expected_state) in tests { - assert_eq!( - RTCSctpTransportState::from(state_string), - expected_state, - "testCase: {expected_state}", - ); - } - } - - #[test] - fn test_sctp_transport_state_string() { - let tests = vec![ - (RTCSctpTransportState::Unspecified, crate::UNSPECIFIED_STR), - (RTCSctpTransportState::Connecting, "connecting"), - (RTCSctpTransportState::Connected, "connected"), - (RTCSctpTransportState::Closed, "closed"), - ]; - - for (state, expected_string) in tests { - assert_eq!(state.to_string(), expected_string) - } - } -} diff --git a/webrtc/src/sctp_transport/sctp_transport_test.rs b/webrtc/src/sctp_transport/sctp_transport_test.rs deleted file mode 100644 index 39a14cb80..000000000 --- a/webrtc/src/sctp_transport/sctp_transport_test.rs +++ /dev/null @@ -1,43 +0,0 @@ -use portable_atomic::AtomicU16; - -use super::*; - -#[tokio::test] -async fn test_generate_data_channel_id() -> Result<()> { - let sctp_transport_with_channels = |ids: &[u16]| -> RTCSctpTransport { - let mut data_channels = vec![]; - for id in ids { - data_channels.push(Arc::new(RTCDataChannel { - id: AtomicU16::new(*id), - ..Default::default() - })); - } - - RTCSctpTransport { - data_channels: Arc::new(Mutex::new(data_channels)), - ..Default::default() - } - }; - - let tests = vec![ - (DTLSRole::Client, sctp_transport_with_channels(&[]), 0), - (DTLSRole::Client, sctp_transport_with_channels(&[1]), 0), - (DTLSRole::Client, sctp_transport_with_channels(&[0]), 2), - (DTLSRole::Client, sctp_transport_with_channels(&[0, 2]), 4), - (DTLSRole::Client, sctp_transport_with_channels(&[0, 4]), 2), - (DTLSRole::Server, sctp_transport_with_channels(&[]), 1), - (DTLSRole::Server, sctp_transport_with_channels(&[0]), 1), - (DTLSRole::Server, sctp_transport_with_channels(&[1]), 3), - (DTLSRole::Server, sctp_transport_with_channels(&[1, 3]), 5), - (DTLSRole::Server, sctp_transport_with_channels(&[1, 5]), 3), - ]; - - for (role, s, expected) in tests { - match s.generate_and_set_data_channel_id(role).await { - Ok(actual) => assert_eq!(actual, expected), - Err(err) => panic!("failed to generate id: {err}"), - }; - } - - Ok(()) -} diff --git a/webrtc/src/stats/mod.rs b/webrtc/src/stats/mod.rs deleted file mode 100644 index 928059e8b..000000000 --- a/webrtc/src/stats/mod.rs +++ /dev/null @@ -1,688 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::time::SystemTime; - -use ice::agent::agent_stats::{CandidatePairStats, CandidateStats}; -use ice::agent::Agent; -use ice::candidate::{CandidatePairState, CandidateType}; -use ice::network_type::NetworkType; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use smol_str::SmolStr; -use stats_collector::StatsCollector; -use tokio::time::Instant; - -use crate::data_channel::data_channel_state::RTCDataChannelState; -use crate::data_channel::RTCDataChannel; -use crate::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint; -use crate::peer_connection::certificate::RTCCertificate; -use crate::rtp_transceiver::rtp_codec::RTCRtpCodecParameters; -use crate::rtp_transceiver::{PayloadType, SSRC}; -use crate::sctp_transport::RTCSctpTransport; - -mod serialize; -pub mod stats_collector; - -#[derive(Debug, Serialize, Deserialize)] -pub enum RTCStatsType { - #[serde(rename = "candidate-pair")] - CandidatePair, - #[serde(rename = "certificate")] - Certificate, - #[serde(rename = "codec")] - Codec, - #[serde(rename = "csrc")] - CSRC, - #[serde(rename = "data-channel")] - DataChannel, - #[serde(rename = "inbound-rtp")] - InboundRTP, - #[serde(rename = "local-candidate")] - LocalCandidate, - #[serde(rename = "outbound-rtp")] - OutboundRTP, - #[serde(rename = "peer-connection")] - PeerConnection, - #[serde(rename = "receiver")] - Receiver, - #[serde(rename = "remote-candidate")] - RemoteCandidate, - #[serde(rename = "remote-inbound-rtp")] - RemoteInboundRTP, - #[serde(rename = "remote-outbound-rtp")] - RemoteOutboundRTP, - #[serde(rename = "sender")] - Sender, - #[serde(rename = "transport")] - Transport, -} - -pub enum SourceStatsType { - LocalCandidate(CandidateStats), - RemoteCandidate(CandidateStats), -} - -#[derive(Debug)] -pub enum StatsReportType { - CandidatePair(ICECandidatePairStats), - CertificateStats(CertificateStats), - Codec(CodecStats), - DataChannel(DataChannelStats), - LocalCandidate(ICECandidateStats), - PeerConnection(PeerConnectionStats), - RemoteCandidate(ICECandidateStats), - SCTPTransport(ICETransportStats), - Transport(ICETransportStats), - InboundRTP(InboundRTPStats), - OutboundRTP(OutboundRTPStats), - RemoteInboundRTP(RemoteInboundRTPStats), - RemoteOutboundRTP(RemoteOutboundRTPStats), -} - -impl From for StatsReportType { - fn from(stats: SourceStatsType) -> Self { - match stats { - SourceStatsType::LocalCandidate(stats) => StatsReportType::LocalCandidate( - ICECandidateStats::new(stats, RTCStatsType::LocalCandidate), - ), - SourceStatsType::RemoteCandidate(stats) => StatsReportType::RemoteCandidate( - ICECandidateStats::new(stats, RTCStatsType::RemoteCandidate), - ), - } - } -} - -impl Serialize for StatsReportType { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - StatsReportType::CandidatePair(stats) => stats.serialize(serializer), - StatsReportType::CertificateStats(stats) => stats.serialize(serializer), - StatsReportType::Codec(stats) => stats.serialize(serializer), - StatsReportType::DataChannel(stats) => stats.serialize(serializer), - StatsReportType::LocalCandidate(stats) => stats.serialize(serializer), - StatsReportType::PeerConnection(stats) => stats.serialize(serializer), - StatsReportType::RemoteCandidate(stats) => stats.serialize(serializer), - StatsReportType::SCTPTransport(stats) => stats.serialize(serializer), - StatsReportType::Transport(stats) => stats.serialize(serializer), - StatsReportType::InboundRTP(stats) => stats.serialize(serializer), - StatsReportType::OutboundRTP(stats) => stats.serialize(serializer), - StatsReportType::RemoteInboundRTP(stats) => stats.serialize(serializer), - StatsReportType::RemoteOutboundRTP(stats) => stats.serialize(serializer), - } - } -} - -impl<'de> Deserialize<'de> for StatsReportType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = serde_json::Value::deserialize(deserializer)?; - let type_field = value - .get("type") - .ok_or_else(|| serde::de::Error::missing_field("type"))?; - let rtc_type: RTCStatsType = serde_json::from_value(type_field.clone()).map_err(|e| { - serde::de::Error::custom(format!( - "failed to deserialize RTCStatsType from the `type` field ({}): {}", - type_field, e - )) - })?; - - match rtc_type { - RTCStatsType::CandidatePair => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::CandidatePair(stats)) - } - RTCStatsType::Certificate => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::CertificateStats(stats)) - } - RTCStatsType::Codec => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::Codec(stats)) - } - RTCStatsType::CSRC => { - todo!() - } - RTCStatsType::DataChannel => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::DataChannel(stats)) - } - RTCStatsType::InboundRTP => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::InboundRTP(stats)) - } - RTCStatsType::LocalCandidate => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::LocalCandidate(stats)) - } - RTCStatsType::OutboundRTP => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::OutboundRTP(stats)) - } - RTCStatsType::PeerConnection => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::PeerConnection(stats)) - } - RTCStatsType::Receiver => { - todo!() - } - RTCStatsType::RemoteCandidate => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::RemoteCandidate(stats)) - } - RTCStatsType::RemoteInboundRTP => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::RemoteInboundRTP(stats)) - } - RTCStatsType::RemoteOutboundRTP => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::RemoteOutboundRTP(stats)) - } - RTCStatsType::Sender => { - todo!() - } - RTCStatsType::Transport => { - let stats = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(StatsReportType::Transport(stats)) - } - } - } -} - -#[derive(Debug)] -pub struct StatsReport { - pub reports: HashMap, -} - -impl From for StatsReport { - fn from(collector: StatsCollector) -> Self { - StatsReport { - reports: collector.into_reports(), - } - } -} - -impl Serialize for StatsReport { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.reports.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for StatsReport { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = serde_json::Value::deserialize(deserializer)?; - let root = value - .as_object() - .ok_or(serde::de::Error::custom("root object missing"))?; - - let mut reports = HashMap::new(); - for (key, value) in root { - let report = serde_json::from_value(value.clone()).map_err(|e| { - serde::de::Error::custom(format!( - "failed to deserialize `StatsReportType` from key={}, value={}: {}", - key, value, e - )) - })?; - reports.insert(key.clone(), report); - } - Ok(Self { reports }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ICECandidatePairStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCIceCandidatePairStats - // TODO: Add `transportId` - pub local_candidate_id: String, - pub remote_candidate_id: String, - pub state: CandidatePairState, - pub nominated: bool, - pub packets_sent: u32, - pub packets_received: u32, - pub bytes_sent: u64, - pub bytes_received: u64, - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub last_packet_sent_timestamp: Instant, - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub last_packet_received_timestamp: Instant, - pub total_round_trip_time: f64, - pub current_round_trip_time: f64, - pub available_outgoing_bitrate: f64, - pub available_incoming_bitrate: f64, - pub requests_received: u64, - pub requests_sent: u64, - pub responses_received: u64, - pub responses_sent: u64, - pub consent_requests_sent: u64, - // TODO: Add `packetsDiscardedOnSend` - // TODO: Add `bytesDiscardedOnSend` - - // Non-canon - pub circuit_breaker_trigger_count: u32, - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub consent_expired_timestamp: Instant, - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub first_request_timestamp: Instant, - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub last_request_timestamp: Instant, - pub retransmissions_sent: u64, -} - -impl From for ICECandidatePairStats { - fn from(stats: CandidatePairStats) -> Self { - ICECandidatePairStats { - available_incoming_bitrate: stats.available_incoming_bitrate, - available_outgoing_bitrate: stats.available_outgoing_bitrate, - bytes_received: stats.bytes_received, - bytes_sent: stats.bytes_sent, - circuit_breaker_trigger_count: stats.circuit_breaker_trigger_count, - consent_expired_timestamp: stats.consent_expired_timestamp, - consent_requests_sent: stats.consent_requests_sent, - current_round_trip_time: stats.current_round_trip_time, - first_request_timestamp: stats.first_request_timestamp, - id: format!("{}-{}", stats.local_candidate_id, stats.remote_candidate_id), - last_packet_received_timestamp: stats.last_packet_received_timestamp, - last_packet_sent_timestamp: stats.last_packet_sent_timestamp, - last_request_timestamp: stats.last_request_timestamp, - local_candidate_id: stats.local_candidate_id, - nominated: stats.nominated, - packets_received: stats.packets_received, - packets_sent: stats.packets_sent, - remote_candidate_id: stats.remote_candidate_id, - requests_received: stats.requests_received, - requests_sent: stats.requests_sent, - responses_received: stats.responses_received, - responses_sent: stats.responses_sent, - retransmissions_sent: stats.retransmissions_sent, - state: stats.state, - stats_type: RTCStatsType::CandidatePair, - timestamp: stats.timestamp, - total_round_trip_time: stats.total_round_trip_time, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ICECandidateStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCIceCandidateStats - pub candidate_type: CandidateType, - pub deleted: bool, - pub ip: String, - pub network_type: NetworkType, - pub port: u16, - pub priority: u32, - pub relay_protocol: String, - pub url: String, -} - -impl ICECandidateStats { - fn new(stats: CandidateStats, stats_type: RTCStatsType) -> Self { - ICECandidateStats { - candidate_type: stats.candidate_type, - deleted: stats.deleted, - id: stats.id, - ip: stats.ip, - network_type: stats.network_type, - port: stats.port, - priority: stats.priority, - relay_protocol: stats.relay_protocol, - stats_type, - timestamp: stats.timestamp, - url: stats.url, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ICETransportStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // Non-canon - pub bytes_received: usize, - pub bytes_sent: usize, -} - -impl ICETransportStats { - pub(crate) fn new(id: String, agent: Arc) -> Self { - ICETransportStats { - id, - bytes_received: agent.get_bytes_received(), - bytes_sent: agent.get_bytes_sent(), - stats_type: RTCStatsType::Transport, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CertificateStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCCertificateStats - pub fingerprint: String, - pub fingerprint_algorithm: String, - // TODO: Add `base64Certificate` and `issuerCertificateId`. -} - -impl CertificateStats { - pub(crate) fn new(cert: &RTCCertificate, fingerprint: RTCDtlsFingerprint) -> Self { - CertificateStats { - // TODO: base64_certificate - fingerprint: fingerprint.value, - fingerprint_algorithm: fingerprint.algorithm, - id: cert.stats_id.clone(), - // TODO: issuer_certificate_id - stats_type: RTCStatsType::Certificate, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CodecStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCCodecStats - pub payload_type: PayloadType, - pub mime_type: String, - pub channels: u16, - pub clock_rate: u32, - pub sdp_fmtp_line: String, - // TODO: Add `transportId` -} - -impl From<&RTCRtpCodecParameters> for CodecStats { - fn from(codec: &RTCRtpCodecParameters) -> Self { - CodecStats { - channels: codec.capability.channels, - clock_rate: codec.capability.clock_rate, - id: codec.stats_id.clone(), - mime_type: codec.capability.mime_type.clone(), - payload_type: codec.payload_type, - sdp_fmtp_line: codec.capability.sdp_fmtp_line.clone(), - stats_type: RTCStatsType::Codec, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DataChannelStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCDataChannelStats - pub bytes_received: usize, - pub bytes_sent: usize, - pub data_channel_identifier: u16, - pub label: String, - pub messages_received: usize, - pub messages_sent: usize, - pub protocol: String, - pub state: RTCDataChannelState, -} - -impl DataChannelStats { - pub(crate) async fn from(data_channel: &RTCDataChannel) -> Self { - let state = data_channel.ready_state(); - - let mut bytes_received = 0; - let mut bytes_sent = 0; - let mut messages_received = 0; - let mut messages_sent = 0; - - let lock = data_channel.data_channel.lock().await; - - if let Some(internal) = &*lock { - bytes_received = internal.bytes_received(); - bytes_sent = internal.bytes_sent(); - messages_received = internal.messages_received(); - messages_sent = internal.messages_sent(); - } - - Self { - bytes_received, - bytes_sent, - data_channel_identifier: data_channel.id(), // TODO: "The value is initially null" - id: data_channel.stats_id.clone(), - label: data_channel.label.clone(), - messages_received, - messages_sent, - protocol: data_channel.protocol.clone(), - state, - stats_type: RTCStatsType::DataChannel, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PeerConnectionStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCPeerConnectionStats - pub data_channels_closed: u32, - pub data_channels_opened: u32, - - // Non-canon - pub data_channels_accepted: u32, - pub data_channels_requested: u32, -} - -impl PeerConnectionStats { - pub fn new(transport: &RTCSctpTransport, stats_id: String, data_channels_closed: u32) -> Self { - PeerConnectionStats { - data_channels_accepted: transport.data_channels_accepted(), - data_channels_closed, - data_channels_opened: transport.data_channels_opened(), - data_channels_requested: transport.data_channels_requested(), - id: stats_id, - stats_type: RTCStatsType::PeerConnection, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InboundRTPStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCRtpStreamStats - pub ssrc: SSRC, - pub kind: String, // Either "video" or "audio" - // TODO: Add transportId - // TODO: Add codecId - - // RTCReceivedRtpStreamStats - pub packets_received: u64, - // TODO: packetsLost - // TODO: jitter(maybe, might be uattainable for the same reason as `framesDropped`) - // NB: `framesDropped` can't be produced since we aren't decoding, might be worth introducing a - // way for consumers to control this in the future. - - // RTCInboundRtpStreamStats - pub track_identifier: String, - pub mid: SmolStr, - // TODO: `remoteId` - // NB: `framesDecoded`, `frameWidth`, frameHeight`, `framesPerSecond`, `qpSum`, - // `totalDecodeTime`, `totalInterFrameDelay`, and `totalSquaredInterFrameDelay` are all decoder - // specific values and can't be produced since we aren't decoding. - pub last_packet_received_timestamp: Option, - pub header_bytes_received: u64, - // TODO: `packetsDiscarded`. This value only makes sense if we have jitter buffer, which we - // cannot assume. - // TODO: `fecPacketsReceived`, `fecPacketsDiscarded` - pub bytes_received: u64, - pub nack_count: u64, - pub fir_count: Option, - pub pli_count: Option, - // NB: `totalProcessingDelay`, `estimatedPlayoutTimestamp`, `jitterBufferDelay`, - // `jitterBufferTargetDelay`, `jitterBufferEmittedCount`, `jitterBufferMinimumDelay`, - // `totalSamplesReceived`, `concealedSamples`, `silentConcealedSamples`, `concealmentEvents`, - // `insertedSamplesForDeceleration`, `removedSamplesForAcceleration`, `audioLevel`, - // `totalAudioEneregy`, `totalSampleDuration`, `framesReceived, and `decoderImplementation` are - // all decoder specific and can't be produced since we aren't decoding. -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OutboundRTPStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCRtpStreamStats - pub ssrc: SSRC, - pub kind: String, // Either "video" or "audio" - // TODO: Add transportId - // TODO: Add codecId - - // RTCSentRtpStreamStats - pub packets_sent: u64, - pub bytes_sent: u64, - - // RTCOutboundRtpStreamStats - // NB: non-canon in browsers this is available via `RTCMediaSourceStats` which we are unlikely to implement - pub track_identifier: String, - pub mid: SmolStr, - // TODO: `mediaSourceId` and `remoteId` - pub rid: Option, - pub header_bytes_sent: u64, - // TODO: `retransmittedPacketsSent` and `retransmittedPacketsSent` - // NB: `targetBitrate`, `totalEncodedBytesTarget`, `frameWidth` `frameHeight`, `framesPerSecond`, `framesSent`, - // `hugeFramesSent`, `framesEncoded`, `keyFramesEncoded`, `qpSum`, and `totalEncodeTime` are - // all encoder specific and can't be produced snce we aren't encoding. - // TODO: `totalPacketSendDelay` time from `TrackLocalWriter::write_rtp` to being written to - // socket. - - // NB: `qualityLimitationReason`, `qualityLimitationDurations`, and `qualityLimitationResolutionChanges` are all - // encoder specific and can't be produced since we aren't encoding. - pub nack_count: u64, - pub fir_count: Option, - pub pli_count: Option, - // NB: `encoderImplementation` is encoder specific and can't be produced since we aren't - // encoding. -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoteInboundRTPStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCRtpStreamStats - pub ssrc: SSRC, - pub kind: String, // Either "video" or "audio" - // TODO: Add transportId - // TODO: Add codecId - - // RTCReceivedRtpStreamStats - pub packets_received: u64, - pub packets_lost: i64, - // TODO: jitter(maybe, might be uattainable for the same reason as `framesDropped`) - // NB: `framesDropped` can't be produced since we aren't decoding, might be worth introducing a - // way for consumers to control this in the future. - - // RTCRemoteInboundRtpStreamStats - pub local_id: String, - pub round_trip_time: Option, - pub total_round_trip_time: f64, - pub fraction_lost: f64, - pub round_trip_time_measurements: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoteOutboundRTPStats { - // RTCStats - #[serde(with = "serialize::instant_to_epoch_seconds")] - pub timestamp: Instant, - #[serde(rename = "type")] - pub stats_type: RTCStatsType, - pub id: String, - - // RTCRtpStreamStats - pub ssrc: SSRC, - pub kind: String, // Either "video" or "audio" - // TODO: Add transportId - // TODO: Add codecId - - // RTCSentRtpStreamStats - pub packets_sent: u64, - pub bytes_sent: u64, - - // RTCRemoteOutboundRtpStreamStats - pub local_id: String, - // TODO: `remote_timestamp` - pub round_trip_time: Option, - pub reports_sent: u64, - pub total_round_trip_time: f64, - pub round_trip_time_measurements: u64, -} diff --git a/webrtc/src/stats/serialize.rs b/webrtc/src/stats/serialize.rs deleted file mode 100644 index b1abd394b..000000000 --- a/webrtc/src/stats/serialize.rs +++ /dev/null @@ -1,50 +0,0 @@ -/// Serializes a `tokio::time::Instant` to an approximation of epoch time in the form -/// of an `f64` where the integer portion is seconds and the decimal portion is milliseconds. -/// For instance, `Monday, May 30, 2022 10:45:26.456 PM UTC` converts to `1653950726.456`. -/// -/// Note that an `Instant` is not connected to real world time, so this conversion is -/// approximate. -pub mod instant_to_epoch_seconds { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; - use tokio::time::Instant; - - pub fn serialize(instant: &Instant, serializer: S) -> Result - where - S: Serializer, - { - let system_now = SystemTime::now(); - let instant_now = Instant::now(); - let approx = system_now - (instant_now - *instant); - let epoch = approx - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - - let epoch_ms = epoch.as_millis() as f64 / 1000.0; - - epoch_ms.serialize(serializer) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let epoch_seconds: f64 = Deserialize::deserialize(deserializer)?; - - let since_epoch = Duration::from_secs_f64(epoch_seconds); - - let system_now = SystemTime::now(); - let instant_now = Instant::now(); - - let deserialized_system_time = UNIX_EPOCH + since_epoch; - - let adjustment = match deserialized_system_time.duration_since(system_now) { - Ok(duration) => -duration.as_secs_f64(), - Err(e) => e.duration().as_secs_f64(), - }; - - let adjusted_instant = instant_now + Duration::from_secs_f64(adjustment); - - Ok(adjusted_instant) - } -} diff --git a/webrtc/src/stats/stats_collector.rs b/webrtc/src/stats/stats_collector.rs deleted file mode 100644 index e0228dfe9..000000000 --- a/webrtc/src/stats/stats_collector.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::collections::HashMap; - -use util::sync::Mutex; - -use super::StatsReportType; - -#[derive(Debug, Default)] -pub struct StatsCollector { - pub(crate) reports: Mutex>, -} - -impl StatsCollector { - pub(crate) fn new() -> Self { - StatsCollector { - ..Default::default() - } - } - - pub(crate) fn insert(&self, id: String, stats: StatsReportType) { - let mut reports = self.reports.lock(); - reports.insert(id, stats); - } - - pub(crate) fn merge(&self, stats: HashMap) { - let mut reports = self.reports.lock(); - reports.extend(stats) - } - - pub(crate) fn into_reports(self) -> HashMap { - self.reports.into_inner() - } -} diff --git a/webrtc/src/track/mod.rs b/webrtc/src/track/mod.rs deleted file mode 100644 index 8c0a2d5be..000000000 --- a/webrtc/src/track/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub mod track_local; -pub mod track_remote; - -use std::sync::Arc; - -use interceptor::stream_info::StreamInfo; -use interceptor::{RTCPReader, RTPReader}; -use track_remote::*; - -pub(crate) const RTP_OUTBOUND_MTU: usize = 1200; -pub(crate) const RTP_PAYLOAD_TYPE_BITMASK: u8 = 0x7F; - -#[derive(Clone)] -pub(crate) struct TrackStream { - pub(crate) stream_info: Option, - pub(crate) rtp_read_stream: Option>, - pub(crate) rtp_interceptor: Option>, - pub(crate) rtcp_read_stream: Option>, - pub(crate) rtcp_interceptor: Option>, -} - -/// TrackStreams maintains a mapping of RTP/RTCP streams to a specific track -/// a RTPReceiver may contain multiple streams if we are dealing with Simulcast -#[derive(Clone)] -pub(crate) struct TrackStreams { - pub(crate) track: Arc, - pub(crate) stream: TrackStream, - pub(crate) repair_stream: TrackStream, -} diff --git a/webrtc/src/track/track_local/mod.rs b/webrtc/src/track/track_local/mod.rs deleted file mode 100644 index fcfd8bda7..000000000 --- a/webrtc/src/track/track_local/mod.rs +++ /dev/null @@ -1,170 +0,0 @@ -#[cfg(test)] -mod track_local_static_test; - -pub mod track_local_static_rtp; -pub mod track_local_static_sample; - -use std::any::Any; -use std::fmt; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use async_trait::async_trait; -use interceptor::{Attributes, RTPWriter}; -use portable_atomic::AtomicBool; -use smol_str::SmolStr; -use tokio::sync::Mutex; -use util::Unmarshal; - -use crate::error::{Error, Result}; -use crate::rtp_transceiver::rtp_codec::*; -use crate::rtp_transceiver::*; - -/// TrackLocalWriter is the Writer for outbound RTP Packets -#[async_trait] -pub trait TrackLocalWriter: fmt::Debug { - /// write_rtp encrypts a RTP packet and writes to the connection - async fn write_rtp(&self, p: &rtp::packet::Packet) -> Result; - - /// write encrypts and writes a full RTP packet - async fn write(&self, b: &[u8]) -> Result; -} - -/// TrackLocalContext is the Context passed when a TrackLocal has been Binded/Unbinded from a PeerConnection, and used -/// in Interceptors. -#[derive(Default, Debug, Clone)] -pub struct TrackLocalContext { - pub(crate) id: String, - pub(crate) params: RTCRtpParameters, - pub(crate) ssrc: SSRC, - pub(crate) write_stream: Option>, - pub(crate) paused: Arc, - pub(crate) mid: Option, -} - -impl TrackLocalContext { - /// codec_parameters returns the negotiated RTPCodecParameters. These are the codecs supported by both - /// PeerConnections and the SSRC/PayloadTypes - pub fn codec_parameters(&self) -> &[RTCRtpCodecParameters] { - &self.params.codecs - } - - /// header_extensions returns the negotiated RTPHeaderExtensionParameters. These are the header extensions supported by - /// both PeerConnections and the SSRC/PayloadTypes - pub fn header_extensions(&self) -> &[RTCRtpHeaderExtensionParameters] { - &self.params.header_extensions - } - - /// ssrc requires the negotiated SSRC of this track - /// This track may have multiple if RTX is enabled - pub fn ssrc(&self) -> SSRC { - self.ssrc - } - - /// write_stream returns the write_stream for this TrackLocal. The implementer writes the outbound - /// media packets to it - pub fn write_stream(&self) -> Option> { - self.write_stream.clone() - } - - /// id is a unique identifier that is used for both bind/unbind - pub fn id(&self) -> String { - self.id.clone() - } -} -/// TrackLocal is an interface that controls how the user can send media -/// The user can provide their own TrackLocal implementations, or use -/// the implementations in pkg/media -#[async_trait] -pub trait TrackLocal { - /// bind should implement the way how the media data flows from the Track to the PeerConnection - /// This will be called internally after signaling is complete and the list of available - /// codecs has been determined - async fn bind(&self, t: &TrackLocalContext) -> Result; - - /// unbind should implement the teardown logic when the track is no longer needed. This happens - /// because a track has been stopped. - async fn unbind(&self, t: &TrackLocalContext) -> Result<()>; - - /// id is the unique identifier for this Track. This should be unique for the - /// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' - /// and stream_id would be 'desktop' or 'webcam' - fn id(&self) -> &str; - - /// RID is the RTP Stream ID for this track. - fn rid(&self) -> Option<&str>; - - /// stream_id is the group this track belongs too. This must be unique - fn stream_id(&self) -> &str; - - /// kind controls if this TrackLocal is audio or video - fn kind(&self) -> RTPCodecType; - - fn as_any(&self) -> &dyn Any; -} - -/// TrackBinding is a single bind for a Track -/// Bind can be called multiple times, this stores the -/// result for a single bind call so that it can be used when writing -#[derive(Default, Debug)] -pub(crate) struct TrackBinding { - id: String, - ssrc: SSRC, - payload_type: PayloadType, - params: RTCRtpParameters, - write_stream: Option>, - sender_paused: Arc, - hdr_ext_ids: Vec, -} - -impl TrackBinding { - pub fn is_sender_paused(&self) -> bool { - self.sender_paused.load(Ordering::SeqCst) - } -} - -pub(crate) struct InterceptorToTrackLocalWriter { - pub(crate) interceptor_rtp_writer: Mutex>>, - sender_paused: Arc, -} - -impl InterceptorToTrackLocalWriter { - pub(crate) fn new(paused: Arc) -> Self { - InterceptorToTrackLocalWriter { - interceptor_rtp_writer: Mutex::new(None), - sender_paused: paused, - } - } - - fn is_sender_paused(&self) -> bool { - self.sender_paused.load(Ordering::SeqCst) - } -} - -impl std::fmt::Debug for InterceptorToTrackLocalWriter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("InterceptorToTrackLocalWriter").finish() - } -} - -#[async_trait] -impl TrackLocalWriter for InterceptorToTrackLocalWriter { - async fn write_rtp(&self, pkt: &rtp::packet::Packet) -> Result { - if self.is_sender_paused() { - return Ok(0); - } - - let interceptor_rtp_writer = self.interceptor_rtp_writer.lock().await; - if let Some(writer) = &*interceptor_rtp_writer { - let a = Attributes::new(); - Ok(writer.write(pkt, &a).await?) - } else { - Ok(0) - } - } - - async fn write(&self, mut b: &[u8]) -> Result { - let pkt = rtp::packet::Packet::unmarshal(&mut b)?; - self.write_rtp(&pkt).await - } -} diff --git a/webrtc/src/track/track_local/track_local_static_rtp.rs b/webrtc/src/track/track_local/track_local_static_rtp.rs deleted file mode 100644 index 18cdaed7e..000000000 --- a/webrtc/src/track/track_local/track_local_static_rtp.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::collections::HashMap; - -use bytes::{Bytes, BytesMut}; -use tokio::sync::Mutex; -use util::{Marshal, MarshalSize}; - -use super::*; -use crate::error::flatten_errs; - -/// TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. -/// If you wish to send a media.Sample use TrackLocalStaticSample -#[derive(Debug)] -pub struct TrackLocalStaticRTP { - pub(crate) bindings: Mutex>>, - codec: RTCRtpCodecCapability, - id: String, - rid: Option, - stream_id: String, -} - -impl TrackLocalStaticRTP { - /// returns a TrackLocalStaticRTP without rid. - pub fn new(codec: RTCRtpCodecCapability, id: String, stream_id: String) -> Self { - TrackLocalStaticRTP { - codec, - bindings: Mutex::new(vec![]), - id, - rid: None, - stream_id, - } - } - - /// returns a TrackLocalStaticRTP with rid. - pub fn new_with_rid( - codec: RTCRtpCodecCapability, - id: String, - rid: String, - stream_id: String, - ) -> Self { - TrackLocalStaticRTP { - codec, - bindings: Mutex::new(vec![]), - id, - rid: Some(rid), - stream_id, - } - } - - /// codec gets the Codec of the track - pub fn codec(&self) -> RTCRtpCodecCapability { - self.codec.clone() - } - - pub async fn any_binding_paused(&self) -> bool { - let bindings = self.bindings.lock().await; - bindings - .iter() - .any(|b| b.sender_paused.load(Ordering::SeqCst)) - } - - pub async fn all_binding_paused(&self) -> bool { - let bindings = self.bindings.lock().await; - bindings - .iter() - .all(|b| b.sender_paused.load(Ordering::SeqCst)) - } - - /// write_rtp_with_extensions writes a RTP Packet to the TrackLocalStaticRTP - /// If one PeerConnection fails the packets will still be sent to - /// all PeerConnections. The error message will contain the ID of the failed - /// PeerConnections so you can remove them - /// - /// If the RTCRtpSender direction is such that no packets should be sent, any call to this - /// function are blocked internally. Care must be taken to not increase the sequence number - /// while the sender is paused. While the actual _sending_ is blocked, the receiver will - /// miss out when the sequence number "rolls over", which in turn will break SRTP. - /// - /// Extensions that are already configured on the packet are overwritten by extensions in - /// `extensions`. - pub async fn write_rtp_with_extensions( - &self, - p: &rtp::packet::Packet, - extensions: &[rtp::extension::HeaderExtension], - ) -> Result { - let mut n = 0; - let mut write_errs = vec![]; - let mut pkt = p.clone(); - - let bindings = { - let bindings = self.bindings.lock().await; - bindings.clone() - }; - // Prepare the extensions data - let extension_data: HashMap<_, _> = extensions - .iter() - .flat_map(|extension| { - let buf = { - let mut buf = BytesMut::with_capacity(extension.marshal_size()); - buf.resize(extension.marshal_size(), 0); - if let Err(err) = extension.marshal_to(&mut buf) { - write_errs.push(Error::Util(err)); - return None; - } - - buf.freeze() - }; - - Some((extension.uri(), buf)) - }) - .collect(); - - for b in bindings.into_iter() { - if b.is_sender_paused() { - // See caveat in function doc. - continue; - } - pkt.header.ssrc = b.ssrc; - pkt.header.payload_type = b.payload_type; - - for ext in b.hdr_ext_ids.iter() { - let payload = ext.payload.to_owned(); - if let Err(err) = pkt.header.set_extension(ext.id, payload) { - write_errs.push(Error::Rtp(err)); - } - } - - for (uri, data) in extension_data.iter() { - if let Some(id) = b - .params - .header_extensions - .iter() - .find(|ext| &ext.uri == uri) - .map(|ext| ext.id) - { - if let Err(err) = pkt.header.set_extension(id as u8, data.clone()) { - write_errs.push(Error::Rtp(err)); - continue; - } - } - } - - if let Some(write_stream) = &b.write_stream { - match write_stream.write_rtp(&pkt).await { - Ok(m) => { - n += m; - } - Err(err) => { - write_errs.push(err); - } - } - } else { - write_errs.push(Error::new("track binding has none write_stream".to_owned())); - } - } - - flatten_errs(write_errs)?; - Ok(n) - } -} - -#[async_trait] -impl TrackLocal for TrackLocalStaticRTP { - /// bind is called by the PeerConnection after negotiation is complete - /// This asserts that the code requested is supported by the remote peer. - /// If so it setups all the state (SSRC and PayloadType) to have a call - async fn bind(&self, t: &TrackLocalContext) -> Result { - let parameters = RTCRtpCodecParameters { - capability: self.codec.clone(), - ..Default::default() - }; - let mut hdr_ext_ids = vec![]; - if let Some(id) = t - .header_extensions() - .iter() - .find(|e| e.uri == ::sdp::extmap::SDES_MID_URI) - .map(|e| e.id as u8) - { - if let Some(payload) = t - .mid - .as_ref() - .map(|mid| Bytes::copy_from_slice(mid.as_bytes())) - { - hdr_ext_ids.push(rtp::header::Extension { id, payload }); - } - } - - if let Some(id) = t - .header_extensions() - .iter() - .find(|e| e.uri == ::sdp::extmap::SDES_RTP_STREAM_ID_URI) - .map(|e| e.id as u8) - { - if let Some(payload) = self.rid().map(|rid| rid.to_owned().into()) { - hdr_ext_ids.push(rtp::header::Extension { id, payload }); - } - } - - let (codec, match_type) = codec_parameters_fuzzy_search(¶meters, t.codec_parameters()); - if match_type != CodecMatch::None { - { - let mut bindings = self.bindings.lock().await; - bindings.push(Arc::new(TrackBinding { - id: t.id(), - ssrc: t.ssrc(), - payload_type: codec.payload_type, - params: t.params.clone(), - write_stream: t.write_stream(), - sender_paused: t.paused.clone(), - hdr_ext_ids, - })); - } - - Ok(codec) - } else { - Err(Error::ErrUnsupportedCodec) - } - } - - /// unbind implements the teardown logic when the track is no longer needed. This happens - /// because a track has been stopped. - async fn unbind(&self, t: &TrackLocalContext) -> Result<()> { - let mut bindings = self.bindings.lock().await; - let mut idx = None; - for (index, binding) in bindings.iter().enumerate() { - if binding.id == t.id() { - idx = Some(index); - break; - } - } - if let Some(index) = idx { - bindings.remove(index); - Ok(()) - } else { - Err(Error::ErrUnbindFailed) - } - } - - /// id is the unique identifier for this Track. This should be unique for the - /// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' - /// and StreamID would be 'desktop' or 'webcam' - fn id(&self) -> &str { - self.id.as_str() - } - - /// RID is the RTP Stream ID for this track. - fn rid(&self) -> Option<&str> { - self.rid.as_deref() - } - - /// stream_id is the group this track belongs too. This must be unique - fn stream_id(&self) -> &str { - self.stream_id.as_str() - } - - /// kind controls if this TrackLocal is audio or video - fn kind(&self) -> RTPCodecType { - if self.codec.mime_type.starts_with("audio/") { - RTPCodecType::Audio - } else if self.codec.mime_type.starts_with("video/") { - RTPCodecType::Video - } else { - RTPCodecType::Unspecified - } - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -#[async_trait] -impl TrackLocalWriter for TrackLocalStaticRTP { - /// write_rtp writes a RTP Packet to the TrackLocalStaticRTP - /// If one PeerConnection fails the packets will still be sent to - /// all PeerConnections. The error message will contain the ID of the failed - /// PeerConnections so you can remove them - /// - /// If the RTCRtpSender direction is such that no packets should be sent, any call to this - /// function are blocked internally. Care must be taken to not increase the sequence number - /// while the sender is paused. While the actual _sending_ is blocked, the receiver will - /// miss out when the sequence number "rolls over", which in turn will break SRTP. - async fn write_rtp(&self, p: &rtp::packet::Packet) -> Result { - self.write_rtp_with_extensions(p, &[]).await - } - - /// write writes a RTP Packet as a buffer to the TrackLocalStaticRTP - /// If one PeerConnection fails the packets will still be sent to - /// all PeerConnections. The error message will contain the ID of the failed - /// PeerConnections so you can remove them - async fn write(&self, mut b: &[u8]) -> Result { - let pkt = rtp::packet::Packet::unmarshal(&mut b)?; - self.write_rtp(&pkt).await?; - Ok(b.len()) - } -} diff --git a/webrtc/src/track/track_local/track_local_static_sample.rs b/webrtc/src/track/track_local/track_local_static_sample.rs deleted file mode 100644 index 2c4d5166e..000000000 --- a/webrtc/src/track/track_local/track_local_static_sample.rs +++ /dev/null @@ -1,324 +0,0 @@ -use log::warn; -use media::Sample; -use tokio::sync::Mutex; - -use super::track_local_static_rtp::TrackLocalStaticRTP; -use super::*; -use crate::error::flatten_errs; -use crate::track::RTP_OUTBOUND_MTU; - -#[derive(Debug, Clone)] -struct TrackLocalStaticSampleInternal { - packetizer: Option>, - sequencer: Option>, - clock_rate: f64, - did_warn_about_wonky_pause: bool, -} - -/// TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples. -/// If you wish to send a RTP Packet use TrackLocalStaticRTP -#[derive(Debug)] -pub struct TrackLocalStaticSample { - rtp_track: TrackLocalStaticRTP, - internal: Mutex, -} - -impl TrackLocalStaticSample { - /// returns a TrackLocalStaticSample without RID - pub fn new(codec: RTCRtpCodecCapability, id: String, stream_id: String) -> Self { - let rtp_track = TrackLocalStaticRTP::new(codec, id, stream_id); - - TrackLocalStaticSample { - rtp_track, - internal: Mutex::new(TrackLocalStaticSampleInternal { - packetizer: None, - sequencer: None, - clock_rate: 0.0f64, - did_warn_about_wonky_pause: false, - }), - } - } - - /// returns a TrackLocalStaticSample with RID - pub fn new_with_rid( - codec: RTCRtpCodecCapability, - id: String, - rid: String, - stream_id: String, - ) -> Self { - let rtp_track = TrackLocalStaticRTP::new_with_rid(codec, id, rid, stream_id); - - TrackLocalStaticSample { - rtp_track, - internal: Mutex::new(TrackLocalStaticSampleInternal { - packetizer: None, - sequencer: None, - clock_rate: 0.0f64, - did_warn_about_wonky_pause: false, - }), - } - } - - /// codec gets the Codec of the track - pub fn codec(&self) -> RTCRtpCodecCapability { - self.rtp_track.codec() - } - - /// write_sample writes a Sample to the TrackLocalStaticSample - /// If one PeerConnection fails the packets will still be sent to - /// all PeerConnections. The error message will contain the ID of the failed - /// PeerConnections so you can remove them - pub async fn write_sample(&self, sample: &Sample) -> Result<()> { - self.write_sample_with_extensions(sample, &[]).await - } - - /// Write a sample with provided RTP extensions. - /// - /// Alternatively to this method [`TrackLocalStaticSample::sample_writer`] can be used instead. - /// - /// See [`TrackLocalStaticSample::write_sample`] for further details. - pub async fn write_sample_with_extensions( - &self, - sample: &Sample, - extensions: &[rtp::extension::HeaderExtension], - ) -> Result<()> { - let mut internal = self.internal.lock().await; - - if internal.packetizer.is_none() || internal.sequencer.is_none() { - return Ok(()); - } - - let (any_paused, all_paused) = ( - self.rtp_track.any_binding_paused().await, - self.rtp_track.all_binding_paused().await, - ); - - if all_paused { - // Abort already here to not increment sequence numbers. - return Ok(()); - } - - if any_paused { - // This is a problem state due to how this impl is structured. The sequencer will allocate - // one sequence number per RTP packet regardless of how many TrackBinding that will send - // the packet. I.e. we get the same sequence number per multiple SSRC, which is not good - // for SRTP, but that's how it works. - // - // SRTP has a further problem with regards to jumps in sequence number. Consider this: - // - // 1. Create track local - // 2. Bind track local to track 1. - // 3. Bind track local to track 2. - // 4. Pause track 1. - // 5. Keep sending... - // - // At this point, the track local will keep incrementing the sequence number, because we have - // one binding that is still active. However SRTP hmac verifying (tag), can only accept a - // relatively small jump in sequence numbers since it uses the ROC (i.e. how many times the - // sequence number has rolled over), which means if this pause state of one binding persists - // for a longer time, the track can never be resumed since the receiver would have missed - // the rollovers. - if !internal.did_warn_about_wonky_pause { - internal.did_warn_about_wonky_pause = true; - warn!("Detected multiple track bindings where only one was paused"); - } - } - - // skip packets by the number of previously dropped packets - if let Some(sequencer) = &internal.sequencer { - for _ in 0..sample.prev_dropped_packets { - sequencer.next_sequence_number(); - } - } - - let clock_rate = internal.clock_rate; - - let packets = if let Some(packetizer) = &mut internal.packetizer { - let samples = (sample.duration.as_secs_f64() * clock_rate) as u32; - if sample.prev_dropped_packets > 0 { - packetizer.skip_samples(samples * sample.prev_dropped_packets as u32); - } - packetizer.packetize(&sample.data, samples)? - } else { - vec![] - }; - - let mut write_errs = vec![]; - for p in packets { - if let Err(err) = self - .rtp_track - .write_rtp_with_extensions(&p, extensions) - .await - { - write_errs.push(err); - } - } - - flatten_errs(write_errs) - } - - /// Create a builder for writing samples with additional data. - /// - /// # Example - /// ```no_run - /// use rtp::extension::audio_level_extension::AudioLevelExtension; - /// use std::time::Duration; - /// use webrtc::api::media_engine::MIME_TYPE_VP8; - /// use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; - /// use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; - /// - /// #[tokio::main] - /// async fn main() { - /// let track = TrackLocalStaticSample::new( - /// RTCRtpCodecCapability { - /// mime_type: MIME_TYPE_VP8.to_owned(), - /// ..Default::default() - /// }, - /// "video".to_owned(), - /// "webrtc-rs".to_owned(), - /// ); - /// let result = track - /// .sample_writer() - /// .with_audio_level(AudioLevelExtension { - /// level: 10, - /// voice: true, - /// }) - /// .write_sample(&media::Sample{ - /// data: bytes::Bytes::new(), - /// duration: Duration::from_secs(1), - /// ..Default::default() - /// }) - /// .await; - /// } - /// ``` - pub fn sample_writer(&self) -> SampleWriter<'_> { - SampleWriter::new(self) - } -} - -#[async_trait] -impl TrackLocal for TrackLocalStaticSample { - /// Bind is called by the PeerConnection after negotiation is complete - /// This asserts that the code requested is supported by the remote peer. - /// If so it setups all the state (SSRC and PayloadType) to have a call - async fn bind(&self, t: &TrackLocalContext) -> Result { - let codec = self.rtp_track.bind(t).await?; - - let mut internal = self.internal.lock().await; - - // We only need one packetizer - if internal.packetizer.is_some() { - return Ok(codec); - } - - let payloader = codec.capability.payloader_for_codec()?; - let sequencer: Box = - Box::new(rtp::sequence::new_random_sequencer()); - internal.packetizer = Some(Box::new(rtp::packetizer::new_packetizer( - RTP_OUTBOUND_MTU, - 0, // Value is handled when writing - 0, // Value is handled when writing - payloader, - sequencer.clone(), - codec.capability.clock_rate, - ))); - internal.sequencer = Some(sequencer); - internal.clock_rate = codec.capability.clock_rate as f64; - - Ok(codec) - } - - /// unbind implements the teardown logic when the track is no longer needed. This happens - /// because a track has been stopped. - async fn unbind(&self, t: &TrackLocalContext) -> Result<()> { - self.rtp_track.unbind(t).await - } - - /// id is the unique identifier for this Track. This should be unique for the - /// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' - /// and StreamID would be 'desktop' or 'webcam' - fn id(&self) -> &str { - self.rtp_track.id() - } - - /// RID is the RTP Stream ID for this track. - fn rid(&self) -> Option<&str> { - self.rtp_track.rid() - } - - /// stream_id is the group this track belongs too. This must be unique - fn stream_id(&self) -> &str { - self.rtp_track.stream_id() - } - - /// kind controls if this TrackLocal is audio or video - fn kind(&self) -> RTPCodecType { - self.rtp_track.kind() - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -mod sample_writer { - use media::Sample; - use rtp::extension::audio_level_extension::AudioLevelExtension; - use rtp::extension::video_orientation_extension::VideoOrientationExtension; - use rtp::extension::HeaderExtension; - - use super::TrackLocalStaticSample; - use crate::error::Result; - - /// Helper for writing Samples via [`TrackLocalStaticSample`] that carry extra RTP data. - /// - /// Created via [`TrackLocalStaticSample::sample_writer`]. - pub struct SampleWriter<'track> { - track: &'track TrackLocalStaticSample, - extensions: Vec, - } - - impl<'track> SampleWriter<'track> { - pub(super) fn new(track: &'track TrackLocalStaticSample) -> Self { - Self { - track, - extensions: vec![], - } - } - - /// Add a RTP audio level extension to all packets written for the sample. - /// - /// This overwrites any previously configured audio level extension. - pub fn with_audio_level(self, ext: AudioLevelExtension) -> Self { - self.with_extension(HeaderExtension::AudioLevel(ext)) - } - - /// Add a RTP video orientation extension to all packets written for the sample. - /// - /// This overwrites any previously configured video orientation extension. - pub fn with_video_orientation(self, ext: VideoOrientationExtension) -> Self { - self.with_extension(HeaderExtension::VideoOrientation(ext)) - } - - /// Add any RTP extension to all packets written for the sample. - pub fn with_extension(mut self, ext: HeaderExtension) -> Self { - self.extensions.retain(|e| !e.is_same(&ext)); - - self.extensions.push(ext); - - self - } - - /// Write the sample to the track. - /// - /// Creates one or more RTP packets with any extensions specified for each packet and sends - /// them. - pub async fn write_sample(self, sample: &Sample) -> Result<()> { - self.track - .write_sample_with_extensions(sample, &self.extensions) - .await - } - } -} - -pub use sample_writer::SampleWriter; diff --git a/webrtc/src/track/track_local/track_local_static_test.rs b/webrtc/src/track/track_local/track_local_static_test.rs deleted file mode 100644 index b385d98ac..000000000 --- a/webrtc/src/track/track_local/track_local_static_test.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::sync::Arc; - -use bytes::Bytes; -use tokio::sync::{mpsc, Mutex}; - -use super::track_local_static_rtp::*; -use super::track_local_static_sample::*; -use super::*; -use crate::api::media_engine::{MediaEngine, MIME_TYPE_VP8}; -use crate::api::APIBuilder; -use crate::peer_connection::configuration::RTCConfiguration; -use crate::peer_connection::peer_connection_test::*; - -// If a remote doesn't support a Codec used by a `TrackLocalStatic` -// an error should be returned to the user -#[tokio::test] -async fn test_track_local_static_no_codec_intersection() -> Result<()> { - let track: Arc = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: "video/vp8".to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - //"Offerer" - { - let mut pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let mut no_codec_pc = APIBuilder::new() - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - pc.add_track(Arc::clone(&track)).await?; - - if let Err(err) = signal_pair(&mut pc, &mut no_codec_pc).await { - assert_eq!(err, Error::ErrUnsupportedCodec); - } else { - panic!(); - } - - close_pair_now(&no_codec_pc, &pc).await; - } - - //"Answerer" - { - let mut pc = api.new_peer_connection(RTCConfiguration::default()).await?; - - let mut m = MediaEngine::default(); - m.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: "video/VP9".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 96, - ..Default::default() - }, - RTPCodecType::Video, - )?; - let mut vp9only_pc = APIBuilder::new() - .with_media_engine(m) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - vp9only_pc - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - pc.add_track(Arc::clone(&track)).await?; - - if let Err(err) = signal_pair(&mut vp9only_pc, &mut pc).await { - assert_eq!( - err, - Error::ErrUnsupportedCodec, - "expected {}, but got {}", - Error::ErrUnsupportedCodec, - err - ); - } else { - panic!(); - } - - close_pair_now(&vp9only_pc, &pc).await; - } - - //"Local" - { - let (mut offerer, mut answerer) = new_pair(&api).await?; - - let invalid_codec_track = TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: "video/invalid-codec".to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - ); - - offerer.add_track(Arc::new(invalid_codec_track)).await?; - - if let Err(err) = signal_pair(&mut offerer, &mut answerer).await { - assert_eq!(err, Error::ErrUnsupportedCodec); - } else { - panic!(); - } - - close_pair_now(&offerer, &answerer).await; - } - - Ok(()) -} - -// Assert that Bind/Unbind happens when expected -#[tokio::test] -async fn test_track_local_static_closed() -> Result<()> { - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_offer, mut pc_answer) = new_pair(&api).await?; - - pc_answer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let vp8writer: Arc = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: "video/vp8".to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - pc_offer.add_track(Arc::clone(&vp8writer)).await?; - - if let Some(v) = vp8writer.as_any().downcast_ref::() { - let bindings = v.bindings.lock().await; - assert_eq!( - bindings.len(), - 0, - "No binding should exist before signaling" - ); - } else { - panic!(); - } - - signal_pair(&mut pc_offer, &mut pc_answer).await?; - - if let Some(v) = vp8writer.as_any().downcast_ref::() { - let bindings = v.bindings.lock().await; - assert_eq!(bindings.len(), 1, "binding should exist after signaling"); - } else { - panic!(); - } - - close_pair_now(&pc_offer, &pc_answer).await; - - if let Some(v) = vp8writer.as_any().downcast_ref::() { - let bindings = v.bindings.lock().await; - assert_eq!(bindings.len(), 0, "No binding should exist after close"); - } else { - panic!(); - } - - Ok(()) -} - -//use log::LevelFilter; -//use std::io::Write; - -#[tokio::test] -async fn test_track_local_static_payload_type() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut media_engine_one = MediaEngine::default(); - media_engine_one.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 100, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - let mut media_engine_two = MediaEngine::default(); - media_engine_two.register_codec( - RTCRtpCodecParameters { - capability: RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 200, - ..Default::default() - }, - RTPCodecType::Video, - )?; - - let mut offerer = APIBuilder::new() - .with_media_engine(media_engine_one) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - let mut answerer = APIBuilder::new() - .with_media_engine(media_engine_two) - .build() - .new_peer_connection(RTCConfiguration::default()) - .await?; - - let track = Arc::new(TrackLocalStaticSample::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - offerer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - answerer - .add_track(Arc::clone(&track) as Arc) - .await?; - - let (on_track_fired_tx, on_track_fired_rx) = mpsc::channel::<()>(1); - let on_track_fired_tx = Arc::new(Mutex::new(Some(on_track_fired_tx))); - offerer.on_track(Box::new(move |track, _, _| { - let on_track_fired_tx2 = Arc::clone(&on_track_fired_tx); - Box::pin(async move { - assert_eq!(track.payload_type(), 100); - assert_eq!(track.codec().capability.mime_type, MIME_TYPE_VP8); - { - log::debug!("onTrackFiredFunc!!!"); - let mut done = on_track_fired_tx2.lock().await; - done.take(); - } - }) - })); - - signal_pair(&mut offerer, &mut answerer).await?; - - send_video_until_done( - on_track_fired_rx, - vec![track], - Bytes::from_static(&[0x00]), - None, - ) - .await; - - close_pair_now(&offerer, &answerer).await; - - Ok(()) -} - -// Assert that writing to a Track doesn't modify the input -// Even though we can pass a pointer we shouldn't modify the incoming value -#[tokio::test] -async fn test_track_local_static_mutate_input() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (mut pc_offer, mut pc_answer) = new_pair(&api).await?; - - let vp8writer: Arc = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - pc_offer.add_track(Arc::clone(&vp8writer)).await?; - - signal_pair(&mut pc_offer, &mut pc_answer).await?; - - let pkt = rtp::packet::Packet { - header: rtp::header::Header { - ssrc: 1, - payload_type: 1, - ..Default::default() - }, - ..Default::default() - }; - if let Some(v) = vp8writer.as_any().downcast_ref::() { - v.write_rtp(&pkt).await?; - } else { - panic!(); - } - - assert_eq!(pkt.header.ssrc, 1); - assert_eq!(pkt.header.payload_type, 1); - - close_pair_now(&pc_offer, &pc_answer).await; - - Ok(()) -} - -//use std::io::Write; -//use log::LevelFilter; - -// Assert that writing to a Track that has Binded (but not connected) -// does not block -#[tokio::test] -async fn test_track_local_static_binding_non_blocking() -> Result<()> { - /*env_logger::Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{}:{} [{}] {} - {}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.level(), - chrono::Local::now().format("%H:%M:%S.%6f"), - record.args() - ) - }) - .filter(None, LevelFilter::Trace) - .init();*/ - - let mut m = MediaEngine::default(); - m.register_default_codecs()?; - let api = APIBuilder::new().with_media_engine(m).build(); - - let (pc_offer, pc_answer) = new_pair(&api).await?; - - pc_offer - .add_transceiver_from_kind(RTPCodecType::Video, None) - .await?; - - let vp8writer: Arc = Arc::new(TrackLocalStaticRTP::new( - RTCRtpCodecCapability { - mime_type: MIME_TYPE_VP8.to_owned(), - ..Default::default() - }, - "video".to_owned(), - "webrtc-rs".to_owned(), - )); - - pc_answer.add_track(Arc::clone(&vp8writer)).await?; - - let offer = pc_offer.create_offer(None).await?; - pc_answer.set_remote_description(offer).await?; - - let answer = pc_answer.create_answer(None).await?; - pc_answer.set_local_description(answer).await?; - - if let Some(v) = vp8writer.as_any().downcast_ref::() { - v.write(&[0u8; 20]).await?; - } else { - panic!(); - } - - close_pair_now(&pc_offer, &pc_answer).await; - - Ok(()) -} - -/* -//TODO: func BenchmarkTrackLocalWrite(b *testing.B) { - offerPC, answerPC, err := newPair() - defer closePairNow(b, offerPC, answerPC) - if err != nil { - b.Fatalf("Failed to create a PC pair for testing") - } - - track, err := NewTrackLocalStaticRTP(RTPCodecCapability{mime_type: MIME_TYPE_VP8}, "video", "pion") - assert.NoError(b, err) - - _, err = offerPC.AddTrack(track) - assert.NoError(b, err) - - _, err = answerPC.AddTransceiverFromKind(RTPCodecTypeVideo) - assert.NoError(b, err) - - b.SetBytes(1024) - - buf := make([]byte, 1024) - for i := 0; i < b.N; i++ { - _, err := track.Write(buf) - assert.NoError(b, err) - } -} -*/ diff --git a/webrtc/src/track/track_remote/mod.rs b/webrtc/src/track/track_remote/mod.rs deleted file mode 100644 index 7d0565cf9..000000000 --- a/webrtc/src/track/track_remote/mod.rs +++ /dev/null @@ -1,321 +0,0 @@ -use std::collections::VecDeque; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; - -use arc_swap::ArcSwapOption; -use interceptor::{Attributes, Interceptor}; -use portable_atomic::{AtomicU32, AtomicU8, AtomicUsize}; -use smol_str::SmolStr; -use tokio::sync::Mutex; -use util::sync::Mutex as SyncMutex; - -use crate::api::media_engine::MediaEngine; -use crate::error::{Error, Result}; -use crate::rtp_transceiver::rtp_codec::{RTCRtpCodecParameters, RTCRtpParameters, RTPCodecType}; -use crate::rtp_transceiver::rtp_receiver::RTPReceiverInternal; -use crate::rtp_transceiver::{PayloadType, SSRC}; - -lazy_static! { - static ref TRACK_REMOTE_UNIQUE_ID: AtomicUsize = AtomicUsize::new(0); -} -pub type OnMuteHdlrFn = Box< - dyn (FnMut() -> Pin + Send + 'static>>) + Send + Sync + 'static, ->; - -#[derive(Default)] -struct Handlers { - on_mute: ArcSwapOption>, - on_unmute: ArcSwapOption>, -} - -#[derive(Default)] -struct TrackRemoteInternal { - peeked: VecDeque<(rtp::packet::Packet, Attributes)>, -} - -/// TrackRemote represents a single inbound source of media -pub struct TrackRemote { - tid: usize, - - id: SyncMutex, - stream_id: SyncMutex, - - receive_mtu: usize, - payload_type: AtomicU8, //PayloadType, - kind: AtomicU8, //RTPCodecType, - ssrc: AtomicU32, //SSRC, - codec: SyncMutex, - pub(crate) params: SyncMutex, - rid: SmolStr, - - media_engine: Arc, - interceptor: Arc, - - handlers: Arc, - - receiver: Option>, - internal: Mutex, -} - -impl std::fmt::Debug for TrackRemote { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TrackRemote") - .field("id", &self.id) - .field("stream_id", &self.stream_id) - .field("payload_type", &self.payload_type) - .field("kind", &self.kind) - .field("ssrc", &self.ssrc) - .field("codec", &self.codec) - .field("params", &self.params) - .field("rid", &self.rid) - .finish() - } -} - -impl TrackRemote { - pub(crate) fn new( - receive_mtu: usize, - kind: RTPCodecType, - ssrc: SSRC, - rid: SmolStr, - receiver: Weak, - media_engine: Arc, - interceptor: Arc, - ) -> Self { - TrackRemote { - tid: TRACK_REMOTE_UNIQUE_ID.fetch_add(1, Ordering::SeqCst), - id: Default::default(), - stream_id: Default::default(), - receive_mtu, - payload_type: Default::default(), - kind: AtomicU8::new(kind as u8), - ssrc: AtomicU32::new(ssrc), - codec: Default::default(), - params: Default::default(), - rid, - receiver: Some(receiver), - media_engine, - interceptor, - handlers: Default::default(), - - internal: Default::default(), - } - } - - pub fn tid(&self) -> usize { - self.tid - } - - /// id is the unique identifier for this Track. This should be unique for the - /// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' - /// and StreamID would be 'desktop' or 'webcam' - pub fn id(&self) -> String { - let id = self.id.lock(); - id.clone() - } - - pub fn set_id(&self, s: String) { - let mut id = self.id.lock(); - *id = s; - } - - /// stream_id is the group this track belongs too. This must be unique - pub fn stream_id(&self) -> String { - let stream_id = self.stream_id.lock(); - stream_id.clone() - } - - pub fn set_stream_id(&self, s: String) { - let mut stream_id = self.stream_id.lock(); - *stream_id = s; - } - - /// rid gets the RTP Stream ID of this Track - /// With Simulcast you will have multiple tracks with the same ID, but different RID values. - /// In many cases a TrackRemote will not have an RID, so it is important to assert it is non-zero - pub fn rid(&self) -> &str { - self.rid.as_str() - } - - /// payload_type gets the PayloadType of the track - pub fn payload_type(&self) -> PayloadType { - self.payload_type.load(Ordering::SeqCst) - } - - pub fn set_payload_type(&self, payload_type: PayloadType) { - self.payload_type.store(payload_type, Ordering::SeqCst); - } - - /// kind gets the Kind of the track - pub fn kind(&self) -> RTPCodecType { - self.kind.load(Ordering::SeqCst).into() - } - - pub fn set_kind(&self, kind: RTPCodecType) { - self.kind.store(kind as u8, Ordering::SeqCst); - } - - /// ssrc gets the SSRC of the track - pub fn ssrc(&self) -> SSRC { - self.ssrc.load(Ordering::SeqCst) - } - - pub fn set_ssrc(&self, ssrc: SSRC) { - self.ssrc.store(ssrc, Ordering::SeqCst); - } - - /// msid gets the Msid of the track - pub fn msid(&self) -> String { - format!("{} {}", self.stream_id(), self.id()) - } - - /// codec gets the Codec of the track - pub fn codec(&self) -> RTCRtpCodecParameters { - let codec = self.codec.lock(); - codec.clone() - } - - pub fn set_codec(&self, codec: RTCRtpCodecParameters) { - let mut c = self.codec.lock(); - *c = codec; - } - - pub fn params(&self) -> RTCRtpParameters { - let p = self.params.lock(); - p.clone() - } - - pub fn set_params(&self, params: RTCRtpParameters) { - let mut p = self.params.lock(); - *p = params; - } - - pub fn onmute(&self, handler: F) - where - F: FnMut() -> Pin + Send + 'static>> + Send + 'static + Sync, - { - self.handlers - .on_mute - .store(Some(Arc::new(Mutex::new(Box::new(handler))))); - } - - pub fn onunmute(&self, handler: F) - where - F: FnMut() -> Pin + Send + 'static>> + Send + 'static + Sync, - { - self.handlers - .on_unmute - .store(Some(Arc::new(Mutex::new(Box::new(handler))))); - } - - /// Reads data from the track. - /// - /// **Cancel Safety:** This method is not cancel safe. Dropping the resulting [`Future`] before - /// it returns [`std::task::Poll::Ready`] will cause data loss. - pub async fn read(&self, b: &mut [u8]) -> Result<(rtp::packet::Packet, Attributes)> { - { - // Internal lock scope - let mut internal = self.internal.lock().await; - if let Some((pkt, attributes)) = internal.peeked.pop_front() { - self.check_and_update_track(&pkt).await?; - - return Ok((pkt, attributes)); - } - }; - - let receiver = match self.receiver.as_ref().and_then(|r| r.upgrade()) { - Some(r) => r, - None => return Err(Error::ErrRTPReceiverNil), - }; - - let (pkt, attributes) = receiver.read_rtp(b, self.tid).await?; - self.check_and_update_track(&pkt).await?; - Ok((pkt, attributes)) - } - - /// check_and_update_track checks payloadType for every incoming packet - /// once a different payloadType is detected the track will be updated - pub(crate) async fn check_and_update_track(&self, pkt: &rtp::packet::Packet) -> Result<()> { - let payload_type = pkt.header.payload_type; - if payload_type != self.payload_type() { - let p = self - .media_engine - .get_rtp_parameters_by_payload_type(payload_type) - .await?; - - if let Some(receiver) = &self.receiver { - if let Some(receiver) = receiver.upgrade() { - self.kind.store(receiver.kind as u8, Ordering::SeqCst); - } - } - self.payload_type.store(payload_type, Ordering::SeqCst); - { - let mut codec = self.codec.lock(); - *codec = if let Some(codec) = p.codecs.first() { - codec.clone() - } else { - return Err(Error::ErrCodecNotFound); - }; - } - { - let mut params = self.params.lock(); - *params = p; - } - } - - Ok(()) - } - - /// read_rtp is a convenience method that wraps Read and unmarshals for you. - pub async fn read_rtp(&self) -> Result<(rtp::packet::Packet, Attributes)> { - let mut b = vec![0u8; self.receive_mtu]; - let (pkt, attributes) = self.read(&mut b).await?; - - Ok((pkt, attributes)) - } - - /// peek is like Read, but it doesn't discard the packet read - pub(crate) async fn peek(&self, b: &mut [u8]) -> Result<(rtp::packet::Packet, Attributes)> { - let (pkt, a) = self.read(b).await?; - - // this might overwrite data if somebody peeked between the Read - // and us getting the lock. Oh well, we'll just drop a packet in - // that case. - { - let mut internal = self.internal.lock().await; - internal.peeked.push_back((pkt.clone(), a.clone())); - } - Ok((pkt, a)) - } - - /// Set the initially peeked data for this track. - /// - /// This is useful when a track is first created to populate data read from the track in the - /// process of identifying the track as part of simulcast probing. Using this during other - /// parts of the track's lifecycle is probably an error. - pub(crate) async fn prepopulate_peeked_data( - &self, - data: VecDeque<(rtp::packet::Packet, Attributes)>, - ) { - let mut internal = self.internal.lock().await; - internal.peeked = data; - } - - pub(crate) async fn fire_onmute(&self) { - let on_mute = self.handlers.on_mute.load(); - - if let Some(f) = on_mute.as_ref() { - (f.lock().await)().await - }; - } - - pub(crate) async fn fire_onunmute(&self) { - let on_unmute = self.handlers.on_unmute.load(); - - if let Some(f) = on_unmute.as_ref() { - (f.lock().await)().await - }; - } -} From c8a6fad7c788f4d9ba77c73d9cfa7657fb81f42e Mon Sep 17 00:00:00 2001 From: alexlapa Date: Wed, 26 Jun 2024 12:32:35 +0300 Subject: [PATCH 2/6] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df8893093..93c84daa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ * [#330 Fix the problem that the UDP port of the server relay is not released](https://github.com/webrtc-rs/webrtc/pull/330) by [@clia](https://github.com/clia). * Added `alloc_close_notify` config parameter to `ServerConfig` and `Allocation`, to receive notify on allocation close event, with metrics data. +* Major refactor, add TCP transport [#1] + +[#1]: https://github.com/instrumentisto/medea-turn-rs/pull/1 ## v0.6.1 From 2948bf59dfbbae92c2c08242d37c64d0c121dc73 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 26 Jun 2024 16:14:26 +0200 Subject: [PATCH 3/6] Toolchain corrections --- .editorconfig | 30 ++ .github/workflows/ci.yml | 124 ++++-- .gitignore | 15 +- .rustfmt.toml | 12 +- CHANGELOG.md | 44 +++ Cargo.lock | 800 --------------------------------------- Cargo.toml | 16 +- LICENSE-APACHE | 25 -- LICENSE-MIT | 38 +- Makefile | 36 +- README.md | 4 +- 11 files changed, 225 insertions(+), 919 deletions(-) create mode 100644 .editorconfig delete mode 100644 Cargo.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..22cad94f5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 80 + +[*.md] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false +max_line_length = off + +[*.rs] +indent_style = space +indent_size = 4 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9bc41971..f997a7d44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: CI on: push: branches: ["main"] + tags: ["v*"] pull_request: branches: ["main"] @@ -11,12 +12,7 @@ concurrency: cancel-in-progress: true env: - CACHE: ${{ (github.event_name == 'push' - || github.event_name == 'pull_request') - && github.ref != 'refs/heads/main' - && !contains(github.event.head_commit.message, '[fresh ci]') }} RUST_BACKTRACE: 1 - RUST_VER: "1.79" jobs: @@ -28,9 +24,10 @@ jobs: if: ${{ github.event_name == 'pull_request' }} needs: - clippy + - msrv - rustdoc - rustfmt - - test-unit + - test runs-on: ubuntu-latest steps: - run: true @@ -45,20 +42,18 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VER }} + toolchain: stable components: clippy - - uses: Swatinem/rust-cache@v2 - if: ${{ env.CACHE == 'true' }} - run: make cargo.lint rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@v1 with: toolchain: nightly @@ -73,47 +68,116 @@ jobs: # Testing # ########### - test-unit: - name: test (unit, ${{ matrix.toolchain }}) + msrv: + name: MSRV strategy: fail-fast: false matrix: - toolchain: ["stable", "nightly"] - runs-on: ubuntu-latest + msrv: ["1.72.0"] + os: ["ubuntu", "macOS", "windows"] + runs-on: ${{ matrix.os }}-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: nightly + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ matrix.msrv }} + + - run: cargo +nightly update -Z minimal-versions + + - run: make test.cargo + + test: + strategy: + fail-fast: false + matrix: + toolchain: ["stable", "beta", "nightly"] + os: ["ubuntu", "macOS", "windows"] + runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ (matrix.toolchain == 'stable' && env.RUST_VER) - || matrix.toolchain }} + toolchain: ${{ matrix.toolchain }} components: rust-src - - uses: Swatinem/rust-cache@v2 - if: ${{ env.CACHE == 'true' }} - run: cargo install cargo-careful if: ${{ matrix.toolchain == 'nightly' }} - - run: make test.unit - careful=${{ (matrix.toolchain == 'nightly' && 'yes') - || 'no' }} + - run: make test.cargo + careful=${{ (matrix.toolchain == 'nightly' && 'yes') + || 'no' }} - ############ - # Building # - ############ + ################# + # Documentation # + ################# rustdoc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VER }} - - uses: Swatinem/rust-cache@v2 - if: ${{ env.CACHE == 'true' }} + toolchain: stable - run: make cargo.doc private=yes open=no env: RUSTFLAGS: -D warnings + + + + + ############# + # Releasing # + ############# + + publish: + name: publish (crates.io) + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: ["release-github"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + - run: cargo publish -p metrics-prometheus + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }} + + release-github: + name: release (GitHub) + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: ["clippy", "msrv", "rustdoc", "rustfmt", "test"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Parse release version + id: release + run: echo "version=${GITHUB_REF#refs/tags/v}" + >> $GITHUB_OUTPUT + - name: Verify release version matches Cargo manifest + run: | + test "${{ steps.release.outputs.version }}" \ + == "$(grep -m1 'version = "' Cargo.toml | cut -d'"' -f2)" + + - name: Parse CHANGELOG link + id: changelog + run: echo "link=${{ github.server_url }}/${{ github.repository }}/blob/v${{ steps.release.outputs.version }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.release.outputs.version }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' CHANGELOG.md)" + >> $GITHUB_OUTPUT + + - name: Create GitHub release + uses: softprops/action-gh-release@v1 + with: + name: ${{ steps.release.outputs.version }} + body: | + [API docs](https://docs.rs/synthez/${{ steps.release.outputs.version }}) + [Changelog](${{ steps.changelog.outputs.link }}) + prerelease: ${{ contains(steps.release.outputs.version, '-') }} diff --git a/.gitignore b/.gitignore index ef58f6ced..1e25d8ede 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ /.idea/ -/crates/target/ -/crates/.idea/ - -# These are backup files generated by rustfmt -**/*.rs.bk +/.vscode/ +/*.iml +.DS_Store -# Editor configs -.vscode +/Cargo.lock +/target/ diff --git a/.rustfmt.toml b/.rustfmt.toml index 0f9c48ca3..947f9c57b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,13 +1,15 @@ -# See full list at: -# https://github.com/rust-lang-nursery/rustfmt/blob/master/Configurations.md +# Project configuration for rustfmt Rust code formatter. +# See full list of configurations at: +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md max_width = 80 +use_small_heuristics = "Max" + format_strings = false imports_granularity = "Crate" -normalize_comments = true -wrap_comments = true -reorder_impl_items = true +format_code_in_doc_comments = true +format_macro_matchers = true use_try_shorthand = true error_on_line_overflow = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c84daa1..f71fea8c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +`medea-turn` changelog +====================== + +All user visible changes to this project will be documented in this file. This project uses [Semantic Versioning 2.0.0]. + + + + +## [0.7.0] · 2024-??-?? (unreleased) +[0.7.0]: /../../tree/v0.7.0 + +### Initially re-implemented + +- Performed major refactoring with non-server code removing. ([#1]) +- Added TCP transport. ([#1]) + +### [Upstream changes](https://github.com/webrtc-rs/webrtc/blob/89285ceba23dc57fc99386cb978d2d23fe909437/turn/CHANGELOG.md#unreleased) + +- Fixed non-released UDP port of server relay. ([webrtc-rs/webrtc#330] by [@clia]) +- Added `alloc_close_notify` config parameter to `ServerConfig` and `Allocation` to receive notify on allocation close event, with metrics data. ([webrtc-rs/webrtc#421] by [@clia]) + +[@clia]: https://github.com/clia +[#1]: /../../pull/1 +[webrtc-rs/webrtc#330]: https://github.com/webrtc-rs/webrtc/pull/330 +[webrtc-rs/webrtc#421]: https://github.com/webrtc-rs/webrtc/pull/421 + + + + +## Previous releases + +See [old upstream CHANGELOG](https://github.com/webrtc-rs/webrtc/blob/turn-v0.6.1/turn/CHANGELOG.md). + + + + +[Semantic Versioning 2.0.0]: https://semver.org + + + + + + + # webrtc-turn changelog ## Unreleased diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index d07ba7dc9..000000000 --- a/Cargo.lock +++ /dev/null @@ -1,800 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytecodec" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" -dependencies = [ - "byteorder", - "trackable 0.2.24", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" - -[[package]] -name = "cc" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "libc" -version = "0.2.155" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "log" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" - -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "medea-turn" -version = "0.8.0" -dependencies = [ - "async-trait", - "bytecodec", - "bytes", - "futures", - "hex", - "log", - "rand", - "stun_codec", - "thiserror", - "tokio", - "tokio-test", - "tokio-util", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" -dependencies = [ - "memchr", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "stun_codec" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" -dependencies = [ - "bytecodec", - "byteorder", - "crc", - "hmac", - "md5", - "sha1", - "trackable 1.3.0", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "tokio" -version = "1.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-test" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] - -[[package]] -name = "tokio-util" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "trackable" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" -dependencies = [ - "trackable 1.3.0", - "trackable_derive", -] - -[[package]] -name = "trackable" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" -dependencies = [ - "trackable_derive", -] - -[[package]] -name = "trackable_derive" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" -dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index 5fcf210ba..ad62ace5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "medea-turn" -version = "0.8.0" -authors = ["Rain Liu ", "Instrumentisto Team "] +version = "0.9.0-dev" +authors = ["Instrumentisto Team "] edition = "2021" rust-version = "1.70" -description = "A pure Rust implementation of TURN" +description = "TURN implementation used by Medea media server." license = "MIT OR Apache-2.0" homepage = "https://github.com/instrumentisto/medea-turn-rs" repository = "https://github.com/instrumentisto/medea-turn-rs" @@ -18,14 +18,8 @@ futures = "0.3" log = "0.4" rand = "0.8" stun_codec = "0.3" -thiserror = "1" -tokio = { version = "1.32.0", default-features = false, features = [ - "io-util", - "macros", - "net", - "rt-multi-thread", - "time", -] } +thiserror = "1.0" +tokio = { version = "1.32", default-features = false, features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } tokio-util = { version = "0.7", features = ["codec"] } [dev-dependencies] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 16fe87b06..1b5ec8b78 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -174,28 +174,3 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index e11d93bef..59f49a934 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,21 +1,25 @@ MIT License -Copyright (c) 2021 WebRTC.rs +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile index 30df759cb..2971b7768 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ # Aliases # ########### -all: fmt lint test.unit +all: fmt lint docs test docs: cargo.doc @@ -27,7 +27,7 @@ fmt: cargo.fmt lint: cargo.lint -test: test.unit +test: test.cargo @@ -36,22 +36,18 @@ test: test.unit # Cargo commands # ################## -cargo-crate = $(if $(call eq,$(crate),),--workspace,-p $(crate)) - - -# Generate crates documentation from Rust sources. +# Generate crate documentation from Rust sources. # # Usage: -# make cargo.doc [crate=] [private=(yes|no)] -# [open=(yes|no)] [clean=(no|yes)] +# make cargo.doc [private=(yes|no)] [open=(no|yes)] [clean=(no|yes)] cargo.doc: ifeq ($(clean),yes) @rm -rf target/doc/ endif - cargo doc $(cargo-crate) --all-features \ + cargo doc --all-features \ $(if $(call eq,$(private),no),,--document-private-items) \ - $(if $(call eq,$(open),no),,--open) + $(if $(call eq,$(open),yes),--open,) # Format Rust sources with rustfmt. @@ -66,10 +62,13 @@ cargo.fmt: # Lint Rust sources with Clippy. # # Usage: -# make cargo.lint [crate=] +# make cargo.lint cargo.lint: - cargo clippy $(cargo-crate) --all-features -- -D warnings + cargo clippy --all-features -- -D warnings + + +cargo.test: test.cargo @@ -78,13 +77,12 @@ cargo.lint: # Testing commands # #################### - -# Run project unit tests. +# Run Rust tests. # # Usage: -# make test.unit [crate=] [careful=(no|yes)] +# make test.cargo [careful=(no|yes)] -test.unit: +test.cargo: ifeq ($(careful),yes) ifeq ($(shell cargo install --list | grep cargo-careful),) cargo install cargo-careful @@ -103,6 +101,6 @@ endif # .PHONY section # ################## -.PHONY: all docs mt lint test \ - cargo.doc cargo.fmt cargo.lint \ - test.unit +.PHONY: all docs fmt lint test \ + cargo.doc cargo.fmt cargo.lint cargo.test \ + test.cargo diff --git a/README.md b/README.md index 6ef759760..ff80fa1b1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -medea-turn-rs -==== +`medea-turn` +============ [![CI](https://github.com/instrumentisto/medea-turn-rs/workflows/CI/badge.svg?branch=main "CI")](https://github.com/instrumentisto/medea-turn-rs/actions?query=workflow%3ACI+branch%3Amain) [![Rust 1.70+](https://img.shields.io/badge/rustc-1.70+-lightgray.svg "Rust 1.70+")](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html) From 2b2af30ae566e56ef58ee9bf9caef82e63cc2513 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 26 Jun 2024 16:20:20 +0200 Subject: [PATCH 4/6] Toolchain corrections --- .github/workflows/ci.yml | 21 ++++++++++----------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f997a7d44..eca82449e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: stable @@ -53,7 +53,7 @@ jobs: rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: nightly @@ -73,11 +73,11 @@ jobs: strategy: fail-fast: false matrix: - msrv: ["1.72.0"] + msrv: ["1.70.0"] os: ["ubuntu", "macOS", "windows"] runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: nightly @@ -94,10 +94,9 @@ jobs: fail-fast: false matrix: toolchain: ["stable", "beta", "nightly"] - os: ["ubuntu", "macOS", "windows"] - runs-on: ${{ matrix.os }}-latest + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: ${{ matrix.toolchain }} @@ -120,7 +119,7 @@ jobs: rustdoc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: stable @@ -142,12 +141,12 @@ jobs: needs: ["release-github"] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@v1 with: toolchain: stable - - run: cargo publish -p metrics-prometheus + - run: cargo publish -p medea-turn env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }} @@ -178,6 +177,6 @@ jobs: with: name: ${{ steps.release.outputs.version }} body: | - [API docs](https://docs.rs/synthez/${{ steps.release.outputs.version }}) + [API docs](https://docs.rs/medea-turn/${{ steps.release.outputs.version }}) [Changelog](${{ steps.changelog.outputs.link }}) prerelease: ${{ contains(steps.release.outputs.version, '-') }} diff --git a/Cargo.toml b/Cargo.toml index ad62ace5f..3b23c8165 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "medea-turn" -version = "0.9.0-dev" +version = "0.7.0-dev" authors = ["Instrumentisto Team "] edition = "2021" rust-version = "1.70" From 76119407d37027c38fd29c07566a8150650ff8b4 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 26 Jun 2024 16:44:52 +0200 Subject: [PATCH 5/6] Corrections --- .github/workflows/ci.yml | 3 +- Cargo.toml | 2 +- README.md | 15 ++++++-- src/allocation/allocation_manager.rs | 13 ++----- src/allocation/channel_bind.rs | 13 ++----- src/allocation/mod.rs | 16 +++------ src/chandata.rs | 52 +++++++++++++--------------- src/con/tcp.rs | 6 +--- src/relay.rs | 7 ++-- src/server/config.rs | 10 ++---- src/server/mod.rs | 6 +--- src/server/request.rs | 32 +++++++---------- 12 files changed, 68 insertions(+), 107 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eca82449e..80f0a5ef9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} needs: - clippy - - msrv + #- msrv - rustdoc - rustfmt - test @@ -70,6 +70,7 @@ jobs: msrv: name: MSRV + if: ${{ false }} # TODO: re-enable once fully refactored strategy: fail-fast: false matrix: diff --git a/Cargo.toml b/Cargo.toml index 3b23c8165..0e1f97298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] async-trait = "0.1" -bytecodec = "0.4" +bytecodec = "0.4.15" bytes = "1.6" futures = "0.3" log = "0.4" diff --git a/README.md b/README.md index ff80fa1b1..0921b79dc 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,21 @@ [Changelog](https://github.com/instrumentisto/medea-turn-rs/blob/master/CHANGELOG.md) -A pure Rust implementation of TURN. Majorly refactored fork of -[webrtc-rs/turn](https://github.com/webrtc-rs/webrtc/tree/f9734b79b0b87b15f157780ef3c26480cebd5d84/turn). +TURN implementation used by [Medea media server](https://github.com/instrumentisto/medea). Majorly refactored fork of the [`webrtc-rs/turn` crate](https://github.com/webrtc-rs/webrtc/tree/89285ceba23dc57fc99386cb978d2d23fe909437/turn). ## License -Dual licensing under both MIT and Apache-2.0 is the currently accepted standard by the Rust language community and has been used for both the compiler and many public libraries since (see https://doc.rust-lang.org/1.6.0/complement-project-faq.html#why-dual-mitasl2-license). In order to match the community standards, webrtc-rs is using the dual MIT+Apache-2.0 license. +Copyright © 2024 Instrumentisto Team, + +Licensed under either of [Apache License, Version 2.0][APACHE] or [MIT license][MIT] at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the [Apache-2.0 license][APACHE], shall be dual licensed as above, without any additional terms or conditions. + + + + +[APACHE]: https://github.com/instrumentisto/medea-turn-rs/blob/main/LICENSE-APACHE +[MIT]: https://github.com/instrumentisto/medea-turn-rs/blob/main/LICENSE-MIT diff --git a/src/allocation/allocation_manager.rs b/src/allocation/allocation_manager.rs index a7a1427bf..987e347b5 100644 --- a/src/allocation/allocation_manager.rs +++ b/src/allocation/allocation_manager.rs @@ -148,10 +148,7 @@ impl Manager { let a = Arc::new(a); #[allow(clippy::unwrap_used)] drop( - self.allocations - .lock() - .unwrap() - .insert(five_tuple, Arc::clone(&a)), + self.allocations.lock().unwrap().insert(five_tuple, Arc::clone(&a)), ); Ok(a) @@ -330,8 +327,7 @@ mod allocation_manager_test { let port = { // add permission with peer1 address - a.add_permission(peer_listener1.local_addr().unwrap().ip()) - .await; + a.add_permission(peer_listener1.local_addr().unwrap().ip()).await; // add channel with min channel number and peer2 address a.add_channel_bind( ChannelNumber::MIN, @@ -377,10 +373,7 @@ mod allocation_manager_test { let data = data_ch_rx.recv().await.unwrap(); // resolve channel data - assert!( - ChannelData::is_channel_data(&data), - "should be channel data" - ); + assert!(ChannelData::is_channel_data(&data), "should be channel data"); let channel_data = ChannelData::decode(data).unwrap(); assert_eq!( diff --git a/src/allocation/channel_bind.rs b/src/allocation/channel_bind.rs index 60244f5a7..f19a82bde 100644 --- a/src/allocation/channel_bind.rs +++ b/src/allocation/channel_bind.rs @@ -28,11 +28,7 @@ pub(crate) struct ChannelBind { impl ChannelBind { /// Creates a new [`ChannelBind`] pub(crate) const fn new(number: u16, peer: SocketAddr) -> Self { - Self { - number, - peer, - reset_tx: None, - } + Self { number, peer, reset_tx: None } } /// Starts [`ChannelBind`]'s internal lifetime watching loop. @@ -121,17 +117,14 @@ mod channel_bind_test { let addr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0); - a.add_channel_bind(ChannelNumber::MIN, addr, lifetime) - .await?; + a.add_channel_bind(ChannelNumber::MIN, addr, lifetime).await?; Ok(a) } #[tokio::test] async fn test_channel_bind() { - let a = create_channel_bind(Duration::from_millis(20)) - .await - .unwrap(); + let a = create_channel_bind(Duration::from_millis(20)).await.unwrap(); let result = a.get_channel_addr(&ChannelNumber::MIN).await; if let Some(addr) = result { diff --git a/src/allocation/mod.rs b/src/allocation/mod.rs index e549e1ee3..42ed53f44 100644 --- a/src/allocation/mod.rs +++ b/src/allocation/mod.rs @@ -1,6 +1,6 @@ -//! TURN server [Allocation]. +//! TURN server [allocation]. //! -//! [Allocation]:https://datatracker.ietf.org/doc/html/rfc5766#section-5 +//! [allocation]: https://datatracker.ietf.org/doc/html/rfc5766#section-5 mod allocation_manager; mod channel_bind; @@ -72,11 +72,7 @@ impl AllocInfo { username: String, relayed_bytes: usize, ) -> Self { - Self { - five_tuple, - username, - relayed_bytes, - } + Self { five_tuple, username, relayed_bytes } } } @@ -260,11 +256,7 @@ impl Allocation { &self, number: &u16, ) -> Option { - self.channel_bindings - .lock() - .await - .get(number) - .map(ChannelBind::peer) + self.channel_bindings.lock().await.get(number).map(ChannelBind::peer) } /// Gets the [`ChannelBind`]'s number from this [`Allocation`] by `addr`. diff --git a/src/chandata.rs b/src/chandata.rs index 2c6883aaa..f5ddf6aab 100644 --- a/src/chandata.rs +++ b/src/chandata.rs @@ -155,14 +155,8 @@ mod chandata_test { let tests = vec![ ( "equal", - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 3], - }, - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 3], - }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 3] }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 3] }, true, ), ( @@ -171,10 +165,7 @@ mod chandata_test { number: ChannelNumber::MIN + 1, data: vec![1, 2, 3], }, - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 3], - }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 3] }, false, ), ( @@ -183,22 +174,13 @@ mod chandata_test { number: ChannelNumber::MIN, data: vec![1, 2, 3, 4], }, - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 3], - }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 3] }, false, ), ( "data", - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 2], - }, - ChannelData { - number: ChannelNumber::MIN, - data: vec![1, 2, 3], - }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 2] }, + ChannelData { number: ChannelNumber::MIN, data: vec![1, 2, 3] }, false, ), ]; @@ -256,10 +238,26 @@ mod chandata_test { } } - #[rustfmt::skip] const CHANDATA_TEST_HEX: [&str; 2] = [ - "40000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a33776c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e001eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442", - "4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d0403023011310f300d06035504030c06576562525443301e170d3138303831313033353230305a170d3138303931313033353230305a3011310f300d06035504030c065765625254433059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469cfb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd74b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4bc9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a96e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd000000000000001300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e9a19240b3a68b95699546c044f863cf8349bc8046214fefd000000000000001400010116fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed06898c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b", + "40000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a3377\ + 6c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e00\ + 1eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442", + "4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a\ + 308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d040302\ + 3011310f300d06035504030c06576562525443301e170d313830383131303335323030\ + 5a170d3138303931313033353230305a3011310f300d06035504030c06576562525443\ + 3059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469c\ + fb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd7\ + 4b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348\ + 003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8\ + 762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4b\ + c9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a9\ + 6e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd00000000000000\ + 1300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0\ + ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e\ + 9a19240b3a68b95699546c044f863cf8349bc8046214fefd0000000000000014000101\ + 16fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed068\ + 98c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b", ]; #[test] diff --git a/src/con/tcp.rs b/src/con/tcp.rs index 7edda3f01..e8c4f222b 100644 --- a/src/con/tcp.rs +++ b/src/con/tcp.rs @@ -128,11 +128,7 @@ impl TcpServer { } })); - Ok(Self { - ingress_rx: Mutex::new(ingress_rx), - local_addr, - writers, - }) + Ok(Self { ingress_rx: Mutex::new(ingress_rx), local_addr, writers }) } /// Spawns a handler task for the given [`TcpStream`] diff --git a/src/relay.rs b/src/relay.rs index cce7dd52d..79d60502b 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -47,11 +47,8 @@ impl RelayAllocator { use_ipv4: bool, requested_port: u16, ) -> Result<(Arc, SocketAddr), Error> { - let max_retries = if self.max_retries == 0 { - 10 - } else { - self.max_retries - }; + let max_retries = + if self.max_retries == 0 { 10 } else { self.max_retries }; if requested_port == 0 { for _ in 0..max_retries { diff --git a/src/server/config.rs b/src/server/config.rs index 3fe76bcb1..57c991803 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -34,15 +34,9 @@ impl ConnConfig { gen.min_port > gen.max_port, "max_port must be greater than min_port" ); - assert!( - gen.address.is_empty(), - "address must not be an empty string" - ); + assert!(gen.address.is_empty(), "address must not be an empty string"); - Self { - conn, - relay_addr_generator: gen, - } + Self { conn, relay_addr_generator: gen } } } diff --git a/src/server/mod.rs b/src/server/mod.rs index d49460090..43f06116f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -236,11 +236,7 @@ impl Server { let handle = request::handle_message( msg, &conn, - FiveTuple { - src_addr, - dst_addr: local_con_addr, - protocol, - }, + FiveTuple { src_addr, dst_addr: local_con_addr, protocol }, realm.as_str(), channel_bind_lifetime, &allocation_manager, diff --git a/src/server/request.rs b/src/server/request.rs index 2eb5e7549..b8d4e2168 100644 --- a/src/server/request.rs +++ b/src/server/request.rs @@ -409,9 +409,7 @@ async fn handle_allocate_request( let msg = { if let Some(token) = reservation_token { - allocs - .create_reservation(token, a.relay_addr().port()) - .await; + allocs.create_reservation(token, a.relay_addr().port()).await; } let mut msg = Message::new( @@ -719,9 +717,8 @@ async fn handle_send_indication( return Err(Error::NoAllocationFound); }; - let data_attr = msg - .get_attribute::() - .ok_or(Error::AttributeNotFound)?; + let data_attr = + msg.get_attribute::().ok_or(Error::AttributeNotFound)?; let peer_address = msg .get_attribute::() .map(XorPeerAddress::address) @@ -733,9 +730,7 @@ async fn handle_send_indication( } // TODO: dont clone - a.relay(data_attr.data(), peer_address) - .await - .map_err(Into::into) + a.relay(data_attr.data(), peer_address).await.map_err(Into::into) } /// Handles the given [`Message`] as a [ChannelBind Request][1]. @@ -767,9 +762,8 @@ async fn handle_channel_bind_request( return Err(Error::AttributeNotFound); }; - let Some(peer_addr) = msg - .get_attribute::() - .map(XorPeerAddress::address) + let Some(peer_addr) = + msg.get_attribute::().map(XorPeerAddress::address) else { answer_with_err(conn, five_tuple.src_addr, bad_request_msg).await?; @@ -901,15 +895,16 @@ async fn answer_with_err( /// ensuring that it is not greater than configured /// [`MAXIMUM_ALLOCATION_LIFETIME`]. fn get_lifetime(m: &Message) -> Duration { - m.get_attribute::() - .map(Lifetime::lifetime) - .map_or(DEFAULT_LIFETIME, |lifetime| { + m.get_attribute::().map(Lifetime::lifetime).map_or( + DEFAULT_LIFETIME, + |lifetime| { if lifetime > MAXIMUM_ALLOCATION_LIFETIME { DEFAULT_LIFETIME } else { lifetime } - }) + }, + ) } #[cfg(test)] @@ -1010,10 +1005,7 @@ mod request_test { }; let nonces = Arc::new(Mutex::new(HashMap::new())); - nonces - .lock() - .await - .insert(STATIC_KEY.to_owned(), Instant::now()); + nonces.lock().await.insert(STATIC_KEY.to_owned(), Instant::now()); allocation_manager .create_allocation( From 3b4ae5b9e53c23f22398965143ee36e72a61a24d Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 26 Jun 2024 17:07:53 +0200 Subject: [PATCH 6/6] Fix CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f0a5ef9..44efe0993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,10 @@ name: CI on: push: - branches: ["main"] + branches: ["master"] tags: ["v*"] pull_request: - branches: ["main"] + branches: ["master"] concurrency: group: ${{ github.workflow }}-${{ github.ref }}

- WebRTC.rs -
-