mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-12-23 12:26:46 +00:00
refactor(mstsgu): follow up to PR 913 (#920)
Some checks are pending
CI / Fuzzing (push) Blocked by required conditions
CI / Check formatting (push) Waiting to run
CI / Check typos (push) Waiting to run
CI / Checks [linux] (push) Blocked by required conditions
CI / Checks [macos] (push) Blocked by required conditions
CI / Checks [windows] (push) Blocked by required conditions
CI / Web Client (push) Blocked by required conditions
CI / FFI (push) Blocked by required conditions
CI / Success (push) Blocked by required conditions
Coverage / Coverage Report (push) Waiting to run
Release crates / Open release PR (push) Waiting to run
Release crates / Release crates (push) Waiting to run
Some checks are pending
CI / Fuzzing (push) Blocked by required conditions
CI / Check formatting (push) Waiting to run
CI / Check typos (push) Waiting to run
CI / Checks [linux] (push) Blocked by required conditions
CI / Checks [macos] (push) Blocked by required conditions
CI / Checks [windows] (push) Blocked by required conditions
CI / Web Client (push) Blocked by required conditions
CI / FFI (push) Blocked by required conditions
CI / Success (push) Blocked by required conditions
Coverage / Coverage Report (push) Waiting to run
Release crates / Open release PR (push) Waiting to run
Release crates / Release crates (push) Waiting to run
- Update tokio-tungstenite to latest - Fix the dependencies - Move the top-level documentation into a README.md that we re-include in the source code - Re-order the imports using the nightly formater - Audit the unwraps and remove them - Fix the UTF-16 string length computation
This commit is contained in:
parent
c84b46be91
commit
27f5504508
8 changed files with 132 additions and 163 deletions
|
|
@ -256,6 +256,10 @@ State machines to drive an RDP connection acceptance sequence
|
||||||
|
|
||||||
Extendable skeleton for implementing custom RDP servers.
|
Extendable skeleton for implementing custom RDP servers.
|
||||||
|
|
||||||
|
#### [`crates/ironrdp-mstsgu`](./crates/ironrdp-mstsgu) (@steffengy)
|
||||||
|
|
||||||
|
Terminal Services Gateway Server Protocol implementation.
|
||||||
|
|
||||||
#### [`crates/ironrdp-glutin-renderer`](./crates/ironrdp-glutin-renderer) (no maintainer)
|
#### [`crates/ironrdp-glutin-renderer`](./crates/ironrdp-glutin-renderer) (no maintainer)
|
||||||
|
|
||||||
`glutin` primitives for OpenGL rendering.
|
`glutin` primitives for OpenGL rendering.
|
||||||
|
|
|
||||||
141
Cargo.lock
generated
141
Cargo.lock
generated
|
|
@ -1510,7 +1510,7 @@ dependencies = [
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"ironrdp",
|
"ironrdp",
|
||||||
"ironrdp-cliprdr-native",
|
"ironrdp-cliprdr-native",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"sspi",
|
"sspi",
|
||||||
"thiserror 2.0.14",
|
"thiserror 2.0.14",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2332,7 +2332,7 @@ dependencies = [
|
||||||
"ironrdp-cliprdr",
|
"ironrdp-cliprdr",
|
||||||
"ironrdp-cliprdr-native",
|
"ironrdp-cliprdr-native",
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-displaycontrol",
|
"ironrdp-displaycontrol",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-graphics",
|
"ironrdp-graphics",
|
||||||
|
|
@ -2359,7 +2359,7 @@ version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-async",
|
"ironrdp-async",
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2370,7 +2370,7 @@ name = "ironrdp-ainput"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|
@ -2382,7 +2382,7 @@ version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
@ -2403,7 +2403,7 @@ version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
@ -2426,14 +2426,14 @@ dependencies = [
|
||||||
"ironrdp",
|
"ironrdp",
|
||||||
"ironrdp-cfg",
|
"ironrdp-cfg",
|
||||||
"ironrdp-cliprdr-native",
|
"ironrdp-cliprdr-native",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-dvc-pipe-proxy",
|
"ironrdp-dvc-pipe-proxy",
|
||||||
"ironrdp-mstsgu",
|
"ironrdp-mstsgu",
|
||||||
"ironrdp-propertyset",
|
"ironrdp-propertyset",
|
||||||
"ironrdp-rdcleanpath",
|
"ironrdp-rdcleanpath",
|
||||||
"ironrdp-rdpfile",
|
"ironrdp-rdpfile",
|
||||||
"ironrdp-rdpsnd-native",
|
"ironrdp-rdpsnd-native",
|
||||||
"ironrdp-tls 0.1.3",
|
"ironrdp-tls",
|
||||||
"ironrdp-tokio",
|
"ironrdp-tokio",
|
||||||
"proc-exit",
|
"proc-exit",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
|
|
@ -2442,7 +2442,7 @@ dependencies = [
|
||||||
"softbuffer",
|
"softbuffer",
|
||||||
"tap",
|
"tap",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.27.0",
|
"tokio-tungstenite",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -2460,7 +2460,7 @@ name = "ironrdp-cliprdr"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2470,7 +2470,7 @@ dependencies = [
|
||||||
name = "ironrdp-cliprdr-format"
|
name = "ironrdp-cliprdr-format"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"png",
|
"png",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2479,7 +2479,7 @@ name = "ironrdp-cliprdr-native"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-cliprdr",
|
"ironrdp-cliprdr",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
@ -2489,8 +2489,8 @@ name = "ironrdp-connector"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-error 0.1.3",
|
"ironrdp-error",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"picky",
|
"picky",
|
||||||
|
|
@ -2506,23 +2506,14 @@ dependencies = [
|
||||||
name = "ironrdp-core"
|
name = "ironrdp-core"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-error 0.1.3",
|
"ironrdp-error",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ironrdp-core"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2db60a59716a84d09040d29c9e75e81545842510fccb0934c09b28e78b46680"
|
|
||||||
dependencies = [
|
|
||||||
"ironrdp-error 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironrdp-displaycontrol"
|
name = "ironrdp-displaycontrol"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
|
|
@ -2533,7 +2524,7 @@ dependencies = [
|
||||||
name = "ironrdp-dvc"
|
name = "ironrdp-dvc"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"slab",
|
"slab",
|
||||||
|
|
@ -2545,7 +2536,7 @@ name = "ironrdp-dvc-pipe-proxy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
|
|
@ -2557,12 +2548,6 @@ dependencies = [
|
||||||
name = "ironrdp-error"
|
name = "ironrdp-error"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ironrdp-error"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4a9d7794e854eef2f13fdf79c8502bcc567a75a15fd0522885f37739386a4cef"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironrdp-futures"
|
name = "ironrdp-futures"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -2579,7 +2564,7 @@ dependencies = [
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
"ironrdp-cliprdr",
|
"ironrdp-cliprdr",
|
||||||
"ironrdp-cliprdr-format",
|
"ironrdp-cliprdr-format",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-displaycontrol",
|
"ironrdp-displaycontrol",
|
||||||
"ironrdp-graphics",
|
"ironrdp-graphics",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
|
|
@ -2599,7 +2584,7 @@ dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"expect-test",
|
"expect-test",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
|
|
@ -2626,12 +2611,12 @@ dependencies = [
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ironrdp-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ironrdp-core",
|
||||||
"ironrdp-error 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ironrdp-error",
|
||||||
"ironrdp-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ironrdp-tls",
|
||||||
"log",
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.26.2",
|
"tokio-tungstenite",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -2645,8 +2630,8 @@ dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"der-parser",
|
"der-parser",
|
||||||
"expect-test",
|
"expect-test",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-error 0.1.3",
|
"ironrdp-error",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"md-5",
|
"md-5",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
|
|
@ -2683,8 +2668,8 @@ name = "ironrdp-rdpdr"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-error 0.1.3",
|
"ironrdp-error",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2694,7 +2679,7 @@ dependencies = [
|
||||||
name = "ironrdp-rdpdr-native"
|
name = "ironrdp-rdpdr-native"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-rdpdr",
|
"ironrdp-rdpdr",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
|
|
@ -2714,7 +2699,7 @@ name = "ironrdp-rdpsnd"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2744,7 +2729,7 @@ dependencies = [
|
||||||
"ironrdp-ainput",
|
"ironrdp-ainput",
|
||||||
"ironrdp-async",
|
"ironrdp-async",
|
||||||
"ironrdp-cliprdr",
|
"ironrdp-cliprdr",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-displaycontrol",
|
"ironrdp-displaycontrol",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-graphics",
|
"ironrdp-graphics",
|
||||||
|
|
@ -2768,10 +2753,10 @@ name = "ironrdp-session"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-displaycontrol",
|
"ironrdp-displaycontrol",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-error 0.1.3",
|
"ironrdp-error",
|
||||||
"ironrdp-graphics",
|
"ironrdp-graphics",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
"ironrdp-svc",
|
"ironrdp-svc",
|
||||||
|
|
@ -2789,7 +2774,7 @@ name = "ironrdp-svc"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-pdu",
|
"ironrdp-pdu",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2804,7 +2789,7 @@ dependencies = [
|
||||||
"ironrdp-cliprdr",
|
"ironrdp-cliprdr",
|
||||||
"ironrdp-cliprdr-format",
|
"ironrdp-cliprdr-format",
|
||||||
"ironrdp-connector",
|
"ironrdp-connector",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-displaycontrol",
|
"ironrdp-displaycontrol",
|
||||||
"ironrdp-dvc",
|
"ironrdp-dvc",
|
||||||
"ironrdp-fuzzing",
|
"ironrdp-fuzzing",
|
||||||
|
|
@ -2833,7 +2818,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"ironrdp",
|
"ironrdp",
|
||||||
"ironrdp-async",
|
"ironrdp-async",
|
||||||
"ironrdp-tls 0.1.3",
|
"ironrdp-tls",
|
||||||
"ironrdp-tokio",
|
"ironrdp-tokio",
|
||||||
"semver",
|
"semver",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -2851,18 +2836,6 @@ dependencies = [
|
||||||
"x509-cert",
|
"x509-cert",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ironrdp-tls"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fc807c143533f41e19bf323e8c7f78995953ce8427d37220dde53906cbd48a3"
|
|
||||||
dependencies = [
|
|
||||||
"tokio",
|
|
||||||
"tokio-native-tls",
|
|
||||||
"tokio-rustls",
|
|
||||||
"x509-cert",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironrdp-tokio"
|
name = "ironrdp-tokio"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
|
@ -2892,7 +2865,7 @@ dependencies = [
|
||||||
"iron-remote-desktop",
|
"iron-remote-desktop",
|
||||||
"ironrdp",
|
"ironrdp",
|
||||||
"ironrdp-cliprdr-format",
|
"ironrdp-cliprdr-format",
|
||||||
"ironrdp-core 0.1.5",
|
"ironrdp-core",
|
||||||
"ironrdp-futures",
|
"ironrdp-futures",
|
||||||
"ironrdp-propertyset",
|
"ironrdp-propertyset",
|
||||||
"ironrdp-rdcleanpath",
|
"ironrdp-rdcleanpath",
|
||||||
|
|
@ -5523,24 +5496,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-tungstenite"
|
|
||||||
version = "0.26.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
|
||||||
dependencies = [
|
|
||||||
"futures-util",
|
|
||||||
"log",
|
|
||||||
"native-tls",
|
|
||||||
"rustls",
|
|
||||||
"rustls-native-certs",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"tokio",
|
|
||||||
"tokio-native-tls",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tungstenite 0.26.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
|
|
@ -5556,7 +5511,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tungstenite 0.27.0",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5776,26 +5731,6 @@ version = "0.25.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tungstenite"
|
|
||||||
version = "0.26.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"data-encoding",
|
|
||||||
"http",
|
|
||||||
"httparse",
|
|
||||||
"log",
|
|
||||||
"native-tls",
|
|
||||||
"rand 0.9.2",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"sha1",
|
|
||||||
"thiserror 2.0.14",
|
|
||||||
"utf-8",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,7 @@ impl Config {
|
||||||
gw_endpoint: gw_addr,
|
gw_endpoint: gw_addr,
|
||||||
gw_user: String::new(),
|
gw_user: String::new(),
|
||||||
gw_pass: String::new(),
|
gw_pass: String::new(),
|
||||||
server: String::new(), // TODO non-standard port? also dont use here?
|
server: String::new(), // TODO: non-standard port? also dont use here?
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ name = "ironrdp-mstsgu"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "Terminal Services Gateway Server Protocol"
|
description = "Terminal Services Gateway Server Protocol"
|
||||||
|
publish = false # TODO: publish
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
|
@ -11,9 +12,9 @@ authors.workspace = true
|
||||||
keywords.workspace = true
|
keywords.workspace = true
|
||||||
categories.workspace = true
|
categories.workspace = true
|
||||||
|
|
||||||
#[lib]
|
[lib]
|
||||||
#doctest = false
|
doctest = false
|
||||||
#test = false
|
test = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
@ -21,20 +22,20 @@ rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"]
|
||||||
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"]
|
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = "2.9"
|
|
||||||
ironrdp-core = { version = "0.1", features = ["std"] }
|
|
||||||
ironrdp-error = { version = "0.1" }
|
|
||||||
tokio = { version = "1.43", features = ["macros", "rt"] }
|
|
||||||
tokio-util = { version = "0.7" }
|
|
||||||
tokio-tungstenite = { version = "0.26" }
|
|
||||||
ironrdp-tls = { "version" = "0.1.3" }
|
|
||||||
hyper = { version = "1.6", features = ["client", "http1"] }
|
|
||||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
|
||||||
http-body-util = { version = "0.1" }
|
|
||||||
futures-util = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
bitflags = "2.9"
|
||||||
|
futures-util = "0.3"
|
||||||
|
http-body-util = { version = "0.1" }
|
||||||
|
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||||
|
hyper = { version = "1.6", features = ["client", "http1"] }
|
||||||
|
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] }
|
||||||
|
ironrdp-error = { path = "../ironrdp-error", version = "0.1" }
|
||||||
|
ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" }
|
||||||
|
log = "0.4"
|
||||||
|
tokio-tungstenite = { version = "0.27" }
|
||||||
|
tokio-util = { version = "0.7" }
|
||||||
|
tokio = { version = "1.43", features = ["macros", "rt"] }
|
||||||
|
uuid = { version = "1.16", features = ["v4"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
11
crates/ironrdp-mstsgu/README.md
Normal file
11
crates/ironrdp-mstsgu/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# IronRDP MS-TSGU
|
||||||
|
|
||||||
|
[Terminal Services Gateway Server Protocol][MS-TSGU] implementation for IronRDP.
|
||||||
|
|
||||||
|
This crate
|
||||||
|
- implements an MVP state needed to connect through Microsoft RD Gateway,
|
||||||
|
- only supports the HTTPS protocol with WebSocket (and not the legacy HTTP, HTTP-RPC or UDP protocols),
|
||||||
|
- does not implement reconnection/reauthentication, and
|
||||||
|
- only supports basic auth.
|
||||||
|
|
||||||
|
[MS-TSGU]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
//! [MS-TSGU] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188
|
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||||
//! * This implements a MVP (in terms of recentness) state needed to connect through microsoft rdp gateway.
|
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||||
//! * This only supports the HTTPS protocol with Websocket (and not the legacy HTTP, HTTP-RPC or UDP protocols).
|
|
||||||
//! * This does not implement reconnection/reauthentication.
|
#[macro_use]
|
||||||
//! * This only supports basic auth.
|
mod macros;
|
||||||
|
|
||||||
|
mod proto;
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::fmt::Display;
|
use core::fmt::Display;
|
||||||
use core::pin::Pin;
|
use core::pin::Pin;
|
||||||
|
|
@ -22,17 +25,16 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
||||||
use tokio_tungstenite::tungstenite::http::{self};
|
use tokio_tungstenite::tungstenite::http;
|
||||||
use tokio_tungstenite::tungstenite::protocol::Role;
|
use tokio_tungstenite::tungstenite::protocol::Role;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
use tokio_tungstenite::WebSocketStream;
|
use tokio_tungstenite::WebSocketStream;
|
||||||
|
use tokio_util::sync::PollSender;
|
||||||
|
|
||||||
mod proto;
|
use self::proto::{
|
||||||
use proto::{
|
|
||||||
ChannelPkt, ChannelResp, DataPkt, HandshakeReqPkt, HandshakeRespPkt, HttpCapsTy, KeepalivePkt, PktHdr, PktTy,
|
ChannelPkt, ChannelResp, DataPkt, HandshakeReqPkt, HandshakeRespPkt, HttpCapsTy, KeepalivePkt, PktHdr, PktTy,
|
||||||
TunnelAuthPkt, TunnelAuthRespPkt, TunnelReqPkt, TunnelRespPkt,
|
TunnelAuthPkt, TunnelAuthRespPkt, TunnelReqPkt, TunnelRespPkt,
|
||||||
};
|
};
|
||||||
use tokio_util::sync::PollSender;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GwConnectTarget {
|
pub struct GwConnectTarget {
|
||||||
|
|
@ -50,9 +52,10 @@ type Error = ironrdp_error::Error<GwErrorKind>;
|
||||||
pub enum GwErrorKind {
|
pub enum GwErrorKind {
|
||||||
InvalidGwTarget,
|
InvalidGwTarget,
|
||||||
Connect,
|
Connect,
|
||||||
PacketEOF,
|
PacketEof,
|
||||||
UnsupportedFeature,
|
UnsupportedFeature,
|
||||||
Custom,
|
Custom,
|
||||||
|
Encode,
|
||||||
Decode,
|
Decode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,12 +77,13 @@ impl GwErrorExt for ironrdp_error::Error<GwErrorKind> {
|
||||||
impl Display for GwErrorKind {
|
impl Display for GwErrorKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let x = match self {
|
let x = match self {
|
||||||
GwErrorKind::InvalidGwTarget => "Invalid GW Target",
|
GwErrorKind::InvalidGwTarget => "invalid GW Target",
|
||||||
GwErrorKind::Connect => "Connection error",
|
GwErrorKind::Connect => "connection error",
|
||||||
GwErrorKind::PacketEOF => "PacketEOF",
|
GwErrorKind::PacketEof => "PacketEOF",
|
||||||
GwErrorKind::UnsupportedFeature => "Unsupported feature",
|
GwErrorKind::UnsupportedFeature => "unsupported feature",
|
||||||
GwErrorKind::Custom => "Custom",
|
GwErrorKind::Custom => "custom",
|
||||||
GwErrorKind::Decode => "Decode",
|
GwErrorKind::Encode => "encode",
|
||||||
|
GwErrorKind::Decode => "decode",
|
||||||
};
|
};
|
||||||
f.write_str(x)
|
f.write_str(x)
|
||||||
}
|
}
|
||||||
|
|
@ -87,14 +91,6 @@ impl Display for GwErrorKind {
|
||||||
|
|
||||||
impl core::error::Error for GwErrorKind {}
|
impl core::error::Error for GwErrorKind {}
|
||||||
|
|
||||||
/// Creates a `ConnectorError` with `Custom` kind and a source error attached to it
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! custom_err {
|
|
||||||
( $context:expr, $source:expr $(,)? ) => {{
|
|
||||||
<$crate::Error as $crate::GwErrorExt>::custom($context, $source)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GwConn {
|
struct GwConn {
|
||||||
client_name: String,
|
client_name: String,
|
||||||
target: GwConnectTarget,
|
target: GwConnectTarget,
|
||||||
|
|
@ -270,13 +266,15 @@ impl GwConn {
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
let pos = {
|
let pos = {
|
||||||
let mut cur = WriteCursor::new(&mut buf);
|
let mut cur = WriteCursor::new(&mut buf);
|
||||||
payload.encode(&mut cur).unwrap();
|
payload
|
||||||
|
.encode(&mut cur)
|
||||||
|
.map_err(|e| Error::new("packet encode", GwErrorKind::Encode).with_source(e))?;
|
||||||
cur.pos()
|
cur.pos()
|
||||||
};
|
};
|
||||||
self.ws_sink
|
self.ws_sink
|
||||||
.send(Message::Binary(Bytes::copy_from_slice(&buf[..pos])))
|
.send(Message::Binary(Bytes::copy_from_slice(&buf[..pos])))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| custom_err!("WS Send error", e))?;
|
.map_err(|e| custom_err!("WebSocket send error", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,7 +290,7 @@ impl GwConn {
|
||||||
|
|
||||||
let hdr = PktHdr::decode(&mut cur).map_err(|_| Error::new("PktHdr", GwErrorKind::Decode))?;
|
let hdr = PktHdr::decode(&mut cur).map_err(|_| Error::new("PktHdr", GwErrorKind::Decode))?;
|
||||||
if cur.len() != hdr.length as usize - hdr.size() {
|
if cur.len() != hdr.length as usize - hdr.size() {
|
||||||
return Err(Error::new("read_packet", GwErrorKind::PacketEOF));
|
return Err(Error::new("read_packet", GwErrorKind::PacketEof));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((hdr, msg.split_off(cur.pos())))
|
Ok((hdr, msg.split_off(cur.pos())))
|
||||||
|
|
|
||||||
7
crates/ironrdp-mstsgu/src/macros.rs
Normal file
7
crates/ironrdp-mstsgu/src/macros.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/// Creates a [`crate::Error`] with `Custom` kind and a source error attached to it
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! custom_err {
|
||||||
|
( $context:expr, $source:expr $(,)? ) => {{
|
||||||
|
<$crate::Error as $crate::GwErrorExt>::custom($context, $source)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use ironrdp_core::{
|
use ironrdp_core::{
|
||||||
ensure_fixed_part_size, ensure_size, unsupported_value_err, Decode, Encode, ReadCursor, WriteCursor,
|
cast_int, cast_length, ensure_fixed_part_size, ensure_size, unsupported_value_err, Decode, Encode, ReadCursor,
|
||||||
|
WriteCursor,
|
||||||
};
|
};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
|
|
@ -70,7 +71,7 @@ pub(crate) struct PktHdr {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PktHdr {
|
impl PktHdr {
|
||||||
const FIXED_PART_SIZE: usize = 4 /* ty */ + 2/* _reserved */ + 2 /* length */;
|
const FIXED_PART_SIZE: usize = 4 /* ty */ + 2 /* _reserved */ + 2 /* length */;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encode for PktHdr {
|
impl Encode for PktHdr {
|
||||||
|
|
@ -89,7 +90,7 @@ impl Encode for PktHdr {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
8
|
Self::FIXED_PART_SIZE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +124,7 @@ impl Encode for HandshakeReqPkt {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::HandshakeReq,
|
ty: PktTy::HandshakeReq,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: u32::try_from(self.size()).expect("handshake packet size fits in u32"),
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
|
|
@ -141,7 +142,7 @@ impl Encode for HandshakeReqPkt {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
PktHdr::default().size() + 6
|
PktHdr::FIXED_PART_SIZE + 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,7 @@ impl Encode for TunnelReqPkt {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::TunnelCreate,
|
ty: PktTy::TunnelCreate,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: u32::try_from(self.size()).expect("tunnel request packet size fits in u32"),
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
|
|
@ -303,14 +304,16 @@ impl Encode for ExtendedAuthPkt {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::ExtendedAuth,
|
ty: PktTy::ExtendedAuth,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: cast_int!("packet length", self.size())?,
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
|
|
||||||
dst.write_u32(self.error_code);
|
dst.write_u32(self.error_code);
|
||||||
dst.write_u16(u16::try_from(self.blob.len()).unwrap());
|
let blob_len: u16 = cast_int!("blob length", self.blob.len())?;
|
||||||
|
dst.write_u16(blob_len);
|
||||||
dst.write_slice(&self.blob);
|
dst.write_slice(&self.blob);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,17 +352,23 @@ impl Encode for TunnelAuthPkt {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::TunnelAuth,
|
ty: PktTy::TunnelAuth,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: cast_int!("packet length", self.size())?,
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
|
|
||||||
dst.write_u16(self.fields_present);
|
dst.write_u16(self.fields_present);
|
||||||
dst.write_u16(u16::try_from(2 * (self.client_name.len() + 1)).unwrap());
|
|
||||||
|
let client_name_len = self.client_name.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000).
|
||||||
|
let client_name_len: u16 = cast_int!("client name length", client_name_len)?;
|
||||||
|
dst.write_u16(client_name_len);
|
||||||
|
|
||||||
for c in self.client_name.encode_utf16() {
|
for c in self.client_name.encode_utf16() {
|
||||||
dst.write_u16(c);
|
dst.write_u16(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
dst.write_u16(0);
|
dst.write_u16(0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,19 +418,22 @@ impl Encode for ChannelPkt {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::ChannelCreate,
|
ty: PktTy::ChannelCreate,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: cast_int!("packet length", self.size())?,
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
|
|
||||||
dst.write_u8(u8::try_from(self.resources.len()).unwrap());
|
let resources_count: u8 = cast_length!("resources count", self.resources.len())?;
|
||||||
|
dst.write_u8(resources_count);
|
||||||
dst.write_u8(0); // alt_names
|
dst.write_u8(0); // alt_names
|
||||||
dst.write_u16(self.port);
|
dst.write_u16(self.port);
|
||||||
dst.write_u16(self.protocol);
|
dst.write_u16(self.protocol);
|
||||||
|
|
||||||
// 2.2.10.3 HTTP_CHANNEL_PACKET_VARIABLE
|
// 2.2.10.3 HTTP_CHANNEL_PACKET_VARIABLE
|
||||||
for res in &self.resources {
|
for res in &self.resources {
|
||||||
dst.write_u16(u16::try_from(2 * (res.len() + 1)).unwrap());
|
let res_utf16_len = res.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000).
|
||||||
|
let res_len: u16 = cast_int!("resource name UTF-16 length", res_utf16_len)?;
|
||||||
|
dst.write_u16(res_len);
|
||||||
for b in res.encode_utf16() {
|
for b in res.encode_utf16() {
|
||||||
dst.write_u16(b);
|
dst.write_u16(b);
|
||||||
}
|
}
|
||||||
|
|
@ -496,11 +508,12 @@ impl Encode for DataPkt<'_> {
|
||||||
|
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::Data,
|
ty: PktTy::Data,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: cast_int!("packet length", self.size())?,
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)?;
|
hdr.encode(dst)?;
|
||||||
dst.write_u16(u16::try_from(self.data.len()).unwrap());
|
let data_len: u16 = cast_int!("data payload length", self.data.len())?;
|
||||||
|
dst.write_u16(data_len);
|
||||||
dst.write_slice(self.data);
|
dst.write_slice(self.data);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -531,7 +544,7 @@ impl Encode for KeepalivePkt {
|
||||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||||
let hdr = PktHdr {
|
let hdr = PktHdr {
|
||||||
ty: PktTy::Keepalive,
|
ty: PktTy::Keepalive,
|
||||||
length: u32::try_from(self.size()).unwrap(),
|
length: u32::try_from(self.size()).expect("keepalive packet size fits in u32"),
|
||||||
..PktHdr::default()
|
..PktHdr::default()
|
||||||
};
|
};
|
||||||
hdr.encode(dst)
|
hdr.encode(dst)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue