Browse Source

Merge pull request #262 from paullouisageneau/fix-duplicate-candidates

Prevent duplicate candidates
Paul-Louis Ageneau 4 years ago
parent
commit
87df64a002
6 changed files with 181 additions and 124 deletions
  1. 16 7
      include/rtc/candidate.hpp
  2. 1 0
      include/rtc/description.hpp
  3. 133 104
      src/candidate.cpp
  4. 15 4
      src/description.cpp
  5. 4 2
      src/icetransport.cpp
  6. 12 7
      src/peerconnection.cpp

+ 16 - 7
include/rtc/candidate.hpp

@@ -40,29 +40,38 @@ public:
 	enum class ResolveMode { Simple, Lookup };
 	enum class ResolveMode { Simple, Lookup };
 	bool resolve(ResolveMode mode = ResolveMode::Simple);
 	bool resolve(ResolveMode mode = ResolveMode::Simple);
 
 
+	Type type() const;
+	TransportType transportType() const;
+	uint32_t priority() const;
 	string candidate() const;
 	string candidate() const;
 	string mid() const;
 	string mid() const;
 	operator string() const;
 	operator string() const;
 
 
+	bool operator==(const Candidate &other) const;
+	bool operator!=(const Candidate &other) const;
+
 	bool isResolved() const;
 	bool isResolved() const;
 	Family family() const;
 	Family family() const;
-	Type type() const;
-	TransportType transportType() const;
 	std::optional<string> address() const;
 	std::optional<string> address() const;
 	std::optional<uint16_t> port() const;
 	std::optional<uint16_t> port() const;
-	std::optional<uint32_t> priority() const;
 
 
 private:
 private:
-	string mCandidate;
+	void parse(string candidate);
+
+	string mFoundation;
+	uint32_t mComponent, mPriority;
+	string mTypeString, mTransportString;
+	Type mType;
+	TransportType mTransportType;
+	string mNode, mService;
+	string mTail;
+
 	std::optional<string> mMid;
 	std::optional<string> mMid;
 
 
 	// Extracted on resolution
 	// Extracted on resolution
 	Family mFamily;
 	Family mFamily;
-	Type mType;
-	TransportType mTransportType;
 	string mAddress;
 	string mAddress;
 	uint16_t mPort;
 	uint16_t mPort;
-	uint32_t mPriority;
 };
 };
 
 
 } // namespace rtc
 } // namespace rtc

+ 1 - 0
include/rtc/description.hpp

@@ -53,6 +53,7 @@ public:
 	void hintType(Type type);
 	void hintType(Type type);
 	void setFingerprint(string fingerprint);
 	void setFingerprint(string fingerprint);
 
 
+	bool hasCandidate(const Candidate &candidate) const;
 	void addCandidate(Candidate candidate);
 	void addCandidate(Candidate candidate);
 	void addCandidates(std::vector<Candidate> candidates);
 	void addCandidates(std::vector<Candidate> candidates);
 	void endCandidates();
 	void endCandidates();

+ 133 - 104
src/candidate.cpp

@@ -20,6 +20,7 @@
 
 
 #include <algorithm>
 #include <algorithm>
 #include <array>
 #include <array>
+#include <cctype>
 #include <sstream>
 #include <sstream>
 #include <unordered_map>
 #include <unordered_map>
 
 
@@ -39,39 +40,44 @@ using std::string;
 
 
 namespace {
 namespace {
 
 
-inline bool hasprefix(const string &str, const string &prefix) {
+inline bool match_prefix(const string &str, const string &prefix) {
 	return str.size() >= prefix.size() &&
 	return str.size() >= prefix.size() &&
 	       std::mismatch(prefix.begin(), prefix.end(), str.begin()).first == prefix.end();
 	       std::mismatch(prefix.begin(), prefix.end(), str.begin()).first == prefix.end();
 }
 }
 
 
+inline void trim_begin(string &str) {
+	str.erase(str.begin(),
+	          std::find_if(str.begin(), str.end(), [](char c) { return !std::isspace(c); }));
+}
+
+inline void trim_end(string &str) {
+	str.erase(
+	    std::find_if(str.rbegin(), str.rend(), [](char c) { return !std::isspace(c); }).base(),
+	    str.end());
+}
+
 } // namespace
 } // namespace
 
 
 namespace rtc {
 namespace rtc {
 
 
 Candidate::Candidate()
 Candidate::Candidate()
-    : mFamily(Family::Unresolved), mType(Type::Unknown), mTransportType(TransportType::Unknown),
-      mPort(0), mPriority(0) {}
+    : mFoundation("none"), mComponent(0), mPriority(0), mTypeString("unknown"),
+      mTransportString("unknown"), mType(Type::Unknown), mTransportType(TransportType::Unknown),
+      mNode("0.0.0.0"), mService("9"), mFamily(Family::Unresolved), mPort(0) {}
 
 
 Candidate::Candidate(string candidate) : Candidate() {
 Candidate::Candidate(string candidate) : Candidate() {
-	const std::array prefixes{"a=", "candidate:"};
-	for (const string &prefix : prefixes)
-		if (hasprefix(candidate, prefix))
-			candidate.erase(0, prefix.size());
-
-	mCandidate = std::move(candidate);
+	if (!candidate.empty())
+		parse(std::move(candidate));
 }
 }
 
 
-Candidate::Candidate(string candidate, string mid) : Candidate(std::move(candidate)) {
+Candidate::Candidate(string candidate, string mid) : Candidate() {
+	if (!candidate.empty())
+		parse(std::move(candidate));
 	if (!mid.empty())
 	if (!mid.empty())
 		mMid.emplace(std::move(mid));
 		mMid.emplace(std::move(mid));
 }
 }
 
 
-void Candidate::hintMid(string mid) {
-	if (!mMid)
-		mMid.emplace(std::move(mid));
-}
-
-bool Candidate::resolve(ResolveMode mode) {
+void Candidate::parse(string candidate) {
 	using TypeMap_t = std::unordered_map<string, Type>;
 	using TypeMap_t = std::unordered_map<string, Type>;
 	using TcpTypeMap_t = std::unordered_map<string, TransportType>;
 	using TcpTypeMap_t = std::unordered_map<string, TransportType>;
 
 
@@ -84,99 +90,122 @@ bool Candidate::resolve(ResolveMode mode) {
 	                                        {"passive", TransportType::TcpPassive},
 	                                        {"passive", TransportType::TcpPassive},
 	                                        {"so", TransportType::TcpSo}};
 	                                        {"so", TransportType::TcpSo}};
 
 
-	if (mFamily != Family::Unresolved)
-		return true;
+	const std::array prefixes{"a=", "candidate:"};
+	for (const string &prefix : prefixes)
+		if (match_prefix(candidate, prefix))
+			candidate.erase(0, prefix.size());
 
 
-	if (mCandidate.empty())
-		throw std::logic_error("Candidate is empty");
-
-	PLOG_VERBOSE << "Resolving candidate (mode="
-	             << (mode == ResolveMode::Simple ? "simple" : "lookup") << "): " << mCandidate;
+	PLOG_VERBOSE << "Parsing candidate: " << candidate;
 
 
 	// See RFC 8445 for format
 	// See RFC 8445 for format
-	std::istringstream iss(mCandidate);
-	int component{0}, priority{0};
-	string foundation, transport, node, service, typ_, type;
-	if (iss >> foundation >> component >> transport >> priority &&
-	    iss >> node >> service >> typ_ >> type && typ_ == "typ") {
-
-		string left;
-		std::getline(iss, left);
-
-		if (auto it = TypeMap.find(type); it != TypeMap.end())
-			mType = it->second;
-		else
-			mType = Type::Unknown;
-
-		if (transport == "UDP" || transport == "udp") {
-			mTransportType = TransportType::Udp;
-		} else if (transport == "TCP" || transport == "tcp") {
-			std::istringstream iss(left);
-			string tcptype_, tcptype;
-			if (iss >> tcptype_ >> tcptype && tcptype_ == "tcptype") {
-				if (auto it = TcpTypeMap.find(tcptype); it != TcpTypeMap.end())
-					mTransportType = it->second;
-				else
-					mTransportType = TransportType::TcpUnknown;
-
-			} else {
+	std::istringstream iss(candidate);
+	string transport, typ_, type;
+	if (!(iss >> mFoundation >> mComponent >> mTransportString >> mPriority &&
+	      iss >> mNode >> mService >> typ_ >> mTypeString && typ_ == "typ"))
+		throw std::invalid_argument("Invalid candidate format");
+
+	std::getline(iss, mTail);
+	trim_begin(mTail);
+	trim_end(mTail);
+
+	if (auto it = TypeMap.find(type); it != TypeMap.end())
+		mType = it->second;
+	else
+		mType = Type::Unknown;
+
+	if (transport == "UDP" || transport == "udp") {
+		mTransportType = TransportType::Udp;
+	} else if (transport == "TCP" || transport == "tcp") {
+		// Peek tail to find TCP type
+		std::istringstream iss(mTail);
+		string tcptype_, tcptype;
+		if (iss >> tcptype_ >> tcptype && tcptype_ == "tcptype") {
+			if (auto it = TcpTypeMap.find(tcptype); it != TcpTypeMap.end())
+				mTransportType = it->second;
+			else
 				mTransportType = TransportType::TcpUnknown;
 				mTransportType = TransportType::TcpUnknown;
-			}
+
 		} else {
 		} else {
-			mTransportType = TransportType::Unknown;
+			mTransportType = TransportType::TcpUnknown;
 		}
 		}
+	} else {
+		mTransportType = TransportType::Unknown;
+	}
+}
 
 
-		// Try to resolve the node
-		struct addrinfo hints = {};
-		hints.ai_family = AF_UNSPEC;
-		hints.ai_flags = AI_ADDRCONFIG;
-		if (mTransportType == TransportType::Udp) {
-			hints.ai_socktype = SOCK_DGRAM;
-			hints.ai_protocol = IPPROTO_UDP;
-		} else if (mTransportType != TransportType::Unknown) {
-			hints.ai_socktype = SOCK_STREAM;
-			hints.ai_protocol = IPPROTO_TCP;
-		}
+void Candidate::hintMid(string mid) {
+	if (!mMid)
+		mMid.emplace(std::move(mid));
+}
 
 
-		if (mode == ResolveMode::Simple)
-			hints.ai_flags |= AI_NUMERICHOST;
-
-		struct addrinfo *result = nullptr;
-		if (getaddrinfo(node.c_str(), service.c_str(), &hints, &result) == 0) {
-			for (auto p = result; p; p = p->ai_next) {
-				if (p->ai_family == AF_INET || p->ai_family == AF_INET6) {
-					// Rewrite the candidate
-					char nodebuffer[MAX_NUMERICNODE_LEN];
-					char servbuffer[MAX_NUMERICSERV_LEN];
-					if (getnameinfo(p->ai_addr, socklen_t(p->ai_addrlen), nodebuffer,
-					                MAX_NUMERICNODE_LEN, servbuffer, MAX_NUMERICSERV_LEN,
-					                NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
-
-						mAddress = nodebuffer;
-						mPort = uint16_t(std::stoul(servbuffer));
-						mFamily = p->ai_family == AF_INET6 ? Family::Ipv6 : Family::Ipv4;
-
-						const char sp{' '};
-						std::ostringstream oss;
-						oss << foundation << sp << component << sp << transport << sp << priority;
-						oss << sp << nodebuffer << sp << servbuffer << sp << "typ" << sp << type;
-						oss << left;
-						mCandidate = oss.str();
-
-						PLOG_VERBOSE << "Resolved candidate: " << mCandidate;
-						break;
-					}
+bool Candidate::resolve(ResolveMode mode) {
+	PLOG_VERBOSE << "Resolving candidate (mode="
+	             << (mode == ResolveMode::Simple ? "simple" : "lookup") << "): " << mNode << ' '
+	             << mService;
+
+	// Try to resolve the node and service
+	struct addrinfo hints = {};
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_flags = AI_ADDRCONFIG;
+	if (mTransportType == TransportType::Udp) {
+		hints.ai_socktype = SOCK_DGRAM;
+		hints.ai_protocol = IPPROTO_UDP;
+	} else if (mTransportType != TransportType::Unknown) {
+		hints.ai_socktype = SOCK_STREAM;
+		hints.ai_protocol = IPPROTO_TCP;
+	}
+
+	if (mode == ResolveMode::Simple)
+		hints.ai_flags |= AI_NUMERICHOST;
+
+	struct addrinfo *result = nullptr;
+	if (getaddrinfo(mNode.c_str(), mService.c_str(), &hints, &result) == 0) {
+		for (auto p = result; p; p = p->ai_next) {
+			if (p->ai_family == AF_INET || p->ai_family == AF_INET6) {
+				char nodebuffer[MAX_NUMERICNODE_LEN];
+				char servbuffer[MAX_NUMERICSERV_LEN];
+				if (getnameinfo(p->ai_addr, socklen_t(p->ai_addrlen), nodebuffer,
+				                MAX_NUMERICNODE_LEN, servbuffer, MAX_NUMERICSERV_LEN,
+				                NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
+
+					mAddress = nodebuffer;
+					mPort = uint16_t(std::stoul(servbuffer));
+					mFamily = p->ai_family == AF_INET6 ? Family::Ipv6 : Family::Ipv4;
+					PLOG_VERBOSE << "Resolved candidate: " << mAddress << ' ' << mPort;
+					break;
 				}
 				}
 			}
 			}
-
-			freeaddrinfo(result);
 		}
 		}
+
+		freeaddrinfo(result);
 	}
 	}
 
 
 	return mFamily != Family::Unresolved;
 	return mFamily != Family::Unresolved;
 }
 }
 
 
-string Candidate::candidate() const { return "candidate:" + mCandidate; }
+Candidate::Type Candidate::type() const { return mType; }
+
+Candidate::TransportType Candidate::transportType() const { return mTransportType; }
+
+uint32_t Candidate::priority() const { return mPriority; }
+
+string Candidate::candidate() const {
+	const char sp{' '};
+	std::ostringstream oss;
+	oss << "candidate:";
+	oss << mFoundation << sp << mComponent << sp << mTransportString << sp << mPriority << sp;
+	if (isResolved())
+		oss << mAddress << sp << mPort;
+	else
+		oss << mNode << sp << mService;
+
+	oss << sp << "typ" << sp << mTypeString;
+
+	if (!mTail.empty())
+		oss << sp << mTail;
+
+	return oss.str();
+}
 
 
 string Candidate::mid() const { return mMid.value_or("0"); }
 string Candidate::mid() const { return mMid.value_or("0"); }
 
 
@@ -186,13 +215,17 @@ Candidate::operator string() const {
 	return line.str();
 	return line.str();
 }
 }
 
 
-bool Candidate::isResolved() const { return mFamily != Family::Unresolved; }
+bool Candidate::operator==(const Candidate &other) const {
+	return mFoundation == other.mFoundation;
+}
 
 
-Candidate::Family Candidate::family() const { return mFamily; }
+bool Candidate::operator!=(const Candidate &other) const {
+	return mFoundation != other.mFoundation;
+}
 
 
-Candidate::Type Candidate::type() const { return mType; }
+bool Candidate::isResolved() const { return mFamily != Family::Unresolved; }
 
 
-Candidate::TransportType Candidate::transportType() const { return mTransportType; }
+Candidate::Family Candidate::family() const { return mFamily; }
 
 
 std::optional<string> Candidate::address() const {
 std::optional<string> Candidate::address() const {
 	return isResolved() ? std::make_optional(mAddress) : nullopt;
 	return isResolved() ? std::make_optional(mAddress) : nullopt;
@@ -202,10 +235,6 @@ std::optional<uint16_t> Candidate::port() const {
 	return isResolved() ? std::make_optional(mPort) : nullopt;
 	return isResolved() ? std::make_optional(mPort) : nullopt;
 }
 }
 
 
-std::optional<uint32_t> Candidate::priority() const {
-	return isResolved() ? std::make_optional(mPriority) : nullopt;
-}
-
 } // namespace rtc
 } // namespace rtc
 
 
 std::ostream &operator<<(std::ostream &out, const rtc::Candidate &candidate) {
 std::ostream &operator<<(std::ostream &out, const rtc::Candidate &candidate) {
@@ -217,11 +246,11 @@ std::ostream &operator<<(std::ostream &out, const rtc::Candidate::Type &type) {
 	case rtc::Candidate::Type::Host:
 	case rtc::Candidate::Type::Host:
 		return out << "host";
 		return out << "host";
 	case rtc::Candidate::Type::PeerReflexive:
 	case rtc::Candidate::Type::PeerReflexive:
-		return out << "peer_reflexive";
+		return out << "prflx";
 	case rtc::Candidate::Type::ServerReflexive:
 	case rtc::Candidate::Type::ServerReflexive:
-		return out << "server_reflexive";
+		return out << "srflx";
 	case rtc::Candidate::Type::Relayed:
 	case rtc::Candidate::Type::Relayed:
-		return out << "relayed";
+		return out << "relay";
 	default:
 	default:
 		return out << "unknown";
 		return out << "unknown";
 	}
 	}

+ 15 - 4
src/description.cpp

@@ -171,16 +171,27 @@ void Description::setFingerprint(string fingerprint) {
 	mFingerprint.emplace(std::move(fingerprint));
 	mFingerprint.emplace(std::move(fingerprint));
 }
 }
 
 
+bool Description::hasCandidate(const Candidate &candidate) const {
+	for (const Candidate &other : mCandidates)
+		if (candidate == other)
+			return true;
+
+	return false;
+}
+
 void Description::addCandidate(Candidate candidate) {
 void Description::addCandidate(Candidate candidate) {
 	candidate.hintMid(bundleMid());
 	candidate.hintMid(bundleMid());
+
+	for (const Candidate &other : mCandidates)
+		if (candidate == other)
+			return;
+
 	mCandidates.emplace_back(std::move(candidate));
 	mCandidates.emplace_back(std::move(candidate));
 }
 }
 
 
 void Description::addCandidates(std::vector<Candidate> candidates) {
 void Description::addCandidates(std::vector<Candidate> candidates) {
-	for (Candidate candidate : candidates) {
-		candidate.hintMid(bundleMid());
-		mCandidates.emplace_back(std::move(candidate));
-	}
+	for (Candidate candidate : candidates)
+		addCandidate(std::move(candidate));
 }
 }
 
 
 void Description::endCandidates() { mEnded = true; }
 void Description::endCandidates() { mEnded = true; }

+ 4 - 2
src/icetransport.cpp

@@ -564,12 +564,14 @@ bool IceTransport::addRemoteCandidate(const Candidate &candidate) {
 		return false;
 		return false;
 
 
 	// Warning: the candidate string must start with "a=candidate:" and it must not end with a
 	// Warning: the candidate string must start with "a=candidate:" and it must not end with a
-	// newline, else libnice will reject it.
+	// newline or whitespace, else libnice will reject it.
 	string sdp(candidate);
 	string sdp(candidate);
 	NiceCandidate *cand =
 	NiceCandidate *cand =
 	    nice_agent_parse_remote_candidate_sdp(mNiceAgent.get(), mStreamId, sdp.c_str());
 	    nice_agent_parse_remote_candidate_sdp(mNiceAgent.get(), mStreamId, sdp.c_str());
-	if (!cand)
+	if (!cand) {
+		PLOG_WARNING << "Rejected ICE candidate: " << sdp;
 		return false;
 		return false;
+	}
 
 
 	GSList *list = g_slist_append(nullptr, cand);
 	GSList *list = g_slist_append(nullptr, cand);
 	int ret = nice_agent_set_remote_candidates(mNiceAgent.get(), mStreamId, 1, list);
 	int ret = nice_agent_set_remote_candidates(mNiceAgent.get(), mStreamId, 1, list);

+ 12 - 7
src/peerconnection.cpp

@@ -1079,7 +1079,7 @@ void PeerConnection::processLocalCandidate(Candidate candidate) {
 	if (!mLocalDescription)
 	if (!mLocalDescription)
 		throw std::logic_error("Got a local candidate without local description");
 		throw std::logic_error("Got a local candidate without local description");
 
 
-	candidate.resolve(Candidate::ResolveMode::Simple); // for proper SDP generation later
+	candidate.resolve(Candidate::ResolveMode::Simple);
 	mLocalDescription->addCandidate(candidate);
 	mLocalDescription->addCandidate(candidate);
 
 
 	PLOG_VERBOSE << "Issuing local candidate: " << candidate;
 	PLOG_VERBOSE << "Issuing local candidate: " << candidate;
@@ -1116,21 +1116,26 @@ void PeerConnection::processRemoteCandidate(Candidate candidate) {
 
 
 	candidate.hintMid(mRemoteDescription->bundleMid());
 	candidate.hintMid(mRemoteDescription->bundleMid());
 
 
+	if (mRemoteDescription->hasCandidate(candidate))
+		return; // already in description, ignore
+
 	if (candidate.resolve(Candidate::ResolveMode::Simple)) {
 	if (candidate.resolve(Candidate::ResolveMode::Simple)) {
-		iceTransport->addRemoteCandidate(candidate);
+		mRemoteDescription->addCandidate(candidate);
+		iceTransport->addRemoteCandidate(std::move(candidate));
 	} else {
 	} else {
+		mRemoteDescription->addCandidate(candidate); // still add the unresolved candidate
+
 		// OK, we might need a lookup, do it asynchronously
 		// OK, we might need a lookup, do it asynchronously
 		// We don't use the thread pool because we have no control on the timeout
 		// We don't use the thread pool because we have no control on the timeout
 		weak_ptr<IceTransport> weakIceTransport{iceTransport};
 		weak_ptr<IceTransport> weakIceTransport{iceTransport};
-		std::thread t([weakIceTransport, candidate]() mutable {
-			if (candidate.resolve(Candidate::ResolveMode::Lookup))
+		std::thread t([weakIceTransport, candidate = std::move(candidate)]() mutable {
+			if (candidate.resolve(Candidate::ResolveMode::Lookup)) {
 				if (auto iceTransport = weakIceTransport.lock())
 				if (auto iceTransport = weakIceTransport.lock())
-					iceTransport->addRemoteCandidate(candidate);
+					iceTransport->addRemoteCandidate(std::move(candidate));
+			}
 		});
 		});
 		t.detach();
 		t.detach();
 	}
 	}
-
-	mRemoteDescription->addCandidate(std::move(candidate));
 }
 }
 
 
 void PeerConnection::triggerDataChannel(weak_ptr<DataChannel> weakDataChannel) {
 void PeerConnection::triggerDataChannel(weak_ptr<DataChannel> weakDataChannel) {