mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 10:56:13 -08:00
feat(upnp): inline the IGD client (#2555)
* feat(upnp): inline the IGD client Replace igd-next with the subset EasyTier uses for SSDP discovery, device description parsing, and SOAP port-mapping requests. Route SSDP and HTTP connections through EasyTier's existing socket factories so platform-specific socket policy remains centralized. Preserve dynamic service types, XML escaping, response limits, and request timeouts from the upstream implementation. Keep the upstream MIT license and retain mapping inspection only for integration tests. * chore(ohos): refresh lockfile after IGD migration Update the independent OHOS workspace lockfile for the changed EasyTier dependency set so Cargo --locked accepts it. Remove the stale igd-next package and its now-unused transitive dependencies.
This commit is contained in:
1 parent
57afa290a4
commit
e972307510
11 files changed
+1337
-531
No files matched your search
Generated
+14
-258
@@ -32,7 +32,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -243,7 +243,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
@@ -502,18 +502,6 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_impl"
|
||||
version = "1.2.1"
|
||||
@@ -1228,18 +1216,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1249,7 +1226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20 0.9.1",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
@@ -1609,15 +1586,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
@@ -1838,7 +1806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
@@ -2338,9 +2306,11 @@ dependencies = [
|
||||
"hickory-server",
|
||||
"hkdf",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"igd-next",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"indoc",
|
||||
"kcp-sys",
|
||||
"log",
|
||||
@@ -2406,6 +2376,7 @@ dependencies = [
|
||||
"windows-service",
|
||||
"winreg 0.52.0",
|
||||
"x25519-dalek",
|
||||
"xmltree",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
@@ -3501,25 +3472,11 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.2.0",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
@@ -4349,12 +4306,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -4392,26 +4343,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "igd-next"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d"
|
||||
dependencies = [
|
||||
"attohttpc",
|
||||
"bytes",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"rand 0.10.1",
|
||||
"tokio",
|
||||
"url",
|
||||
"xmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.22"
|
||||
@@ -4840,12 +4771,6 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.9.0"
|
||||
@@ -6690,7 +6615,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -6702,7 +6627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -7157,12 +7082,6 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -7214,17 +7133,6 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20 0.10.0",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
@@ -7282,12 +7190,6 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.2.2"
|
||||
@@ -8588,7 +8490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -8599,7 +8501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.13",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -10872,24 +10774,6 @@ dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
@@ -10952,28 +10836,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.14.0",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.5.0"
|
||||
@@ -10987,18 +10849,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"hashbrown 0.15.3",
|
||||
"indexmap 2.14.0",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.15"
|
||||
@@ -12019,32 +11869,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
@@ -12054,74 +11878,6 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.14.0",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.8.0",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wl-clipboard-rs"
|
||||
version = "0.9.3"
|
||||
|
||||
+36
-245
@@ -26,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -224,24 +224,22 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-write-file"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeb1e2c1d58618bea806ccca5bbe65dc4e868be16f69ff118a39049389687548"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_impl"
|
||||
version = "1.3.0"
|
||||
@@ -495,18 +493,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -516,7 +503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20 0.9.1",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
@@ -722,15 +709,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
@@ -860,7 +838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
@@ -1132,6 +1110,7 @@ dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"atomic-write-file",
|
||||
"atomic_refcell",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
@@ -1163,10 +1142,13 @@ dependencies = [
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hkdf",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"igd-next",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"indoc",
|
||||
"kcp-sys",
|
||||
"log",
|
||||
@@ -1195,6 +1177,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"socket2 0.5.10",
|
||||
@@ -1219,6 +1202,7 @@ dependencies = [
|
||||
"windows 0.62.2",
|
||||
"windows-service",
|
||||
"winreg 0.52.0",
|
||||
"xmltree",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
@@ -1791,25 +1775,11 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"r-efi",
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
@@ -2075,6 +2045,15 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -2348,12 +2327,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -2381,26 +2354,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "igd-next"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d"
|
||||
dependencies = [
|
||||
"attohttpc",
|
||||
"bytes",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"rand 0.10.1",
|
||||
"tokio",
|
||||
"url",
|
||||
"xmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
@@ -2425,8 +2378,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2618,12 +2569,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libbz2-rs-sys"
|
||||
version = "0.2.2"
|
||||
@@ -3382,7 +3327,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -3394,7 +3339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -3777,12 +3722,6 @@ 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 = "radix_trie"
|
||||
version = "0.2.1"
|
||||
@@ -3814,17 +3753,6 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20 0.10.0",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -3863,12 +3791,6 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
@@ -4384,7 +4306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -4395,7 +4317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -5358,16 +5280,7 @@ version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.46.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5442,40 +5355,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.80"
|
||||
@@ -6183,94 +6062,6 @@ version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn 2.0.106",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
|
||||
+13
-2
@@ -223,8 +223,13 @@ shellexpand = "3.1.1"
|
||||
|
||||
# for fake tcp
|
||||
flume = { version = "0.12", optional = true }
|
||||
igd-next = { version = "0.17.0", features = ["aio_tokio"], optional = true }
|
||||
|
||||
# for upnp
|
||||
http-body-util = { version = "0.1", optional = true }
|
||||
hyper = { version = "1", default-features = false, features = ["client", "http1"], optional = true }
|
||||
hyper-util = { version = "0.1", default-features = false, features = ["tokio"], optional = true }
|
||||
natpmp = { version = "0.5.0", optional = true }
|
||||
xmltree = { version = "0.10", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||
machine-uid = "0.5.3"
|
||||
@@ -399,7 +404,13 @@ faketcp = [
|
||||
"easytier-proto/faketcp",
|
||||
]
|
||||
zstd = ["easytier-core/zstd", "easytier-proto/zstd"]
|
||||
upnp = ["dep:igd-next", "dep:natpmp"]
|
||||
upnp = [
|
||||
"dep:http-body-util",
|
||||
"dep:hyper",
|
||||
"dep:hyper-util",
|
||||
"dep:natpmp",
|
||||
"dep:xmltree",
|
||||
]
|
||||
endpoint-discovery = [
|
||||
"dns-resolver",
|
||||
"easytier-core/endpoint-discovery",
|
||||
|
||||
+16
-21
@@ -10,17 +10,14 @@ use easytier_core::connectivity::hole_punch::port_mapping::{
|
||||
ActiveUdpPortMapping as CoreActiveUdpPortMapping, UdpPortMappingAttemptError,
|
||||
UdpPortMappingBackend, UdpPortMappingLifecycle,
|
||||
};
|
||||
use igd_next::{
|
||||
AddAnyPortError, PortMappingProtocol, SearchOptions,
|
||||
aio::{
|
||||
Gateway,
|
||||
tokio::{Tokio, search_gateway},
|
||||
},
|
||||
};
|
||||
use natpmp::{
|
||||
Protocol as NatPmpProtocol, Response as NatPmpResponse, new_tokio_natpmp, new_tokio_natpmp_with,
|
||||
};
|
||||
|
||||
use crate::igd_next::{
|
||||
AddAnyPortError, Gateway, PortMappingProtocol, SearchOptions, search_gateway,
|
||||
};
|
||||
|
||||
use super::netns::NetNS;
|
||||
|
||||
const UPNP_SEARCH_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
@@ -29,11 +26,9 @@ const NAT_PMP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const UPNP_LEASE_DURATION_SECS: u32 = 300;
|
||||
const UPNP_DESCRIPTION: &str = "EasyTier udp hole punch";
|
||||
|
||||
type TokioGateway = Gateway<Tokio>;
|
||||
|
||||
enum PortMappingBackend {
|
||||
NatPmp { gateway: Ipv4Addr },
|
||||
Igd { gateway: TokioGateway },
|
||||
Igd { gateway: Gateway },
|
||||
}
|
||||
|
||||
struct ActiveUdpPortMapping {
|
||||
@@ -95,7 +90,7 @@ impl ActiveUdpPortMapping {
|
||||
async fn discover_igd_gateway(
|
||||
net_ns: &NetNS,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<(TokioGateway, SocketAddr)> {
|
||||
) -> anyhow::Result<(Gateway, SocketAddr)> {
|
||||
let _g = net_ns.guard();
|
||||
let gateway = search_gateway(SearchOptions {
|
||||
timeout: Some(UPNP_SEARCH_TIMEOUT),
|
||||
@@ -111,7 +106,7 @@ impl ActiveUdpPortMapping {
|
||||
|
||||
async fn establish_via_igd(
|
||||
local_listener: &url::Url,
|
||||
gateway: TokioGateway,
|
||||
gateway: Gateway,
|
||||
local_addr: SocketAddr,
|
||||
) -> anyhow::Result<Self> {
|
||||
let gateway_external_port = add_udp_mapping_port_igd(&gateway, local_addr, local_listener)
|
||||
@@ -252,7 +247,7 @@ pub(crate) fn spawn_udp_port_mapping_lifecycle(
|
||||
async fn discover_igd_gateway_in_netns(
|
||||
net_ns: NetNS,
|
||||
local_listener: url::Url,
|
||||
) -> anyhow::Result<(TokioGateway, SocketAddr)> {
|
||||
) -> anyhow::Result<(Gateway, SocketAddr)> {
|
||||
if !should_run_port_mapping_in_dedicated_thread(&net_ns) {
|
||||
return ActiveUdpPortMapping::discover_igd_gateway(&net_ns, &local_listener).await;
|
||||
}
|
||||
@@ -275,7 +270,7 @@ async fn discover_igd_gateway_in_netns(
|
||||
async fn establish_igd_mapping_in_netns(
|
||||
net_ns: NetNS,
|
||||
local_listener: url::Url,
|
||||
gateway: TokioGateway,
|
||||
gateway: Gateway,
|
||||
local_addr: SocketAddr,
|
||||
) -> anyhow::Result<ActiveUdpPortMapping> {
|
||||
if !should_run_port_mapping_in_dedicated_thread(&net_ns) {
|
||||
@@ -352,13 +347,13 @@ fn should_run_port_mapping_in_dedicated_thread(net_ns: &NetNS) -> bool {
|
||||
}
|
||||
|
||||
async fn add_udp_mapping_port_igd(
|
||||
gateway: &TokioGateway,
|
||||
gateway: &Gateway,
|
||||
local_addr: SocketAddr,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<u16> {
|
||||
match gateway
|
||||
.add_any_port(
|
||||
PortMappingProtocol::UDP,
|
||||
PortMappingProtocol::Udp,
|
||||
local_addr,
|
||||
UPNP_LEASE_DURATION_SECS,
|
||||
UPNP_DESCRIPTION,
|
||||
@@ -377,7 +372,7 @@ async fn add_udp_mapping_port_igd(
|
||||
|
||||
gateway
|
||||
.add_port(
|
||||
PortMappingProtocol::UDP,
|
||||
PortMappingProtocol::Udp,
|
||||
local_addr.port(),
|
||||
local_addr,
|
||||
UPNP_LEASE_DURATION_SECS,
|
||||
@@ -533,14 +528,14 @@ async fn resolve_internal_addr(
|
||||
}
|
||||
|
||||
async fn renew_udp_mapping_igd(
|
||||
gateway: &TokioGateway,
|
||||
gateway: &Gateway,
|
||||
local_addr: SocketAddr,
|
||||
external_port: u16,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<()> {
|
||||
gateway
|
||||
.add_port(
|
||||
PortMappingProtocol::UDP,
|
||||
PortMappingProtocol::Udp,
|
||||
external_port,
|
||||
local_addr,
|
||||
UPNP_LEASE_DURATION_SECS,
|
||||
@@ -551,12 +546,12 @@ async fn renew_udp_mapping_igd(
|
||||
}
|
||||
|
||||
async fn remove_udp_mapping_igd(
|
||||
gateway: &TokioGateway,
|
||||
gateway: &Gateway,
|
||||
external_port: u16,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<()> {
|
||||
gateway
|
||||
.remove_port(PortMappingProtocol::UDP, external_port)
|
||||
.remove_port(PortMappingProtocol::Udp, external_port)
|
||||
.await
|
||||
.with_context(|| format!("remove udp port mapping {local_listener}"))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Simon Bernier St-Pierre
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,126 @@
|
||||
use std::{io, string::FromUtf8Error};
|
||||
|
||||
use easytier_core::tunnel::TunnelError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HttpTransportError {
|
||||
#[error("socket error: {0}")]
|
||||
Socket(#[from] TunnelError),
|
||||
#[error("HTTP request error: {0}")]
|
||||
Http(#[from] hyper::http::Error),
|
||||
#[error("HTTP connection error: {0}")]
|
||||
Hyper(#[from] hyper::Error),
|
||||
#[error("HTTP response is not UTF-8: {0}")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
#[error("HTTP request timed out")]
|
||||
TimedOut,
|
||||
#[error("invalid HTTP response: {0}")]
|
||||
InvalidResponse(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum RequestError {
|
||||
#[error(transparent)]
|
||||
Http(#[from] HttpTransportError),
|
||||
#[error("invalid response from gateway: {0}")]
|
||||
InvalidResponse(String),
|
||||
#[error("gateway response error {0}: {1}")]
|
||||
ErrorCode(u16, String),
|
||||
#[error("gateway does not support action: {0}")]
|
||||
UnsupportedAction(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum SearchError {
|
||||
#[error("invalid response")]
|
||||
InvalidResponse,
|
||||
#[error("no response within timeout")]
|
||||
NoResponseWithinTimeout,
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("UTF-8 error: {0}")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
#[error("XML error: {0}")]
|
||||
Xml(#[from] xmltree::ParseError),
|
||||
#[error(transparent)]
|
||||
Http(#[from] HttpTransportError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum AddAnyPortError {
|
||||
#[error("the client is not authorized to map this port")]
|
||||
ActionNotAuthorized,
|
||||
#[error("cannot add a mapping for local port 0")]
|
||||
InternalPortZeroInvalid,
|
||||
#[error("the gateway does not have any free ports")]
|
||||
NoPortsAvailable,
|
||||
#[error("the required same-numbered external port is in use")]
|
||||
ExternalPortInUse,
|
||||
#[error("the gateway only supports permanent leases")]
|
||||
OnlyPermanentLeasesSupported,
|
||||
#[error("the description was too long for the gateway")]
|
||||
DescriptionTooLong,
|
||||
#[error("request error: {0}")]
|
||||
RequestError(#[from] RequestError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum AddPortError {
|
||||
#[error("the client is not authorized to map this port")]
|
||||
ActionNotAuthorized,
|
||||
#[error("cannot add a mapping for local port 0")]
|
||||
InternalPortZeroInvalid,
|
||||
#[error("external port 0 is invalid")]
|
||||
ExternalPortZeroInvalid,
|
||||
#[error("the requested port is in use")]
|
||||
PortInUse,
|
||||
#[error("the gateway requires matching internal and external ports")]
|
||||
SamePortValuesRequired,
|
||||
#[error("the gateway only supports permanent leases")]
|
||||
OnlyPermanentLeasesSupported,
|
||||
#[error("the description was too long for the gateway")]
|
||||
DescriptionTooLong,
|
||||
#[error("request error: {0}")]
|
||||
RequestError(#[source] RequestError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum RemovePortError {
|
||||
#[error("the client is not authorized to remove the port")]
|
||||
ActionNotAuthorized,
|
||||
#[error("the port was not mapped")]
|
||||
NoSuchPortMapping,
|
||||
#[error("request error: {0}")]
|
||||
RequestError(#[source] RequestError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[cfg(test)]
|
||||
pub(crate) enum GetExternalIpError {
|
||||
#[error("the client is not authorized to get the external IP address")]
|
||||
ActionNotAuthorized,
|
||||
#[error("request error: {0}")]
|
||||
RequestError(#[source] RequestError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[cfg(test)]
|
||||
pub(crate) enum GetGenericPortMappingEntryError {
|
||||
#[error("the client is not authorized to look up port mappings")]
|
||||
ActionNotAuthorized,
|
||||
#[error("the provided mapping index is invalid")]
|
||||
SpecifiedArrayIndexInvalid,
|
||||
#[error("request error: {0}")]
|
||||
RequestError(#[source] RequestError),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<RequestError> for GetGenericPortMappingEntryError {
|
||||
fn from(error: RequestError) -> Self {
|
||||
match error {
|
||||
RequestError::ErrorCode(606, _) => Self::ActionNotAuthorized,
|
||||
RequestError::ErrorCode(713, _) => Self::SpecifiedArrayIndexInvalid,
|
||||
other => Self::RequestError(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use super::PortMappingProtocol;
|
||||
|
||||
pub(super) const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r
|
||||
Host:239.255.255.250:1900\r
|
||||
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r
|
||||
Man:\"ssdp:discover\"\r
|
||||
MX:3\r\n\r\n";
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) const GET_EXTERNAL_IP_ACTION: &str = "GetExternalIPAddress";
|
||||
pub(super) const ADD_ANY_PORT_MAPPING_ACTION: &str = "AddAnyPortMapping";
|
||||
pub(super) const ADD_PORT_MAPPING_ACTION: &str = "AddPortMapping";
|
||||
pub(super) const DELETE_PORT_MAPPING_ACTION: &str = "DeletePortMapping";
|
||||
#[cfg(test)]
|
||||
pub(super) const GET_GENERIC_PORT_MAPPING_ENTRY_ACTION: &str = "GetGenericPortMappingEntry";
|
||||
|
||||
const MESSAGE_HEAD: &str = r#"<?xml version="1.0"?>
|
||||
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>"#;
|
||||
const MESSAGE_TAIL: &str = r#"</s:Body>
|
||||
</s:Envelope>"#;
|
||||
|
||||
pub(super) fn soap_action(service_type: &str, action: &str) -> String {
|
||||
format!("\"{service_type}#{action}\"")
|
||||
}
|
||||
|
||||
fn format_message(body: String) -> String {
|
||||
format!("{MESSAGE_HEAD}{body}{MESSAGE_TAIL}")
|
||||
}
|
||||
|
||||
fn xml_escape(input: &str) -> String {
|
||||
let mut output = String::with_capacity(input.len());
|
||||
for character in input.chars() {
|
||||
match character {
|
||||
'&' => output.push_str("&"),
|
||||
'<' => output.push_str("<"),
|
||||
'>' => output.push_str(">"),
|
||||
'"' => output.push_str("""),
|
||||
'\'' => output.push_str("'"),
|
||||
_ => output.push(character),
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn format_get_external_ip_message(service_type: &str) -> String {
|
||||
format_message(format!(
|
||||
r#"<m:GetExternalIPAddress xmlns:m="{service_type}">
|
||||
</m:GetExternalIPAddress>"#
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn format_add_any_port_mapping_message(
|
||||
service_type: &str,
|
||||
schema: &[String],
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> String {
|
||||
let arguments = format_mapping_arguments(
|
||||
schema,
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
);
|
||||
format_message(format!(
|
||||
r#"<u:AddAnyPortMapping xmlns:u="{service_type}">
|
||||
{arguments}
|
||||
</u:AddAnyPortMapping>"#
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn format_add_port_mapping_message(
|
||||
service_type: &str,
|
||||
schema: &[String],
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> String {
|
||||
let arguments = format_mapping_arguments(
|
||||
schema,
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
);
|
||||
format_message(format!(
|
||||
r#"<u:AddPortMapping xmlns:u="{service_type}">
|
||||
{arguments}
|
||||
</u:AddPortMapping>"#
|
||||
))
|
||||
}
|
||||
|
||||
fn format_mapping_arguments(
|
||||
schema: &[String],
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> String {
|
||||
schema
|
||||
.iter()
|
||||
.filter_map(|argument| {
|
||||
let value = match argument.as_str() {
|
||||
"NewEnabled" => "1".to_owned(),
|
||||
"NewExternalPort" => external_port.to_string(),
|
||||
"NewInternalClient" => local_addr.ip().to_string(),
|
||||
"NewInternalPort" => local_addr.port().to_string(),
|
||||
"NewLeaseDuration" => lease_duration.to_string(),
|
||||
"NewPortMappingDescription" => description.to_owned(),
|
||||
"NewProtocol" => protocol.to_string(),
|
||||
"NewRemoteHost" => String::new(),
|
||||
unknown => {
|
||||
tracing::warn!(argument = unknown, "unknown IGD SOAP argument");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(format!("<{argument}>{}</{argument}>", xml_escape(&value)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub(super) fn format_delete_port_message(
|
||||
service_type: &str,
|
||||
schema: &[String],
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
) -> String {
|
||||
let arguments = schema
|
||||
.iter()
|
||||
.filter_map(|argument| {
|
||||
let value = match argument.as_str() {
|
||||
"NewExternalPort" => external_port.to_string(),
|
||||
"NewProtocol" => protocol.to_string(),
|
||||
"NewRemoteHost" => String::new(),
|
||||
unknown => {
|
||||
tracing::warn!(argument = unknown, "unknown IGD SOAP argument");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(format!("<{argument}>{}</{argument}>", xml_escape(&value)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format_message(format!(
|
||||
r#"<u:DeletePortMapping xmlns:u="{service_type}">
|
||||
{arguments}
|
||||
</u:DeletePortMapping>"#
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn format_get_generic_port_mapping_entry_message(
|
||||
service_type: &str,
|
||||
port_mapping_index: u32,
|
||||
) -> String {
|
||||
format_message(format!(
|
||||
r#"<u:GetGenericPortMappingEntry xmlns:u="{service_type}">
|
||||
<NewPortMappingIndex>{port_mapping_index}</NewPortMappingIndex>
|
||||
</u:GetGenericPortMappingEntry>"#
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PPP_SERVICE: &str = "urn:schemas-upnp-org:service:WANPPPConnection:1";
|
||||
|
||||
#[test]
|
||||
fn soap_action_and_body_use_discovered_service_type() {
|
||||
assert_eq!(
|
||||
soap_action(PPP_SERVICE, ADD_PORT_MAPPING_ACTION),
|
||||
"\"urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping\""
|
||||
);
|
||||
|
||||
let body = format_add_port_mapping_message(
|
||||
PPP_SERVICE,
|
||||
&["NewProtocol".to_owned(), "NewExternalPort".to_owned()],
|
||||
PortMappingProtocol::Udp,
|
||||
12345,
|
||||
"192.168.1.5:80".parse().unwrap(),
|
||||
0,
|
||||
"test",
|
||||
);
|
||||
assert!(body.contains(r#"xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1""#));
|
||||
assert!(body.contains("<NewProtocol>UDP</NewProtocol>"));
|
||||
assert!(body.contains("<NewExternalPort>12345</NewExternalPort>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_description_is_xml_escaped() {
|
||||
let body = format_add_port_mapping_message(
|
||||
PPP_SERVICE,
|
||||
&["NewPortMappingDescription".to_owned()],
|
||||
PortMappingProtocol::Udp,
|
||||
12345,
|
||||
"192.168.1.5:80".parse().unwrap(),
|
||||
0,
|
||||
"Bob & Alice </NewPortMappingDescription><evil>",
|
||||
);
|
||||
assert!(body.contains("Bob & Alice </NewPortMappingDescription><evil>"));
|
||||
assert!(!body.contains("<evil>"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
// Derived from igd-next 0.17.0 and upstream commit
|
||||
// bb335d60348386526684d9249d7ae733772279e4, with its network transport
|
||||
// replaced by EasyTier's socket factories. See LICENSE in this
|
||||
// directory for the upstream license.
|
||||
|
||||
mod errors;
|
||||
mod messages;
|
||||
mod parsing;
|
||||
|
||||
use std::{collections::HashMap, fmt, net::SocketAddr, time::Duration};
|
||||
|
||||
#[cfg(test)]
|
||||
use std::net::IpAddr;
|
||||
|
||||
use bytes::Bytes;
|
||||
use easytier_core::socket::{
|
||||
tcp::TcpConnectOptions,
|
||||
udp::{UdpBindOptions, VirtualUdpSocket, VirtualUdpSocketFactory},
|
||||
};
|
||||
use http_body_util::{BodyExt as _, Full, Limited};
|
||||
use hyper::{
|
||||
Method, Request,
|
||||
client::conn::http1,
|
||||
header::{CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, HOST},
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rand::Rng as _;
|
||||
use tokio::time::timeout;
|
||||
|
||||
pub(crate) use errors::AddAnyPortError;
|
||||
#[cfg(test)]
|
||||
use errors::GetExternalIpError;
|
||||
#[cfg(test)]
|
||||
pub(crate) use errors::GetGenericPortMappingEntryError;
|
||||
use errors::{AddPortError, HttpTransportError, RemovePortError, RequestError, SearchError};
|
||||
|
||||
use crate::socket::{tcp::connect_tcp, udp::RuntimeUdpSocketFactory};
|
||||
|
||||
const DEFAULT_SEARCH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_SSDP_RESPONSE_SIZE: usize = 1500;
|
||||
const MAX_HTTP_RESPONSE_SIZE: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PortMappingProtocol {
|
||||
#[cfg(test)]
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
|
||||
impl fmt::Display for PortMappingProtocol {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
#[cfg(test)]
|
||||
Self::Tcp => "TCP",
|
||||
Self::Udp => "UDP",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SearchOptions {
|
||||
pub(crate) bind_addr: SocketAddr,
|
||||
pub(crate) broadcast_address: SocketAddr,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
pub(crate) single_search_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for SearchOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bind_addr: "0.0.0.0:0".parse().unwrap(),
|
||||
broadcast_address: "239.255.255.250:1900".parse().unwrap(),
|
||||
timeout: Some(DEFAULT_SEARCH_TIMEOUT),
|
||||
single_search_timeout: Some(DEFAULT_RESPONSE_TIMEOUT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Gateway {
|
||||
pub(crate) addr: SocketAddr,
|
||||
control_url: String,
|
||||
control_schema: HashMap<String, Vec<String>>,
|
||||
service_type: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) struct PortMappingEntry {
|
||||
pub(crate) external_port: u16,
|
||||
pub(crate) protocol: PortMappingProtocol,
|
||||
pub(crate) internal_port: u16,
|
||||
pub(crate) internal_client: String,
|
||||
pub(crate) port_mapping_description: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn search_gateway(options: SearchOptions) -> Result<Gateway, SearchError> {
|
||||
let search_timeout = options.timeout.unwrap_or(DEFAULT_SEARCH_TIMEOUT);
|
||||
timeout(search_timeout, search_gateway_inner(options))
|
||||
.await
|
||||
.map_err(|_| SearchError::NoResponseWithinTimeout)?
|
||||
}
|
||||
|
||||
async fn search_gateway_inner(options: SearchOptions) -> Result<Gateway, SearchError> {
|
||||
let bind_options = UdpBindOptions::direct_connect().with_local_addr(Some(options.bind_addr));
|
||||
let socket = RuntimeUdpSocketFactory::new()
|
||||
.bind_udp(bind_options)
|
||||
.await
|
||||
.map_err(|error| SearchError::Io(std::io::Error::other(error.to_string())))?;
|
||||
socket
|
||||
.send_to(
|
||||
messages::SEARCH_REQUEST.as_bytes(),
|
||||
options.broadcast_address,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response_timeout = options
|
||||
.single_search_timeout
|
||||
.unwrap_or(DEFAULT_RESPONSE_TIMEOUT);
|
||||
loop {
|
||||
let mut buffer = [0_u8; MAX_SSDP_RESPONSE_SIZE];
|
||||
let (length, from) = match timeout(response_timeout, socket.recv_from(&mut buffer)).await {
|
||||
Ok(Ok(response)) => response,
|
||||
Ok(Err(error)) => {
|
||||
tracing::debug!(?error, "failed to receive IGD discovery response");
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("timed out waiting for IGD discovery response");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let response = match std::str::from_utf8(&buffer[..length]) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
tracing::debug!(?error, %from, "invalid IGD discovery response encoding");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (addr, root_url) = match parsing::parse_search_result(response) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
tracing::debug!(?error, %from, "invalid IGD discovery response");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (service_type, schema_url, control_url) = match get_control_urls(addr, &root_url).await
|
||||
{
|
||||
Ok(urls) => urls,
|
||||
Err(error) => {
|
||||
tracing::debug!(?error, %addr, "failed to read IGD device description");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let control_schema = match get_control_schema(addr, &schema_url).await {
|
||||
Ok(schema) => schema,
|
||||
Err(error) => {
|
||||
tracing::debug!(?error, %addr, "failed to read IGD control schema");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
return Ok(Gateway {
|
||||
addr,
|
||||
control_url,
|
||||
control_schema,
|
||||
service_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_control_urls(
|
||||
addr: SocketAddr,
|
||||
root_url: &str,
|
||||
) -> Result<(String, String, String), SearchError> {
|
||||
let response = send_http_request(addr, root_url, Method::GET, None, String::new()).await?;
|
||||
parsing::parse_control_urls(response.as_ref())
|
||||
}
|
||||
|
||||
async fn get_control_schema(
|
||||
addr: SocketAddr,
|
||||
schema_url: &str,
|
||||
) -> Result<HashMap<String, Vec<String>>, SearchError> {
|
||||
let response = send_http_request(addr, schema_url, Method::GET, None, String::new()).await?;
|
||||
parsing::parse_schemas(response.as_ref())
|
||||
}
|
||||
|
||||
async fn send_http_request(
|
||||
addr: SocketAddr,
|
||||
path: &str,
|
||||
method: Method,
|
||||
soap_action: Option<&str>,
|
||||
body: String,
|
||||
) -> Result<Bytes, HttpTransportError> {
|
||||
timeout(
|
||||
DEFAULT_REQUEST_TIMEOUT,
|
||||
send_http_request_inner(addr, path, method, soap_action, body),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| HttpTransportError::TimedOut)?
|
||||
}
|
||||
|
||||
async fn send_http_request_inner(
|
||||
addr: SocketAddr,
|
||||
path: &str,
|
||||
method: Method,
|
||||
soap_action: Option<&str>,
|
||||
body: String,
|
||||
) -> Result<Bytes, HttpTransportError> {
|
||||
let stream = connect_tcp(TcpConnectOptions::direct_connect(addr)).await?;
|
||||
let (mut sender, connection) = http1::handshake(TokioIo::new(stream)).await?;
|
||||
let body = Bytes::from(body);
|
||||
let mut request = Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
.header(HOST, addr.to_string())
|
||||
.header(CONNECTION, "close")
|
||||
.header(CONTENT_LENGTH, body.len() as u64);
|
||||
if let Some(action) = soap_action {
|
||||
request = request
|
||||
.header("SOAPAction", action)
|
||||
.header(CONTENT_TYPE, "text/xml; charset=utf-8");
|
||||
}
|
||||
let request = request.body(Full::new(body))?;
|
||||
|
||||
let response = async move {
|
||||
let response = sender.send_request(request).await?;
|
||||
let body = Limited::new(response.into_body(), MAX_HTTP_RESPONSE_SIZE)
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|error| HttpTransportError::InvalidResponse(error.to_string()))?
|
||||
.to_bytes();
|
||||
Ok::<_, HttpTransportError>(body)
|
||||
};
|
||||
tokio::pin!(response);
|
||||
tokio::pin!(connection);
|
||||
tokio::select! {
|
||||
biased;
|
||||
result = &mut response => result,
|
||||
result = &mut connection => {
|
||||
result?;
|
||||
response.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Gateway {
|
||||
async fn perform_request(
|
||||
&self,
|
||||
action: &str,
|
||||
body: &str,
|
||||
expected_response: &str,
|
||||
) -> parsing::RequestResult {
|
||||
let soap_action = messages::soap_action(&self.service_type, action);
|
||||
let response = send_http_request(
|
||||
self.addr,
|
||||
&self.control_url,
|
||||
Method::POST,
|
||||
Some(&soap_action),
|
||||
body.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
let response = String::from_utf8(response.to_vec()).map_err(HttpTransportError::from)?;
|
||||
parsing::parse_response(response, expected_response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn get_external_ip(&self) -> Result<IpAddr, GetExternalIpError> {
|
||||
let result = self
|
||||
.perform_request(
|
||||
messages::GET_EXTERNAL_IP_ACTION,
|
||||
&messages::format_get_external_ip_message(&self.service_type),
|
||||
"GetExternalIPAddressResponse",
|
||||
)
|
||||
.await;
|
||||
parsing::parse_get_external_ip_response(result)
|
||||
}
|
||||
|
||||
pub(crate) async fn add_any_port(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<u16, AddAnyPortError> {
|
||||
if local_addr.port() == 0 {
|
||||
return Err(AddAnyPortError::InternalPortZeroInvalid);
|
||||
}
|
||||
|
||||
if let Some(schema) = self
|
||||
.control_schema
|
||||
.get(messages::ADD_ANY_PORT_MAPPING_ACTION)
|
||||
{
|
||||
let external_port = random_port();
|
||||
let result = self
|
||||
.perform_request(
|
||||
messages::ADD_ANY_PORT_MAPPING_ACTION,
|
||||
&messages::format_add_any_port_mapping_message(
|
||||
&self.service_type,
|
||||
schema,
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
),
|
||||
"AddAnyPortMappingResponse",
|
||||
)
|
||||
.await;
|
||||
return parsing::parse_add_any_port_mapping_response(result);
|
||||
}
|
||||
|
||||
self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retry_add_random_port_mapping(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<u16, AddAnyPortError> {
|
||||
for _ in 0..20 {
|
||||
match self
|
||||
.add_random_port_mapping(protocol, local_addr, lease_duration, description)
|
||||
.await
|
||||
{
|
||||
Err(AddAnyPortError::NoPortsAvailable) => continue,
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
Err(AddAnyPortError::NoPortsAvailable)
|
||||
}
|
||||
|
||||
async fn add_random_port_mapping(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<u16, AddAnyPortError> {
|
||||
let external_port = random_port();
|
||||
match self
|
||||
.add_port_mapping(
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(external_port),
|
||||
Err(error) => match parsing::convert_add_random_port_mapping_error(error) {
|
||||
Some(error) => Err(error),
|
||||
None => {
|
||||
self.add_same_port_mapping(protocol, local_addr, lease_duration, description)
|
||||
.await
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_same_port_mapping(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<u16, AddAnyPortError> {
|
||||
self.add_port_mapping(
|
||||
protocol,
|
||||
local_addr.port(),
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
)
|
||||
.await
|
||||
.map(|()| local_addr.port())
|
||||
.map_err(parsing::convert_add_same_port_mapping_error)
|
||||
}
|
||||
|
||||
async fn add_port_mapping(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<(), RequestError> {
|
||||
let schema = self
|
||||
.control_schema
|
||||
.get(messages::ADD_PORT_MAPPING_ACTION)
|
||||
.ok_or_else(|| {
|
||||
RequestError::UnsupportedAction(messages::ADD_PORT_MAPPING_ACTION.to_owned())
|
||||
})?;
|
||||
self.perform_request(
|
||||
messages::ADD_PORT_MAPPING_ACTION,
|
||||
&messages::format_add_port_mapping_message(
|
||||
&self.service_type,
|
||||
schema,
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
),
|
||||
"AddPortMappingResponse",
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn add_port(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
local_addr: SocketAddr,
|
||||
lease_duration: u32,
|
||||
description: &str,
|
||||
) -> Result<(), AddPortError> {
|
||||
if external_port == 0 {
|
||||
return Err(AddPortError::ExternalPortZeroInvalid);
|
||||
}
|
||||
if local_addr.port() == 0 {
|
||||
return Err(AddPortError::InternalPortZeroInvalid);
|
||||
}
|
||||
self.add_port_mapping(
|
||||
protocol,
|
||||
external_port,
|
||||
local_addr,
|
||||
lease_duration,
|
||||
description,
|
||||
)
|
||||
.await
|
||||
.map_err(parsing::convert_add_port_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_port(
|
||||
&self,
|
||||
protocol: PortMappingProtocol,
|
||||
external_port: u16,
|
||||
) -> Result<(), RemovePortError> {
|
||||
let schema = self
|
||||
.control_schema
|
||||
.get(messages::DELETE_PORT_MAPPING_ACTION)
|
||||
.ok_or_else(|| {
|
||||
RemovePortError::RequestError(RequestError::UnsupportedAction(
|
||||
messages::DELETE_PORT_MAPPING_ACTION.to_owned(),
|
||||
))
|
||||
})?;
|
||||
let result = self
|
||||
.perform_request(
|
||||
messages::DELETE_PORT_MAPPING_ACTION,
|
||||
&messages::format_delete_port_message(
|
||||
&self.service_type,
|
||||
schema,
|
||||
protocol,
|
||||
external_port,
|
||||
),
|
||||
"DeletePortMappingResponse",
|
||||
)
|
||||
.await;
|
||||
parsing::parse_delete_port_mapping_response(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn get_generic_port_mapping_entry(
|
||||
&self,
|
||||
index: u32,
|
||||
) -> Result<PortMappingEntry, GetGenericPortMappingEntryError> {
|
||||
let result = self
|
||||
.perform_request(
|
||||
messages::GET_GENERIC_PORT_MAPPING_ENTRY_ACTION,
|
||||
&messages::format_get_generic_port_mapping_entry_message(&self.service_type, index),
|
||||
"GetGenericPortMappingEntryResponse",
|
||||
)
|
||||
.await;
|
||||
parsing::parse_get_generic_port_mapping_entry(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn random_port() -> u16 {
|
||||
rand::thread_rng().gen_range(32_768..65_535)
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
use std::{collections::HashMap, io, net::SocketAddr};
|
||||
|
||||
#[cfg(test)]
|
||||
use std::str::FromStr;
|
||||
|
||||
use url::Url;
|
||||
use xmltree::Element;
|
||||
|
||||
use super::errors::{AddAnyPortError, AddPortError, RemovePortError, RequestError, SearchError};
|
||||
#[cfg(test)]
|
||||
use super::{
|
||||
PortMappingEntry, PortMappingProtocol,
|
||||
errors::{GetExternalIpError, GetGenericPortMappingEntryError},
|
||||
};
|
||||
|
||||
pub(super) fn parse_search_result(text: &str) -> Result<(SocketAddr, String), SearchError> {
|
||||
for line in text.lines().map(str::trim) {
|
||||
let Some((name, value)) = line.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
if !name.eq_ignore_ascii_case("location") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let url = Url::parse(value.trim()).map_err(|_| SearchError::InvalidResponse)?;
|
||||
let host = url.host_str().ok_or(SearchError::InvalidResponse)?;
|
||||
let ip = host.parse().map_err(|_| SearchError::InvalidResponse)?;
|
||||
let port = url
|
||||
.port_or_known_default()
|
||||
.ok_or(SearchError::InvalidResponse)?;
|
||||
let mut path = url.path().to_owned();
|
||||
if let Some(query) = url.query() {
|
||||
path.push('?');
|
||||
path.push_str(query);
|
||||
}
|
||||
return Ok((SocketAddr::new(ip, port), path));
|
||||
}
|
||||
Err(SearchError::InvalidResponse)
|
||||
}
|
||||
|
||||
pub(super) fn parse_control_urls<R>(response: R) -> Result<(String, String, String), SearchError>
|
||||
where
|
||||
R: io::Read,
|
||||
{
|
||||
let root = Element::parse(response)?;
|
||||
root.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.find(|child| child.name == "device")
|
||||
.and_then(parse_device)
|
||||
.ok_or(SearchError::InvalidResponse)
|
||||
}
|
||||
|
||||
fn parse_device(device: &Element) -> Option<(String, String, String)> {
|
||||
let service = device.get_child("serviceList").and_then(|list| {
|
||||
list.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.filter(|child| child.name == "service")
|
||||
.find_map(parse_service)
|
||||
});
|
||||
service.or_else(|| {
|
||||
device.get_child("deviceList").and_then(|list| {
|
||||
list.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.filter(|child| child.name == "device")
|
||||
.find_map(parse_device)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_service(service: &Element) -> Option<(String, String, String)> {
|
||||
let service_type = service.get_child("serviceType")?.get_text()?.into_owned();
|
||||
if ![
|
||||
"urn:schemas-upnp-org:service:WANPPPConnection:1",
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
||||
]
|
||||
.contains(&service_type.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let schema_url = service.get_child("SCPDURL")?.get_text()?.into_owned();
|
||||
let control_url = service.get_child("controlURL")?.get_text()?.into_owned();
|
||||
Some((service_type, schema_url, control_url))
|
||||
}
|
||||
|
||||
pub(super) fn parse_schemas<R>(response: R) -> Result<HashMap<String, Vec<String>>, SearchError>
|
||||
where
|
||||
R: io::Read,
|
||||
{
|
||||
let root = Element::parse(response)?;
|
||||
root.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.find(|child| child.name == "actionList")
|
||||
.map(parse_action_list)
|
||||
.ok_or(SearchError::InvalidResponse)
|
||||
}
|
||||
|
||||
fn parse_action_list(action_list: &Element) -> HashMap<String, Vec<String>> {
|
||||
action_list
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.filter(|child| child.name == "action")
|
||||
.filter_map(parse_action)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_action(action: &Element) -> Option<(String, Vec<String>)> {
|
||||
let name = action.get_child("name")?.get_text()?.into_owned();
|
||||
let arguments = action
|
||||
.get_child("argumentList")?
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|child| child.as_element())
|
||||
.filter(|child| child.name == "argument")
|
||||
.filter_map(parse_input_argument)
|
||||
.collect();
|
||||
Some((name, arguments))
|
||||
}
|
||||
|
||||
fn parse_input_argument(argument: &Element) -> Option<String> {
|
||||
if argument.get_child("direction")?.get_text()?.as_ref() != "in" {
|
||||
return None;
|
||||
}
|
||||
argument
|
||||
.get_child("name")?
|
||||
.get_text()
|
||||
.map(|name| name.into_owned())
|
||||
}
|
||||
|
||||
pub(super) struct RequestResponse {
|
||||
text: String,
|
||||
xml: Element,
|
||||
}
|
||||
|
||||
pub(super) type RequestResult = Result<RequestResponse, RequestError>;
|
||||
|
||||
pub(super) fn parse_response(text: String, expected: &str) -> RequestResult {
|
||||
let mut xml =
|
||||
Element::parse(text.as_bytes()).map_err(|_| RequestError::InvalidResponse(text.clone()))?;
|
||||
let body = xml
|
||||
.get_mut_child("Body")
|
||||
.ok_or_else(|| RequestError::InvalidResponse(text.clone()))?;
|
||||
if let Some(response) = body.take_child(expected) {
|
||||
return Ok(RequestResponse {
|
||||
text,
|
||||
xml: response,
|
||||
});
|
||||
}
|
||||
|
||||
let upnp_error = body
|
||||
.get_child("Fault")
|
||||
.and_then(|fault| fault.get_child("detail"))
|
||||
.and_then(|detail| detail.get_child("UPnPError"))
|
||||
.ok_or_else(|| RequestError::InvalidResponse(text.clone()))?;
|
||||
let code = upnp_error
|
||||
.get_child("errorCode")
|
||||
.and_then(Element::get_text)
|
||||
.and_then(|value| value.parse::<u16>().ok())
|
||||
.ok_or_else(|| RequestError::InvalidResponse(text.clone()))?;
|
||||
let description = upnp_error
|
||||
.get_child("errorDescription")
|
||||
.and_then(Element::get_text)
|
||||
.map(|value| value.into_owned())
|
||||
.ok_or_else(|| RequestError::InvalidResponse(text.clone()))?;
|
||||
Err(RequestError::ErrorCode(code, description))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn parse_get_external_ip_response(
|
||||
result: RequestResult,
|
||||
) -> Result<std::net::IpAddr, GetExternalIpError> {
|
||||
match result {
|
||||
Ok(response) => response
|
||||
.xml
|
||||
.get_child("NewExternalIPAddress")
|
||||
.and_then(Element::get_text)
|
||||
.and_then(|text| text.parse().ok())
|
||||
.ok_or_else(|| {
|
||||
GetExternalIpError::RequestError(RequestError::InvalidResponse(response.text))
|
||||
}),
|
||||
Err(RequestError::ErrorCode(606, _)) => Err(GetExternalIpError::ActionNotAuthorized),
|
||||
Err(error) => Err(GetExternalIpError::RequestError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_add_any_port_mapping_response(
|
||||
result: RequestResult,
|
||||
) -> Result<u16, AddAnyPortError> {
|
||||
match result {
|
||||
Ok(response) => response
|
||||
.xml
|
||||
.get_child("NewReservedPort")
|
||||
.and_then(Element::get_text)
|
||||
.and_then(|text| text.parse().ok())
|
||||
.ok_or_else(|| {
|
||||
AddAnyPortError::RequestError(RequestError::InvalidResponse(response.text))
|
||||
}),
|
||||
Err(error) => Err(match error {
|
||||
RequestError::ErrorCode(605, _) => AddAnyPortError::DescriptionTooLong,
|
||||
RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized,
|
||||
RequestError::ErrorCode(728, _) => AddAnyPortError::NoPortsAvailable,
|
||||
other => AddAnyPortError::RequestError(other),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn convert_add_random_port_mapping_error(
|
||||
error: RequestError,
|
||||
) -> Option<AddAnyPortError> {
|
||||
match error {
|
||||
RequestError::ErrorCode(724, _) => None,
|
||||
RequestError::ErrorCode(605, _) => Some(AddAnyPortError::DescriptionTooLong),
|
||||
RequestError::ErrorCode(606, _) => Some(AddAnyPortError::ActionNotAuthorized),
|
||||
RequestError::ErrorCode(718, _) => Some(AddAnyPortError::NoPortsAvailable),
|
||||
RequestError::ErrorCode(725, _) => Some(AddAnyPortError::OnlyPermanentLeasesSupported),
|
||||
other => Some(AddAnyPortError::RequestError(other)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn convert_add_same_port_mapping_error(error: RequestError) -> AddAnyPortError {
|
||||
match error {
|
||||
RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized,
|
||||
RequestError::ErrorCode(718, _) => AddAnyPortError::ExternalPortInUse,
|
||||
RequestError::ErrorCode(725, _) => AddAnyPortError::OnlyPermanentLeasesSupported,
|
||||
other => AddAnyPortError::RequestError(other),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn convert_add_port_error(error: RequestError) -> AddPortError {
|
||||
match error {
|
||||
RequestError::ErrorCode(605, _) => AddPortError::DescriptionTooLong,
|
||||
RequestError::ErrorCode(606, _) => AddPortError::ActionNotAuthorized,
|
||||
RequestError::ErrorCode(718, _) => AddPortError::PortInUse,
|
||||
RequestError::ErrorCode(724, _) => AddPortError::SamePortValuesRequired,
|
||||
RequestError::ErrorCode(725, _) => AddPortError::OnlyPermanentLeasesSupported,
|
||||
other => AddPortError::RequestError(other),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_delete_port_mapping_response(
|
||||
result: RequestResult,
|
||||
) -> Result<(), RemovePortError> {
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(RequestError::ErrorCode(606, _)) => Err(RemovePortError::ActionNotAuthorized),
|
||||
Err(RequestError::ErrorCode(714, _)) => Err(RemovePortError::NoSuchPortMapping),
|
||||
Err(error) => Err(RemovePortError::RequestError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn parse_get_generic_port_mapping_entry(
|
||||
result: RequestResult,
|
||||
) -> Result<PortMappingEntry, GetGenericPortMappingEntryError> {
|
||||
let response = result?;
|
||||
let xml = response.xml;
|
||||
let invalid = |message: String| {
|
||||
GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(message))
|
||||
};
|
||||
let field = |name: &str| {
|
||||
xml.get_child(name)
|
||||
.ok_or_else(|| invalid(format!("{name} is missing")))
|
||||
};
|
||||
field("NewRemoteHost")?;
|
||||
let external_port = parse_number(&xml, "NewExternalPort")?;
|
||||
let protocol = match field("NewProtocol")?.get_text().as_deref() {
|
||||
Some("UDP") => PortMappingProtocol::Udp,
|
||||
Some("TCP") => PortMappingProtocol::Tcp,
|
||||
_ => return Err(invalid("NewProtocol is invalid".to_owned())),
|
||||
};
|
||||
let internal_port = parse_number(&xml, "NewInternalPort")?;
|
||||
let internal_client = field("NewInternalClient")?
|
||||
.get_text()
|
||||
.map(|text| text.into_owned())
|
||||
.ok_or_else(|| invalid("NewInternalClient is empty".to_owned()))?;
|
||||
match parse_number::<u16>(&xml, "NewEnabled")? {
|
||||
0 | 1 => {}
|
||||
_ => return Err(invalid("NewEnabled is invalid".to_owned())),
|
||||
}
|
||||
let port_mapping_description = field("NewPortMappingDescription")?
|
||||
.get_text()
|
||||
.map(|text| text.into_owned())
|
||||
.unwrap_or_default();
|
||||
parse_number::<u32>(&xml, "NewLeaseDuration")?;
|
||||
|
||||
Ok(PortMappingEntry {
|
||||
external_port,
|
||||
protocol,
|
||||
internal_port,
|
||||
internal_client,
|
||||
port_mapping_description,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_number<T: FromStr>(
|
||||
xml: &Element,
|
||||
name: &str,
|
||||
) -> Result<T, GetGenericPortMappingEntryError> {
|
||||
xml.get_child(name)
|
||||
.and_then(Element::get_text)
|
||||
.and_then(|text| text.parse().ok())
|
||||
.ok_or_else(|| {
|
||||
GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(format!(
|
||||
"{name} is missing or invalid"
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn search_location_is_case_insensitive_and_keeps_query() {
|
||||
let (address, path) = parse_search_result(
|
||||
"HTTP/1.1 200 OK\r\nLOCATION: http://192.168.1.1:5431/root.xml?v=2\r\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(address, "192.168.1.1:5431".parse().unwrap());
|
||||
assert_eq!(path, "/root.xml?v=2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_ppp_service_is_discovered() {
|
||||
let description = r#"
|
||||
<root><device><deviceList><device><deviceList><device><serviceList>
|
||||
<service>
|
||||
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
|
||||
<controlURL>/control</controlURL>
|
||||
<SCPDURL>/schema.xml</SCPDURL>
|
||||
</service>
|
||||
</serviceList></device></deviceList></device></deviceList></device></root>
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_control_urls(description.as_bytes()).unwrap(),
|
||||
(
|
||||
"urn:schemas-upnp-org:service:WANPPPConnection:1".to_owned(),
|
||||
"/schema.xml".to_owned(),
|
||||
"/control".to_owned(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_description_uses_declared_xml_encoding() {
|
||||
let description = r#"<?xml version="1.0" encoding="UTF-16"?>
|
||||
<root><device><serviceList><service>
|
||||
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
||||
<controlURL>/control</controlURL>
|
||||
<SCPDURL>/schema.xml</SCPDURL>
|
||||
</service></serviceList></device></root>"#;
|
||||
let mut encoded = vec![0xff, 0xfe];
|
||||
for code_unit in description.encode_utf16() {
|
||||
encoded.extend_from_slice(&code_unit.to_le_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_control_urls(encoded.as_slice()).unwrap(),
|
||||
(
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:1".to_owned(),
|
||||
"/schema.xml".to_owned(),
|
||||
"/control".to_owned(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_contains_only_input_arguments() {
|
||||
let schema = r#"
|
||||
<scpd><actionList><action><name>AddPortMapping</name><argumentList>
|
||||
<argument><name>NewExternalPort</name><direction>in</direction></argument>
|
||||
<argument><name>Result</name><direction>out</direction></argument>
|
||||
<argument><name>NewProtocol</name><direction>in</direction></argument>
|
||||
</argumentList></action></actionList></scpd>
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_schemas(schema.as_bytes()).unwrap()["AddPortMapping"],
|
||||
["NewExternalPort", "NewProtocol"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soap_fault_preserves_error_code() {
|
||||
let response = r#"
|
||||
<Envelope><Body><Fault><detail><UPnPError>
|
||||
<errorCode>713</errorCode><errorDescription>SpecifiedArrayIndexInvalid</errorDescription>
|
||||
</UPnPError></detail></Fault></Body></Envelope>
|
||||
"#;
|
||||
assert!(matches!(
|
||||
parse_response(response.to_owned(), "GetGenericPortMappingEntryResponse"),
|
||||
Err(RequestError::ErrorCode(713, _))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ mod vpn_portal;
|
||||
pub mod common;
|
||||
#[cfg(feature = "management")]
|
||||
pub mod core;
|
||||
#[cfg(feature = "upnp")]
|
||||
pub(crate) mod igd_next;
|
||||
pub mod proto;
|
||||
#[cfg(feature = "management-rpc")]
|
||||
pub mod rpc_service;
|
||||
|
||||
@@ -7,15 +7,15 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::igd_next::{
|
||||
GetGenericPortMappingEntryError, PortMappingEntry, PortMappingProtocol, SearchOptions,
|
||||
search_gateway,
|
||||
};
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use easytier_core::{
|
||||
connectivity::stun::{StunInfoProvider, StunSocketMapper},
|
||||
process_runtime::CoreProcessRuntime,
|
||||
};
|
||||
use igd_next::{
|
||||
GetGenericPortMappingEntryError, PortMappingEntry, PortMappingProtocol, SearchOptions,
|
||||
aio::tokio::search_gateway,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::{
|
||||
@@ -843,7 +843,7 @@ async fn query_mappings(
|
||||
for index in 0..64 {
|
||||
match gateway.get_generic_port_mapping_entry(index).await {
|
||||
Ok(entry) => {
|
||||
if entry.protocol == PortMappingProtocol::UDP
|
||||
if entry.protocol == PortMappingProtocol::Udp
|
||||
&& entry.port_mapping_description == TEST_IGD_DESCRIPTION
|
||||
{
|
||||
entries.push(entry);
|
||||
|
||||
Reference in new issue
Block a user