Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(246)

Side by Side Diff: talk/app/webrtc/peerconnection.cc

Issue 1344143002: Catching more errors when parsing ICE server URLs. (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Fixing tests that didn't expect PC creation to fail due to ICE parse errors. Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * libjingle 2 * libjingle
3 * Copyright 2012 Google Inc. 3 * Copyright 2012 Google Inc.
4 * 4 *
5 * Redistribution and use in source and binary forms, with or without 5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met: 6 * modification, are permitted provided that the following conditions are met:
7 * 7 *
8 * 1. Redistributions of source code must retain the above copyright notice, 8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer. 9 * this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright notice, 10 * 2. Redistributions in binary form must reproduce the above copyright notice,
(...skipping 26 matching lines...) Expand all
37 #include "talk/app/webrtc/streamcollection.h" 37 #include "talk/app/webrtc/streamcollection.h"
38 #include "webrtc/p2p/client/basicportallocator.h" 38 #include "webrtc/p2p/client/basicportallocator.h"
39 #include "talk/session/media/channelmanager.h" 39 #include "talk/session/media/channelmanager.h"
40 #include "webrtc/base/logging.h" 40 #include "webrtc/base/logging.h"
41 #include "webrtc/base/stringencode.h" 41 #include "webrtc/base/stringencode.h"
42 #include "webrtc/system_wrappers/interface/field_trial.h" 42 #include "webrtc/system_wrappers/interface/field_trial.h"
43 43
44 namespace { 44 namespace {
45 45
46 using webrtc::PeerConnectionInterface; 46 using webrtc::PeerConnectionInterface;
47 using webrtc::StunConfigurations;
48 using webrtc::TurnConfigurations;
49 typedef webrtc::PortAllocatorFactoryInterface::StunConfiguration
50 StunConfiguration;
51 typedef webrtc::PortAllocatorFactoryInterface::TurnConfiguration
52 TurnConfiguration;
47 53
48 // The min number of tokens must present in Turn host uri. 54 // The min number of tokens must present in Turn host uri.
49 // e.g. user@turn.example.org 55 // e.g. user@turn.example.org
50 static const size_t kTurnHostTokensNum = 2; 56 static const size_t kTurnHostTokensNum = 2;
51 // Number of tokens must be preset when TURN uri has transport param. 57 // Number of tokens must be preset when TURN uri has transport param.
52 static const size_t kTurnTransportTokensNum = 2; 58 static const size_t kTurnTransportTokensNum = 2;
53 // The default stun port. 59 // The default stun port.
54 static const int kDefaultStunPort = 3478; 60 static const int kDefaultStunPort = 3478;
55 static const int kDefaultStunTlsPort = 5349; 61 static const int kDefaultStunTlsPort = 5349;
56 static const char kTransport[] = "transport"; 62 static const char kTransport[] = "transport";
57 static const char kUdpTransportType[] = "udp"; 63 static const char kUdpTransportType[] = "udp";
58 static const char kTcpTransportType[] = "tcp"; 64 static const char kTcpTransportType[] = "tcp";
59 65
60 // NOTE: Must be in the same order as the ServiceType enum. 66 // NOTE: Must be in the same order as the ServiceType enum.
61 static const char* kValidIceServiceTypes[] = { 67 static const char* kValidIceServiceTypes[] = {"stun",
62 "stun", "stuns", "turn", "turns", "invalid" }; 68 "stuns",
69 "turn",
70 "turns",
71 "invalid"};
tommi 2015/09/16 21:51:00 why do we need this last value?
Taylor Brandstetter 2015/09/24 00:25:04 It isn't; good catch.
63 72
64 enum ServiceType { 73 enum ServiceType {
65 STUN, // Indicates a STUN server. 74 STUN, // Indicates a STUN server.
tommi 2015/09/16 21:51:01 can you explicitly set this to 0 and add a comment
Taylor Brandstetter 2015/09/24 00:25:04 Done.
66 STUNS, // Indicates a STUN server used with a TLS session. 75 STUNS, // Indicates a STUN server used with a TLS session.
67 TURN, // Indicates a TURN server 76 TURN, // Indicates a TURN server
68 TURNS, // Indicates a TURN server used with a TLS session. 77 TURNS, // Indicates a TURN server used with a TLS session.
69 INVALID, // Unknown. 78 INVALID, // Unknown.
70 }; 79 };
71 80
72 enum { 81 enum {
73 MSG_SET_SESSIONDESCRIPTION_SUCCESS = 0, 82 MSG_SET_SESSIONDESCRIPTION_SUCCESS = 0,
74 MSG_SET_SESSIONDESCRIPTION_FAILED, 83 MSG_SET_SESSIONDESCRIPTION_FAILED,
75 MSG_GETSTATS, 84 MSG_GETSTATS,
(...skipping 16 matching lines...) Expand all
92 } 101 }
93 rtc::scoped_refptr<webrtc::StatsObserver> observer; 102 rtc::scoped_refptr<webrtc::StatsObserver> observer;
94 rtc::scoped_refptr<webrtc::MediaStreamTrackInterface> track; 103 rtc::scoped_refptr<webrtc::MediaStreamTrackInterface> track;
95 }; 104 };
96 105
97 // |in_str| should be of format 106 // |in_str| should be of format
98 // stunURI = scheme ":" stun-host [ ":" stun-port ] 107 // stunURI = scheme ":" stun-host [ ":" stun-port ]
99 // scheme = "stun" / "stuns" 108 // scheme = "stun" / "stuns"
100 // stun-host = IP-literal / IPv4address / reg-name 109 // stun-host = IP-literal / IPv4address / reg-name
101 // stun-port = *DIGIT 110 // stun-port = *DIGIT
102 111 //
103 // draft-petithuguenin-behave-turn-uris-01 112 // draft-petithuguenin-behave-turn-uris-01
104 // turnURI = scheme ":" turn-host [ ":" turn-port ] 113 // turnURI = scheme ":" turn-host [ ":" turn-port ]
105 // turn-host = username@IP-literal / IPv4address / reg-name 114 // turn-host = username@IP-literal / IPv4address / reg-name
106 bool GetServiceTypeAndHostnameFromUri(const std::string& in_str, 115 bool GetServiceTypeAndHostnameFromUri(const std::string& in_str,
107 ServiceType* service_type, 116 ServiceType* service_type,
108 std::string* hostname) { 117 std::string* hostname) {
109 const std::string::size_type colonpos = in_str.find(':'); 118 const std::string::size_type colonpos = in_str.find(':');
110 if (colonpos == std::string::npos || (colonpos + 1) == in_str.length()) { 119 if (colonpos == std::string::npos) {
120 LOG(LS_WARNING) << "Missing ':' in ICE URI: " << in_str;
111 return false; 121 return false;
112 } 122 }
123 if ((colonpos + 1) == in_str.length()) {
124 LOG(LS_WARNING) << "Empty hostname in ICE URI: " << in_str;
125 return false;
126 }
127 *service_type = INVALID;
113 std::string type = in_str.substr(0, colonpos); 128 std::string type = in_str.substr(0, colonpos);
114 for (size_t i = 0; i < ARRAY_SIZE(kValidIceServiceTypes); ++i) { 129 for (size_t i = 0; i < ARRAY_SIZE(kValidIceServiceTypes); ++i) {
115 if (type.compare(kValidIceServiceTypes[i]) == 0) { 130 if (type.compare(kValidIceServiceTypes[i]) == 0) {
tommi 2015/09/16 21:51:00 nit: We don't actually need to create a new string
Taylor Brandstetter 2015/09/24 00:25:04 Done. And although there's no unit test for this f
116 *service_type = static_cast<ServiceType>(i); 131 *service_type = static_cast<ServiceType>(i);
117 break; 132 break;
118 } 133 }
119 } 134 }
120 if (*service_type == INVALID) { 135 if (*service_type == INVALID) {
121 return false; 136 return false;
122 } 137 }
123 *hostname = in_str.substr(colonpos + 1, std::string::npos); 138 *hostname = in_str.substr(colonpos + 1, std::string::npos);
124 return true; 139 return true;
125 } 140 }
126 141
142 bool ParsePort(const std::string& in_str, int* port) {
143 // Make sure port only contains digits. FromString doesn't check this.
144 if (in_str.find_first_not_of("0123456789") != std::string::npos) {
tommi 2015/09/16 21:51:01 nit: could use isdigit() in a loop for O(n) ;)
Taylor Brandstetter 2015/09/24 00:25:04 Done.
145 return false;
146 }
147 return rtc::FromString(in_str, port);
tommi 2015/09/16 21:51:00 out of curiosity I took a look at FromString() and
Taylor Brandstetter 2015/09/24 00:25:04 atoi still allows whitespace and non-numeric chara
148 }
149
127 // This method parses IPv6 and IPv4 literal strings, along with hostnames in 150 // This method parses IPv6 and IPv4 literal strings, along with hostnames in
128 // standard hostname:port format. 151 // standard hostname:port format.
129 // Consider following formats as correct. 152 // Consider following formats as correct.
130 // |hostname:port|, |[IPV6 address]:port|, |IPv4 address|:port, 153 // |hostname:port|, |[IPV6 address]:port|, |IPv4 address|:port,
131 // |hostname|, |[IPv6 address]|, |IPv4 address| 154 // |hostname|, |[IPv6 address]|, |IPv4 address|.
132 bool ParseHostnameAndPortFromString(const std::string& in_str, 155 bool ParseHostnameAndPortFromString(const std::string& in_str,
133 std::string* host, 156 std::string* host,
tommi 2015/09/16 21:51:01 what about adding a DCHECK() at the top of this fu
Taylor Brandstetter 2015/09/24 00:25:04 Done.
134 int* port) { 157 int* port) {
135 if (in_str.at(0) == '[') { 158 if (in_str.at(0) == '[') {
tommi 2015/09/16 21:51:00 Is there ever a chance that in_str is empty?
Taylor Brandstetter 2015/09/24 00:25:04 Not any longer.
136 std::string::size_type closebracket = in_str.rfind(']'); 159 std::string::size_type closebracket = in_str.rfind(']');
137 if (closebracket != std::string::npos) { 160 if (closebracket != std::string::npos) {
138 *host = in_str.substr(1, closebracket - 1); 161 *host = in_str.substr(1, closebracket - 1);
tommi 2015/09/16 21:51:00 nit: we don't really need to assign to host until
Taylor Brandstetter 2015/09/24 00:25:05 Done.
139 std::string::size_type colonpos = in_str.find(':', closebracket); 162 std::string::size_type colonpos = in_str.find(':', closebracket);
140 if (std::string::npos != colonpos) { 163 if (std::string::npos != colonpos) {
141 if (!rtc::FromString( 164 if (!ParsePort(in_str.substr(closebracket + 2, std::string::npos),
142 in_str.substr(closebracket + 2, std::string::npos), port)) { 165 port)) {
143 return false; 166 return false;
144 } 167 }
145 } 168 }
146 } else { 169 } else {
147 return false; 170 return false;
148 } 171 }
149 } else { 172 } else {
150 std::string::size_type colonpos = in_str.find(':'); 173 std::string::size_type colonpos = in_str.find(':');
151 if (std::string::npos != colonpos) { 174 if (std::string::npos != colonpos) {
152 *host = in_str.substr(0, colonpos); 175 *host = in_str.substr(0, colonpos);
tommi 2015/09/16 21:51:00 same here
Taylor Brandstetter 2015/09/24 00:25:04 Done.
153 if (!rtc::FromString( 176 if (!ParsePort(in_str.substr(colonpos + 1, std::string::npos), port)) {
154 in_str.substr(colonpos + 1, std::string::npos), port)) {
155 return false; 177 return false;
156 } 178 }
157 } else { 179 } else {
158 *host = in_str; 180 *host = in_str;
159 } 181 }
160 } 182 }
183 if (host->empty()) {
tommi 2015/09/16 21:51:01 and here I'd skip the if() and just do: return !h
Taylor Brandstetter 2015/09/24 00:25:04 Done.
184 return false;
185 }
161 return true; 186 return true;
162 } 187 }
163 188
164 typedef webrtc::PortAllocatorFactoryInterface::StunConfiguration 189 // Adds a StunConfiguration or TurnConfiguration to the appropriate list,
165 StunConfiguration; 190 // by parsing |url| and using the username/password in |server|.
166 typedef webrtc::PortAllocatorFactoryInterface::TurnConfiguration
167 TurnConfiguration;
168
169 bool ParseIceServerUrl(const PeerConnectionInterface::IceServer& server, 191 bool ParseIceServerUrl(const PeerConnectionInterface::IceServer& server,
170 const std::string& url, 192 const std::string& url,
171 std::vector<StunConfiguration>* stun_config, 193 StunConfigurations* stun_config,
tommi 2015/09/16 21:51:01 can these params ever be null? If they're never al
Taylor Brandstetter 2015/09/24 00:25:04 Done.
172 std::vector<TurnConfiguration>* turn_config) { 194 TurnConfigurations* turn_config) {
173 // draft-nandakumar-rtcweb-stun-uri-01 195 // draft-nandakumar-rtcweb-stun-uri-01
174 // stunURI = scheme ":" stun-host [ ":" stun-port ] 196 // stunURI = scheme ":" stun-host [ ":" stun-port ]
175 // scheme = "stun" / "stuns" 197 // scheme = "stun" / "stuns"
176 // stun-host = IP-literal / IPv4address / reg-name 198 // stun-host = IP-literal / IPv4address / reg-name
177 // stun-port = *DIGIT 199 // stun-port = *DIGIT
178 200
179 // draft-petithuguenin-behave-turn-uris-01 201 // draft-petithuguenin-behave-turn-uris-01
180 // turnURI = scheme ":" turn-host [ ":" turn-port ] 202 // turnURI = scheme ":" turn-host [ ":" turn-port ]
181 // [ "?transport=" transport ] 203 // [ "?transport=" transport ]
182 // scheme = "turn" / "turns" 204 // scheme = "turn" / "turns"
183 // transport = "udp" / "tcp" / transport-ext 205 // transport = "udp" / "tcp" / transport-ext
184 // transport-ext = 1*unreserved 206 // transport-ext = 1*unreserved
185 // turn-host = IP-literal / IPv4address / reg-name 207 // turn-host = IP-literal / IPv4address / reg-name
186 // turn-port = *DIGIT 208 // turn-port = *DIGIT
187 std::vector<std::string> tokens; 209 std::vector<std::string> tokens;
188 std::string turn_transport_type = kUdpTransportType; 210 std::string turn_transport_type = kUdpTransportType;
189 ASSERT(!url.empty()); 211 ASSERT(!url.empty());
190 rtc::tokenize(url, '?', &tokens); 212 rtc::tokenize(url, '?', &tokens);
191 std::string uri_without_transport = tokens[0]; 213 std::string uri_without_transport = tokens[0];
192 // Let's look into transport= param, if it exists. 214 // Let's look into transport= param, if it exists.
193 if (tokens.size() == kTurnTransportTokensNum) { // ?transport= is present. 215 if (tokens.size() == kTurnTransportTokensNum) { // ?transport= is present.
194 std::string uri_transport_param = tokens[1]; 216 std::string uri_transport_param = tokens[1];
195 rtc::tokenize(uri_transport_param, '=', &tokens); 217 rtc::tokenize(uri_transport_param, '=', &tokens);
196 if (tokens[0] == kTransport) { 218 if (tokens[0] == kTransport) {
197 // As per above grammar transport param will be consist of lower case 219 // As per above grammar transport param will be consist of lower case
198 // letters. 220 // letters.
199 if (tokens[1] != kUdpTransportType && tokens[1] != kTcpTransportType) { 221 if (tokens[1] != kUdpTransportType && tokens[1] != kTcpTransportType) {
200 LOG(LS_WARNING) << "Transport param should always be udp or tcp."; 222 LOG(LS_WARNING) << "Transport param should always be udp or tcp.";
201 return true; 223 return false;
202 } 224 }
203 turn_transport_type = tokens[1]; 225 turn_transport_type = tokens[1];
204 } 226 }
205 } 227 }
206 228
207 std::string hoststring; 229 std::string hoststring;
208 ServiceType service_type = INVALID; 230 ServiceType service_type;
209 if (!GetServiceTypeAndHostnameFromUri(uri_without_transport, 231 if (!GetServiceTypeAndHostnameFromUri(uri_without_transport,
210 &service_type, 232 &service_type,
211 &hoststring)) { 233 &hoststring)) {
212 LOG(LS_WARNING) << "Invalid transport parameter in ICE URI: " 234 LOG(LS_WARNING) << "Invalid transport parameter in ICE URI: " << url;
213 << uri_without_transport; 235 return false;
214 return true;
215 } 236 }
216 237
238 // GetServiceTypeAndHostnameFromUri should never give an empty hoststring
217 ASSERT(!hoststring.empty()); 239 ASSERT(!hoststring.empty());
218 240
219 // Let's break hostname. 241 // Let's break hostname.
220 tokens.clear(); 242 tokens.clear();
221 rtc::tokenize(hoststring, '@', &tokens); 243 rtc::tokenize_with_empty_tokens(hoststring, '@', &tokens);
222 ASSERT(!tokens.empty()); 244
223 std::string username(server.username); 245 std::string username(server.username);
224 // TODO(pthatcher): What's the right thing to do if tokens.size() is >2? 246 if (tokens.size() > kTurnHostTokensNum) {
225 // E.g. a string like "foo@bar@bat". 247 LOG(LS_WARNING) << "Invalid user@hostname format: " << hoststring;
226 if (tokens.size() >= kTurnHostTokensNum) { 248 return false;
249 } else if (tokens.size() == kTurnHostTokensNum) {
tommi 2015/09/16 21:51:00 nit: since the previous scope has an early return,
Taylor Brandstetter 2015/09/24 00:25:04 Done.
250 if (tokens[0].empty() || tokens[1].empty()) {
251 LOG(LS_WARNING) << "Invalid user@hostname format: " << hoststring;
252 return false;
253 }
227 username.assign(rtc::s_url_decode(tokens[0])); 254 username.assign(rtc::s_url_decode(tokens[0]));
228 hoststring = tokens[1]; 255 hoststring = tokens[1];
229 } else { 256 } else {
230 hoststring = tokens[0]; 257 hoststring = tokens[0];
231 } 258 }
232 259
233 int port = kDefaultStunPort; 260 int port = kDefaultStunPort;
234 if (service_type == TURNS) { 261 if (service_type == TURNS) {
235 port = kDefaultStunTlsPort; 262 port = kDefaultStunTlsPort;
236 turn_transport_type = kTcpTransportType; 263 turn_transport_type = kTcpTransportType;
237 } 264 }
238 265
239 std::string address; 266 std::string address;
240 if (!ParseHostnameAndPortFromString(hoststring, &address, &port)) { 267 if (!ParseHostnameAndPortFromString(hoststring, &address, &port)) {
241 LOG(WARNING) << "Invalid Hostname format: " << uri_without_transport; 268 LOG(WARNING) << "Invalid hostname format: " << uri_without_transport;
242 return true; 269 return false;
243 } 270 }
244 271
245 if (port <= 0 || port > 0xffff) { 272 if (port <= 0 || port > 0xffff) {
246 LOG(WARNING) << "Invalid port: " << port; 273 LOG(WARNING) << "Invalid port: " << port;
247 return true; 274 return false;
248 } 275 }
249 276
250 switch (service_type) { 277 switch (service_type) {
251 case STUN: 278 case STUN:
252 case STUNS: 279 case STUNS:
253 stun_config->push_back(StunConfiguration(address, port)); 280 stun_config->push_back(StunConfiguration(address, port));
254 break; 281 break;
255 case TURN: 282 case TURN:
256 case TURNS: { 283 case TURNS: {
257 if (username.empty()) {
258 // Turn url example from the spec |url:"turn:user@turn.example.org"|.
259 std::vector<std::string> turn_tokens;
260 rtc::tokenize(address, '@', &turn_tokens);
261 if (turn_tokens.size() == kTurnHostTokensNum) {
262 username.assign(rtc::s_url_decode(turn_tokens[0]));
263 address = turn_tokens[1];
264 }
265 }
266
Taylor Brandstetter 2015/09/15 22:36:05 I'm pretty confident this code can be safely remov
267 bool secure = (service_type == TURNS); 284 bool secure = (service_type == TURNS);
268
269 turn_config->push_back(TurnConfiguration(address, port, 285 turn_config->push_back(TurnConfiguration(address, port,
270 username, 286 username,
271 server.password, 287 server.password,
272 turn_transport_type, 288 turn_transport_type,
273 secure)); 289 secure));
274 break; 290 break;
275 } 291 }
276 case INVALID: 292 case INVALID:
277 default: 293 default:
278 LOG(WARNING) << "Configuration not supported: " << url; 294 LOG(WARNING) << "Configuration not supported: " << url;
279 return false; 295 return false;
280 } 296 }
281 return true; 297 return true;
282 } 298 }
283 299
300 } // namespace
301
302 namespace webrtc {
303
284 bool ParseIceServers(const PeerConnectionInterface::IceServers& servers, 304 bool ParseIceServers(const PeerConnectionInterface::IceServers& servers,
285 std::vector<StunConfiguration>* stun_config, 305 StunConfigurations* stun_config,
286 std::vector<TurnConfiguration>* turn_config) { 306 TurnConfigurations* turn_config) {
287 for (const webrtc::PeerConnectionInterface::IceServer& server : servers) { 307 for (const webrtc::PeerConnectionInterface::IceServer& server : servers) {
288 if (!server.urls.empty()) { 308 if (!server.urls.empty()) {
289 for (const std::string& url : server.urls) { 309 for (const std::string& url : server.urls) {
290 if (url.empty()) { 310 if (url.empty()) {
291 LOG(WARNING) << "Empty uri."; 311 LOG(WARNING) << "Empty uri.";
292 continue; 312 continue;
293 } 313 }
294 if (!ParseIceServerUrl(server, url, stun_config, turn_config)) { 314 if (!ParseIceServerUrl(server, url, stun_config, turn_config)) {
295 return false; 315 return false;
296 } 316 }
(...skipping 19 matching lines...) Expand all
316 return false; 336 return false;
317 if (current_streams->find(new_stream->label()) != NULL) { 337 if (current_streams->find(new_stream->label()) != NULL) {
318 LOG(LS_ERROR) << "MediaStream with label " << new_stream->label() 338 LOG(LS_ERROR) << "MediaStream with label " << new_stream->label()
319 << " is already added."; 339 << " is already added.";
320 return false; 340 return false;
321 } 341 }
322 342
323 return true; 343 return true;
324 } 344 }
325 345
326 } // namespace
327
328 namespace webrtc {
329
330 PeerConnection::PeerConnection(PeerConnectionFactory* factory) 346 PeerConnection::PeerConnection(PeerConnectionFactory* factory)
331 : factory_(factory), 347 : factory_(factory),
332 observer_(NULL), 348 observer_(NULL),
333 uma_observer_(NULL), 349 uma_observer_(NULL),
334 signaling_state_(kStable), 350 signaling_state_(kStable),
335 ice_state_(kIceNew), 351 ice_state_(kIceNew),
336 ice_connection_state_(kIceConnectionNew), 352 ice_connection_state_(kIceConnectionNew),
337 ice_gathering_state_(kIceGatheringNew) { 353 ice_gathering_state_(kIceGatheringNew) {
338 } 354 }
339 355
(...skipping 564 matching lines...) Expand 10 before | Expand all | Expand 10 after
904 if (ice_gathering_state_ != kIceGatheringComplete) { 920 if (ice_gathering_state_ != kIceGatheringComplete) {
905 ice_gathering_state_ = kIceGatheringComplete; 921 ice_gathering_state_ = kIceGatheringComplete;
906 observer_->OnIceGatheringChange(ice_gathering_state_); 922 observer_->OnIceGatheringChange(ice_gathering_state_);
907 } 923 }
908 } 924 }
909 observer_->OnSignalingChange(signaling_state_); 925 observer_->OnSignalingChange(signaling_state_);
910 observer_->OnStateChange(PeerConnectionObserver::kSignalingState); 926 observer_->OnStateChange(PeerConnectionObserver::kSignalingState);
911 } 927 }
912 928
913 } // namespace webrtc 929 } // namespace webrtc
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698