diff --git a/Cargo.lock b/Cargo.lock index 4bf4a13..a418675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -78,15 +78,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -685,9 +685,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -697,15 +697,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -1287,19 +1287,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if 1.0.4", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if 1.0.4", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -1731,9 +1731,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1758,9 +1758,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1789,9 +1789,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "linux-raw-sys" @@ -2131,9 +2131,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2258,18 +2258,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -2278,9 +2278,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2290,9 +2290,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2489,9 +2489,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2502,6 +2502,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -2615,9 +2621,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rfc6979" @@ -2779,9 +2785,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring 0.17.14", @@ -2875,9 +2881,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3137,12 +3143,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3176,8 +3182,6 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "stun" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea256fb46a13f9204e9dee9982997b2c3097db175a9fddaa8350310d03c4d5a3" dependencies = [ "base64 0.22.1", "crc", @@ -3260,12 +3264,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -3382,9 +3386,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3392,7 +3396,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -3409,9 +3413,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3475,7 +3479,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3489,18 +3493,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tonic" @@ -3650,9 +3654,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3789,11 +3793,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -3924,9 +3928,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -3937,9 +3941,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -3951,9 +3955,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3961,9 +3965,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3974,9 +3978,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -4017,9 +4021,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4084,7 +4088,7 @@ dependencies = [ "ring 0.17.14", "rtcp 0.12.0", "rtp 0.12.0", - "rustls 0.23.36", + "rustls 0.23.37", "sdp", "serde", "serde_json", @@ -4146,7 +4150,7 @@ dependencies = [ "rand_core 0.6.4", "rcgen", "ring 0.17.14", - "rustls 0.23.36", + "rustls 0.23.37", "sec1", "serde", "sha1", @@ -4455,15 +4459,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -4497,30 +4492,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4533,12 +4511,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4551,12 +4523,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4569,24 +4535,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4599,12 +4553,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4617,12 +4565,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4635,12 +4577,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4654,16 +4590,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" @@ -4823,18 +4759,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4f50329..85fdf6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,3 +68,6 @@ env_logger = "0.9.0" [build-dependencies] tonic-build = {version = "0.9.2",features = ["prost"]} cbindgen = "0.29.2" + +[patch.crates-io] +stun = { path = "stun-patch" } diff --git a/stun-patch/.gitignore b/stun-patch/.gitignore new file mode 100644 index 0000000..81561ed --- /dev/null +++ b/stun-patch/.gitignore @@ -0,0 +1,11 @@ +# 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-patch/Cargo.toml b/stun-patch/Cargo.toml new file mode 100644 index 0000000..50b7df3 --- /dev/null +++ b/stun-patch/Cargo.toml @@ -0,0 +1,108 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "stun" +version = "0.7.0" +authors = ["Rain Liu "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A pure Rust implementation of STUN" +homepage = "https://webrtc.rs" +documentation = "https://docs.rs/stun" +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/webrtc-rs/webrtc/tree/master/stun" + +[lib] +name = "stun" +path = "src/lib.rs" + +[[example]] +name = "stun_client" +path = "examples/stun_client.rs" +bench = false + +[[example]] +name = "stun_decode" +path = "examples/stun_decode.rs" +bench = false + +[[bench]] +name = "bench" +path = "benches/bench.rs" +harness = false + +[dependencies.base64] +version = "0.22.1" + +[dependencies.crc] +version = "3" + +[dependencies.lazy_static] +version = "1" + +[dependencies.md-5] +version = "0.10" + +[dependencies.rand] +version = "0.8" + +[dependencies.ring] +version = "0.17" + +[dependencies.subtle] +version = "2.4" + +[dependencies.thiserror] +version = "1" + +[dependencies.tokio] +version = "1.32.0" +features = [ + "fs", + "io-util", + "io-std", + "macros", + "net", + "parking_lot", + "rt", + "rt-multi-thread", + "sync", + "time", +] + +[dependencies.url] +version = "2" + +[dependencies.util] +version = "0.10.0" +features = ["conn"] +default-features = false +package = "webrtc-util" + +[dev-dependencies.clap] +version = "3" + +[dev-dependencies.criterion] +version = "0.5" + +[dev-dependencies.tokio-test] +version = "0.4" + +[features] +bench = [] +default = [] diff --git a/stun-patch/LICENSE-APACHE b/stun-patch/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/stun-patch/LICENSE-APACHE @@ -0,0 +1,201 @@ + 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-patch/LICENSE-MIT b/stun-patch/LICENSE-MIT new file mode 100644 index 0000000..e11d93b --- /dev/null +++ b/stun-patch/LICENSE-MIT @@ -0,0 +1,21 @@ +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-patch/src/addr.rs b/stun-patch/src/addr.rs new file mode 100644 index 0000000..cafe609 --- /dev/null +++ b/stun-patch/src/addr.rs @@ -0,0 +1,128 @@ +#[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-patch/src/addr/addr_test.rs b/stun-patch/src/addr/addr_test.rs new file mode 100644 index 0000000..77f5ac6 --- /dev/null +++ b/stun-patch/src/addr/addr_test.rs @@ -0,0 +1,183 @@ +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-patch/src/agent.rs b/stun-patch/src/agent.rs new file mode 100644 index 0000000..4562df7 --- /dev/null +++ b/stun-patch/src/agent.rs @@ -0,0 +1,283 @@ +#[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-patch/src/agent/agent_test.rs b/stun-patch/src/agent/agent_test.rs new file mode 100644 index 0000000..8ca1afa --- /dev/null +++ b/stun-patch/src/agent/agent_test.rs @@ -0,0 +1,195 @@ +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-patch/src/attributes.rs b/stun-patch/src/attributes.rs new file mode 100644 index 0000000..f51a98e --- /dev/null +++ b/stun-patch/src/attributes.rs @@ -0,0 +1,209 @@ +#[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-patch/src/attributes/attributes_test.rs b/stun-patch/src/attributes/attributes_test.rs new file mode 100644 index 0000000..3be540f --- /dev/null +++ b/stun-patch/src/attributes/attributes_test.rs @@ -0,0 +1,86 @@ +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-patch/src/checks.rs b/stun-patch/src/checks.rs new file mode 100644 index 0000000..9f4c134 --- /dev/null +++ b/stun-patch/src/checks.rs @@ -0,0 +1,48 @@ +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-patch/src/client.rs b/stun-patch/src/client.rs new file mode 100644 index 0000000..13d8bd6 --- /dev/null +++ b/stun-patch/src/client.rs @@ -0,0 +1,473 @@ +#[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-patch/src/client/client_test.rs b/stun-patch/src/client/client_test.rs new file mode 100644 index 0000000..c7bad84 --- /dev/null +++ b/stun-patch/src/client/client_test.rs @@ -0,0 +1,12 @@ +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-patch/src/error.rs b/stun-patch/src/error.rs new file mode 100644 index 0000000..083b3ad --- /dev/null +++ b/stun-patch/src/error.rs @@ -0,0 +1,98 @@ +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-patch/src/error_code.rs b/stun-patch/src/error_code.rs new file mode 100644 index 0000000..3bf8cfc --- /dev/null +++ b/stun-patch/src/error_code.rs @@ -0,0 +1,158 @@ +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 = String::from_utf8_lossy(&self.reason); + 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-patch/src/fingerprint.rs b/stun-patch/src/fingerprint.rs new file mode 100644 index 0000000..648c288 --- /dev/null +++ b/stun-patch/src/fingerprint.rs @@ -0,0 +1,64 @@ +#[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-patch/src/fingerprint/fingerprint_test.rs b/stun-patch/src/fingerprint/fingerprint_test.rs new file mode 100644 index 0000000..1ac589d --- /dev/null +++ b/stun-patch/src/fingerprint/fingerprint_test.rs @@ -0,0 +1,73 @@ +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-patch/src/integrity.rs b/stun-patch/src/integrity.rs new file mode 100644 index 0000000..cd692da --- /dev/null +++ b/stun-patch/src/integrity.rs @@ -0,0 +1,118 @@ +#[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-patch/src/integrity/integrity_test.rs b/stun-patch/src/integrity/integrity_test.rs new file mode 100644 index 0000000..e085cfa --- /dev/null +++ b/stun-patch/src/integrity/integrity_test.rs @@ -0,0 +1,93 @@ +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-patch/src/lib.rs b/stun-patch/src/lib.rs new file mode 100644 index 0000000..f13ff34 --- /dev/null +++ b/stun-patch/src/lib.rs @@ -0,0 +1,26 @@ +#![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-patch/src/message.rs b/stun-patch/src/message.rs new file mode 100644 index 0000000..6ac245e --- /dev/null +++ b/stun-patch/src/message.rs @@ -0,0 +1,626 @@ +#[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-patch/src/message/message_test.rs b/stun-patch/src/message/message_test.rs new file mode 100644 index 0000000..afd388e --- /dev/null +++ b/stun-patch/src/message/message_test.rs @@ -0,0 +1,744 @@ +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-patch/src/textattrs.rs b/stun-patch/src/textattrs.rs new file mode 100644 index 0000000..54c5e77 --- /dev/null +++ b/stun-patch/src/textattrs.rs @@ -0,0 +1,95 @@ +#[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-patch/src/textattrs/textattrs_test.rs b/stun-patch/src/textattrs/textattrs_test.rs new file mode 100644 index 0000000..e0a01ab --- /dev/null +++ b/stun-patch/src/textattrs/textattrs_test.rs @@ -0,0 +1,307 @@ +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-patch/src/uattrs.rs b/stun-patch/src/uattrs.rs new file mode 100644 index 0000000..087d809 --- /dev/null +++ b/stun-patch/src/uattrs.rs @@ -0,0 +1,62 @@ +#[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-patch/src/uattrs/uattrs_test.rs b/stun-patch/src/uattrs/uattrs_test.rs new file mode 100644 index 0000000..2351d55 --- /dev/null +++ b/stun-patch/src/uattrs/uattrs_test.rs @@ -0,0 +1,37 @@ +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-patch/src/uri.rs b/stun-patch/src/uri.rs new file mode 100644 index 0000000..5dce476 --- /dev/null +++ b/stun-patch/src/uri.rs @@ -0,0 +1,73 @@ +#[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-patch/src/uri/uri_test.rs b/stun-patch/src/uri/uri_test.rs new file mode 100644 index 0000000..20f13d1 --- /dev/null +++ b/stun-patch/src/uri/uri_test.rs @@ -0,0 +1,68 @@ +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-patch/src/xoraddr.rs b/stun-patch/src/xoraddr.rs new file mode 100644 index 0000000..0a86bb3 --- /dev/null +++ b/stun-patch/src/xoraddr.rs @@ -0,0 +1,173 @@ +#[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-patch/src/xoraddr/xoraddr_test.rs b/stun-patch/src/xoraddr/xoraddr_test.rs new file mode 100644 index 0000000..2d5544a --- /dev/null +++ b/stun-patch/src/xoraddr/xoraddr_test.rs @@ -0,0 +1,250 @@ +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(()) +}