webrtc: decode TURN userinfo before handing it to the ICE agent

`Url` returns the username and password percent-encoded, and they went
into the ICE server configuration as-is, so a password containing `@`,
`/`, `?`, `#`, `%` or a space (written encoded, the only way the URL can
carry it) failed the long-term credential check with a 401 and TURN
never produced a relay candidate. Decode both, as the web client does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXJJGEGdgu26wgCppvUXdZ
This commit is contained in:
rustdeskandClaude Fable 5.1 committed 2026-09-04 23:34:08 +08:00
1 parent 846daf86f0
commit 00f0a720b5
2 files changed
+18 -2

No files matched your search

+1
View File
@@ -52,6 +52,7 @@ thiserror = "1.0"
httparse = "1.10"
base64 = "0.22"
url = "2.5"
percent-encoding = "2"
sha2 = "0.10"
whoami = "1.5"
+17 -2
View File
@@ -41,6 +41,7 @@ use webrtc::peer_connection::RTCPeerConnection;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use percent_encoding::percent_decode_str;
use tokio::sync::{mpsc, watch, Mutex, Semaphore};
use tokio::time::{timeout, timeout_at, Instant};
use url::Url;
@@ -395,8 +396,14 @@ impl WebRTCStream {
if host.is_empty() {
return None;
}
let username = u.username().to_string();
let credential = u.password().unwrap_or_default().to_string();
// `Url` hands userinfo back percent-encoded; the TURN server expects the value,
// so `p%40ss` must reach it as `p@ss` for the long-term credential to verify.
let username = percent_decode_str(u.username())
.decode_utf8_lossy()
.into_owned();
let credential = percent_decode_str(u.password().unwrap_or_default())
.decode_utf8_lossy()
.into_owned();
// A TURN server without credentials is not merely useless: webrtc-rs validates every
// configured server in `new_peer_connection`, so one credential-less entry makes EVERY
// peer connection fail — including plain non-relay ones that never wanted TURN. The
@@ -1468,6 +1475,14 @@ mod tests {
"turn:example.com:3478"
);
// Userinfo arrives percent-encoded from the URL parser; the TURN server wants the
// value, so a password written `p%40ss` authenticates as `p@ss`.
let encoded =
WebRTCStream::get_ice_server_from_url("turn://us%20er:p%40s%2Fs%25@example.com")
.expect("encoded credentials are usable");
assert_eq!(encoded.username, "us er");
assert_eq!(encoded.credential, "p@s/s%");
// TURN without both halves of the credential is dropped rather than passed on:
// webrtc-rs validates every configured server when the peer connection is built, so
// one such entry fails EVERY connection, including those that never wanted TURN.