Browse Source

Merge pull request #616 from paullouisageneau/enhance-url-decoding

Enhance URL decoding
Paul-Louis Ageneau 3 years ago
parent
commit
dc3170bc85

+ 0 - 2
CMakeLists.txt

@@ -118,7 +118,6 @@ set(LIBDATACHANNEL_IMPL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/track.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/track.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/utils.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/utils.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/processor.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/processor.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/base64.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/sha.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/sha.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollinterrupter.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollinterrupter.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollservice.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollservice.cpp
@@ -150,7 +149,6 @@ set(LIBDATACHANNEL_IMPL_HEADERS
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/track.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/track.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/utils.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/utils.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/processor.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/processor.hpp
-	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/base64.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/sha.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/sha.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollinterrupter.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollinterrupter.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollservice.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/pollservice.hpp

+ 3 - 1
DOC.md

@@ -114,10 +114,12 @@ Return value: the identifier of the new Peer Connection or a negative error code
 
 
 The Peer Connection must be deleted with `rtcDeletePeerConnection`.
 The Peer Connection must be deleted with `rtcDeletePeerConnection`.
 
 
-Each entry in `iceServers` must match the format `[("stun"|"turn"|"turns") (":"|"://")][username ":" password "@"]hostname[":" port]["?transport=" ("udp"|"tcp"|"tls")]`. The default scheme is STUN, the default port is 3478 (5349 over TLS), and the default transport is UDP.  For instance, a STUN server URI could be `mystunserver.org`, and a TURN server URI could be `turn:myuser:[email protected]`. Note transports TCP and TLS are only available for a TURN server with libnice as ICE backend and govern only the TURN control connection, meaning relaying is always performed over UDP.
+Each entry in `iceServers` must match the format `[("stun"|"turn"|"turns") (":"|"://")][username ":" password "@"]hostname[":" port]["?transport=" ("udp"|"tcp"|"tls")]`. The default scheme is STUN, the default port is 3478 (5349 over TLS), and the default transport is UDP. For instance, a STUN server URI could be `mystunserver.org`, and a TURN server URI could be `turn:myuser:[email protected]`. Note transports TCP and TLS are only available for a TURN server with libnice as ICE backend and govern only the TURN control connection, meaning relaying is always performed over UDP.
 
 
 The `proxyServer` URI, if present, must match the format `[("http"|"socks5") (":"|"://")][username ":" password "@"]hostname["    :" port]`. The default scheme is HTTP, and the default port is 3128 for HTTP or 1080 for SOCKS5.
 The `proxyServer` URI, if present, must match the format `[("http"|"socks5") (":"|"://")][username ":" password "@"]hostname["    :" port]`. The default scheme is HTTP, and the default port is 3128 for HTTP or 1080 for SOCKS5.
 
 
+If the username or password of an URI contains reserved special characters, they must be percent-encoded. In particular, ":" must be encoded as "%3A" and "@" must by encoded as "%40".
+
 #### rtcDeletePeerConnection
 #### rtcDeletePeerConnection
 
 
 ```
 ```

+ 2 - 1
pages/content/pages/reference.md

@@ -117,10 +117,11 @@ Return value: the identifier of the new Peer Connection or a negative error code
 
 
 The Peer Connection must be deleted with `rtcDeletePeerConnection`.
 The Peer Connection must be deleted with `rtcDeletePeerConnection`.
 
 
-Each entry in `iceServers` must match the format `[("stun"|"turn"|"turns") (":"|"://")][username ":" password "@"]hostname[":" port]["?transport=" ("udp"|"tcp"|"tls")]`. The default scheme is STUN, the default port is 3478 (5349 over TLS), and the default transport is UDP.  For instance, a STUN server URI could be `mystunserver.org`, and a TURN server URI could be `turn:myuser:[email protected]`. Note transports TCP and TLS are only available for a TURN server with libnice as ICE backend and govern only the TURN control connection, meaning relaying is always performed over UDP.
+Each entry in `iceServers` must match the format `[("stun"|"turn"|"turns") (":"|"://")][username ":" password "@"]hostname[":" port]["?transport=" ("udp"|"tcp"|"tls")]`. The default scheme is STUN, the default port is 3478 (5349 over TLS), and the default transport is UDP. For instance, a STUN server URI could be `mystunserver.org`, and a TURN server URI could be `turn:myuser:[email protected]`. Note transports TCP and TLS are only available for a TURN server with libnice as ICE backend and govern only the TURN control connection, meaning relaying is always performed over UDP.
 
 
 The `proxyServer` URI, if present, must match the format `[("http"|"socks5") (":"|"://")][username ":" password "@"]hostname["    :" port]`. The default scheme is HTTP, and the default port is 3128 for HTTP or 1080 for SOCKS5.
 The `proxyServer` URI, if present, must match the format `[("http"|"socks5") (":"|"://")][username ":" password "@"]hostname["    :" port]`. The default scheme is HTTP, and the default port is 3128 for HTTP or 1080 for SOCKS5.
 
 
+If the username or password of an URI contains reserved special characters, they must be percent-encoded. In particular, ":" must be encoded as "%3A" and "@" must by encoded as "%40".
 
 
 #### rtcDeletePeerConnection
 #### rtcDeletePeerConnection
 
 

+ 11 - 24
src/configuration.cpp

@@ -18,12 +18,11 @@
 
 
 #include "configuration.hpp"
 #include "configuration.hpp"
 
 
+#include "impl/utils.hpp"
+
 #include <cassert>
 #include <cassert>
 #include <regex>
 #include <regex>
 
 
-#include <iostream>
-#include <sstream>
-
 namespace {
 namespace {
 
 
 bool parse_url(const std::string &url, std::vector<std::optional<std::string>> &result) {
 bool parse_url(const std::string &url, std::vector<std::optional<std::string>> &result) {
@@ -45,27 +44,12 @@ bool parse_url(const std::string &url, std::vector<std::optional<std::string>> &
 	return true;
 	return true;
 }
 }
 
 
-std::string url_decode(const std::string &str) {
-	static const std::regex r(R"(%[0-9A-Fa-f]{2})", std::regex::extended);
-
-	std::stringstream ss;
-	std::smatch m;
-	for (size_t i = 0; i < str.length(); ++i) {
-		std::string substr = str.substr(i, 3);
-		if (std::regex_match(substr, m, r)) {
-			ss << static_cast<char>(std::stoi("0x" + substr.substr(1, 2), nullptr, 16));
-			i += 2;
-		} else {
-			ss << str[i];
-		}
-	}
-	return ss.str();
-}
-
 } // namespace
 } // namespace
 
 
 namespace rtc {
 namespace rtc {
 
 
+namespace utils = impl::utils;
+
 IceServer::IceServer(const string &url) {
 IceServer::IceServer(const string &url) {
 	std::vector<optional<string>> opt;
 	std::vector<optional<string>> opt;
 	if (!parse_url(url, opt))
 	if (!parse_url(url, opt))
@@ -92,14 +76,17 @@ IceServer::IceServer(const string &url) {
 			relayType = RelayType::TurnTls;
 			relayType = RelayType::TurnTls;
 	}
 	}
 
 
-	username = url_decode(opt[6].value_or(""));
-	password = url_decode(opt[8].value_or(""));
+	username = utils::url_decode(opt[6].value_or(""));
+	password = utils::url_decode(opt[8].value_or(""));
 
 
 	hostname = opt[10].value();
 	hostname = opt[10].value();
-	while (!hostname.empty() && hostname.front() == '[')
+	if(hostname.front() == '[' && hostname.back() == ']') {
+		// IPv6 literal
 		hostname.erase(hostname.begin());
 		hostname.erase(hostname.begin());
-	while (!hostname.empty() && hostname.back() == ']')
 		hostname.pop_back();
 		hostname.pop_back();
+	} else {
+		hostname = utils::url_decode(hostname);
+	}
 
 
 	string service = opt[12].value_or(relayType == RelayType::TurnTls ? "5349" : "3478");
 	string service = opt[12].value_or(relayType == RelayType::TurnTls ? "5349" : "3478");
 	try {
 	try {

+ 0 - 64
src/impl/base64.cpp

@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2020 Paul-Louis Ageneau
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#if RTC_ENABLE_WEBSOCKET
-
-#include "base64.hpp"
-
-namespace rtc::impl {
-
-using std::to_integer;
-
-string to_base64(const binary &data) {
-	static const char tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
-	string out;
-	out.reserve(3 * ((data.size() + 3) / 4));
-	int i = 0;
-	while (data.size() - i >= 3) {
-		auto d0 = to_integer<uint8_t>(data[i]);
-		auto d1 = to_integer<uint8_t>(data[i + 1]);
-		auto d2 = to_integer<uint8_t>(data[i + 2]);
-		out += tab[d0 >> 2];
-		out += tab[((d0 & 3) << 4) | (d1 >> 4)];
-		out += tab[((d1 & 0x0F) << 2) | (d2 >> 6)];
-		out += tab[d2 & 0x3F];
-		i += 3;
-	}
-
-	int left = int(data.size() - i);
-	if (left) {
-		auto d0 = to_integer<uint8_t>(data[i]);
-		out += tab[d0 >> 2];
-		if (left == 1) {
-			out += tab[(d0 & 3) << 4];
-			out += '=';
-		} else { // left == 2
-			auto d1 = to_integer<uint8_t>(data[i + 1]);
-			out += tab[((d0 & 3) << 4) | (d1 >> 4)];
-			out += tab[(d1 & 0x0F) << 2];
-		}
-		out += '=';
-	}
-
-	return out;
-}
-
-} // namespace rtc::impl
-
-#endif

+ 0 - 34
src/impl/base64.hpp

@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2020 Paul-Louis Ageneau
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#ifndef RTC_IMPL_BASE64_H
-#define RTC_IMPL_BASE64_H
-
-#if RTC_ENABLE_WEBSOCKET
-
-#include "common.hpp"
-
-namespace rtc::impl {
-
-string to_base64(const binary &data);
-
-}
-
-#endif
-
-#endif

+ 66 - 0
src/impl/utils.cpp

@@ -18,11 +18,17 @@
 
 
 #include "utils.hpp"
 #include "utils.hpp"
 
 
+#include "impl/internals.hpp"
+
+#include <cctype>
+#include <functional>
 #include <iterator>
 #include <iterator>
 #include <sstream>
 #include <sstream>
 
 
 namespace rtc::impl::utils {
 namespace rtc::impl::utils {
 
 
+using std::to_integer;
+
 std::vector<string> explode(const string &str, char delim) {
 std::vector<string> explode(const string &str, char delim) {
 	std::vector<std::string> result;
 	std::vector<std::string> result;
 	std::istringstream ss(str);
 	std::istringstream ss(str);
@@ -44,4 +50,64 @@ string implode(const std::vector<string> &tokens, char delim) {
 	return result;
 	return result;
 }
 }
 
 
+string url_decode(const string &str) {
+	string result;
+	size_t i = 0;
+	while (i < str.size()) {
+		char c = str[i++];
+		if (c == '%') {
+			auto value = str.substr(i, 2);
+			try {
+				if (value.size() != 2 || !std::isxdigit(value[0]) || !std::isxdigit(value[1]))
+					throw std::exception();
+
+				c = static_cast<char>(std::stoi(value, nullptr, 16));
+				i += 2;
+
+			} catch (...) {
+				PLOG_WARNING << "Invalid percent-encoded character in URL: \"%" + value + "\"";
+			}
+		}
+
+		result.push_back(c);
+	}
+
+	return result;
+}
+
+string base64_encode(const binary &data) {
+	static const char tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+	string out;
+	out.reserve(3 * ((data.size() + 3) / 4));
+	int i = 0;
+	while (data.size() - i >= 3) {
+		auto d0 = to_integer<uint8_t>(data[i]);
+		auto d1 = to_integer<uint8_t>(data[i + 1]);
+		auto d2 = to_integer<uint8_t>(data[i + 2]);
+		out += tab[d0 >> 2];
+		out += tab[((d0 & 3) << 4) | (d1 >> 4)];
+		out += tab[((d1 & 0x0F) << 2) | (d2 >> 6)];
+		out += tab[d2 & 0x3F];
+		i += 3;
+	}
+
+	int left = int(data.size() - i);
+	if (left) {
+		auto d0 = to_integer<uint8_t>(data[i]);
+		out += tab[d0 >> 2];
+		if (left == 1) {
+			out += tab[(d0 & 3) << 4];
+			out += '=';
+		} else { // left == 2
+			auto d1 = to_integer<uint8_t>(data[i + 1]);
+			out += tab[((d0 & 3) << 4) | (d1 >> 4)];
+			out += tab[(d1 & 0x0F) << 2];
+		}
+		out += '=';
+	}
+
+	return out;
+}
+
 } // namespace rtc::impl::utils
 } // namespace rtc::impl::utils

+ 8 - 0
src/impl/utils.hpp

@@ -28,6 +28,14 @@ namespace rtc::impl::utils {
 std::vector<string> explode(const string &str, char delim);
 std::vector<string> explode(const string &str, char delim);
 string implode(const std::vector<string> &tokens, char delim);
 string implode(const std::vector<string> &tokens, char delim);
 
 
+// Decode URL percent-encoding (RFC 3986)
+// See https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1
+string url_decode(const string &str);
+
+// Encode as base64 (RFC 4648)
+// See https://www.rfc-editor.org/rfc/rfc4648.html#section-4
+string base64_encode(const binary &data);
+
 } // namespace rtc::impl
 } // namespace rtc::impl
 
 
 #endif
 #endif

+ 9 - 7
src/impl/websocket.cpp

@@ -22,6 +22,7 @@
 #include "common.hpp"
 #include "common.hpp"
 #include "internals.hpp"
 #include "internals.hpp"
 #include "threadpool.hpp"
 #include "threadpool.hpp"
+#include "utils.hpp"
 
 
 #include "tcptransport.hpp"
 #include "tcptransport.hpp"
 #include "tlstransport.hpp"
 #include "tlstransport.hpp"
@@ -46,9 +47,7 @@ WebSocket::WebSocket(optional<Configuration> optConfig, certificate_ptr certific
 	PLOG_VERBOSE << "Creating WebSocket";
 	PLOG_VERBOSE << "Creating WebSocket";
 }
 }
 
 
-WebSocket::~WebSocket() {
-	PLOG_VERBOSE << "Destroying WebSocket";
-}
+WebSocket::~WebSocket() { PLOG_VERBOSE << "Destroying WebSocket"; }
 
 
 void WebSocket::open(const string &url) {
 void WebSocket::open(const string &url) {
 	PLOG_VERBOSE << "Opening WebSocket to URL: " << url;
 	PLOG_VERBOSE << "Opening WebSocket to URL: " << url;
@@ -79,8 +78,8 @@ void WebSocket::open(const string &url) {
 
 
 	mIsSecure = (scheme != "ws");
 	mIsSecure = (scheme != "ws");
 
 
-	string username = m[6];
-	string password = m[8];
+	string username = utils::url_decode(m[6]);
+	string password = utils::url_decode(m[8]);
 	if (!username.empty() || !password.empty()) {
 	if (!username.empty() || !password.empty()) {
 		PLOG_WARNING << "HTTP authentication support for WebSocket is not implemented";
 		PLOG_WARNING << "HTTP authentication support for WebSocket is not implemented";
 	}
 	}
@@ -95,10 +94,13 @@ void WebSocket::open(const string &url) {
 		host = hostname + ':' + service;
 		host = hostname + ':' + service;
 	}
 	}
 
 
-	while (!hostname.empty() && hostname.front() == '[')
+	if (hostname.front() == '[' && hostname.back() == ']') {
+		// IPv6 literal
 		hostname.erase(hostname.begin());
 		hostname.erase(hostname.begin());
-	while (!hostname.empty() && hostname.back() == ']')
 		hostname.pop_back();
 		hostname.pop_back();
+	} else {
+		hostname = utils::url_decode(hostname);
+	}
 
 
 	string path = m[13];
 	string path = m[13];
 	if (path.empty())
 	if (path.empty())

+ 2 - 3
src/impl/wshandshake.cpp

@@ -17,7 +17,6 @@
  */
  */
 
 
 #include "wshandshake.hpp"
 #include "wshandshake.hpp"
-#include "base64.hpp"
 #include "internals.hpp"
 #include "internals.hpp"
 #include "sha.hpp"
 #include "sha.hpp"
 #include "utils.hpp"
 #include "utils.hpp"
@@ -246,11 +245,11 @@ string WsHandshake::generateKey() {
 	binary key(16);
 	binary key(16);
 	auto k = reinterpret_cast<uint8_t *>(key.data());
 	auto k = reinterpret_cast<uint8_t *>(key.data());
 	std::generate(k, k + key.size(), [&]() { return uint8_t(generator()); });
 	std::generate(k, k + key.size(), [&]() { return uint8_t(generator()); });
-	return to_base64(key);
+	return utils::base64_encode(key);
 }
 }
 
 
 string WsHandshake::computeAcceptKey(const string &key) {
 string WsHandshake::computeAcceptKey(const string &key) {
-	return to_base64(Sha1(string(key) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
+	return utils::base64_encode(Sha1(string(key) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
 }
 }
 
 
 size_t WsHandshake::parseHttpLines(const byte *buffer, size_t size, std::list<string> &lines) {
 size_t WsHandshake::parseHttpLines(const byte *buffer, size_t size, std::list<string> &lines) {

+ 2 - 2
test/turn_connectivity.cpp

@@ -50,9 +50,9 @@ void test_turn_connectivity() {
 	// STUN server example (not necessary, just here for testing)
 	// STUN server example (not necessary, just here for testing)
 	// Please do not use outside of libdatachannel tests
 	// Please do not use outside of libdatachannel tests
 	config1.iceServers.emplace_back("stun:stun.ageneau.net:3478");
 	config1.iceServers.emplace_back("stun:stun.ageneau.net:3478");
-	// TURN server example
+	// TURN server example (note that the reserved special character '@' is percent-encoded)
 	// Please do not use outside of libdatachannel tests
 	// Please do not use outside of libdatachannel tests
-	config2.iceServers.emplace_back("turn:datachannel_test:[email protected]:3478");
+	config2.iceServers.emplace_back("turn:datachannel_test_speci%40l:[email protected]:3478");
 
 
 	PeerConnection pc2(config2);
 	PeerConnection pc2(config2);