blob: a5a36cc1c97cd0a6b60909807b7b70981e675666 [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
#include <string>
#include "talk/p2p/client/connectivitychecker.h"
#include "talk/base/asynchttprequest.h"
#include "talk/base/autodetectproxy.h"
#include "talk/base/basicpacketsocketfactory.h"
#include "talk/base/helpers.h"
#include "talk/base/httpcommon.h"
#include "talk/base/logging.h"
#include "talk/base/proxydetect.h"
#include "talk/base/thread.h"
#include "talk/p2p/base/candidate.h"
#include "talk/p2p/base/common.h"
#include "talk/p2p/base/port.h"
#include "talk/p2p/base/relayport.h"
#include "talk/p2p/base/stunport.h"
namespace cricket {
static const char kSessionTypeVideo[] =
"http://www.google.com/session/video";
static const char kSessionNameRtp[] = "rtp";
static const char kDefaultStunHostname[] = "stun.l.google.com";
static const int kDefaultStunPort = 19302;
// Default maximum time in milliseconds we will wait for connections.
static const uint32 kDefaultTimeoutMs = 3000;
enum {
MSG_START = 1,
MSG_STOP = 2,
MSG_TIMEOUT = 3,
MSG_SIGNAL_RESULTS = 4
};
class TestHttpPortAllocator : public HttpPortAllocator {
public:
TestHttpPortAllocator(talk_base::NetworkManager* network_manager,
const std::string& user_agent,
const std::string& relay_token) :
HttpPortAllocator(network_manager, user_agent) {
SetRelayToken(relay_token);
}
PortAllocatorSession* CreateSession(
const std::string& name, const std::string& session_type) {
return new TestHttpPortAllocatorSession(this, name, session_type,
stun_hosts(), relay_hosts(),
relay_token(), user_agent());
}
};
void TestHttpPortAllocatorSession::ConfigReady(PortConfiguration* config) {
SignalConfigReady(config, proxy_);
}
void TestHttpPortAllocatorSession::OnRequestDone(
talk_base::SignalThread* data) {
talk_base::AsyncHttpRequest* request =
static_cast<talk_base::AsyncHttpRequest*>(data);
SignalRequestDone(request);
// For the https response, pass the result back to the port
// allocator to generate a config with relay addresses we can
// ping. For http, we are only interested in that we got a
// response.
if (request->port() == talk_base::HTTP_SECURE_PORT) {
HttpPortAllocatorSession::OnRequestDone(data);
}
}
ConnectivityChecker::ConnectivityChecker(
talk_base::Thread* worker,
const std::string& jid,
const std::string& session_id,
const std::string& user_agent,
const std::string& relay_token,
const std::string& connection)
: worker_(worker),
jid_(jid),
session_id_(session_id),
user_agent_(user_agent),
relay_token_(relay_token),
connection_(connection),
proxy_detect_(NULL),
timeout_ms_(kDefaultTimeoutMs),
stun_address_(kDefaultStunHostname, kDefaultStunPort) {
}
ConnectivityChecker::~ConnectivityChecker() {
Stop();
nics_.clear();
}
bool ConnectivityChecker::Initialize() {
network_manager_.reset(CreateNetworkManager());
socket_factory_.reset(CreateSocketFactory(worker_));
port_allocator_.reset(CreatePortAllocator(network_manager_.get(),
user_agent_, relay_token_));
return true;
}
void ConnectivityChecker::Start() {
main_ = talk_base::Thread::Current();
worker_->Post(this, MSG_START);
}
void ConnectivityChecker::Stop() {
worker_->Post(this, MSG_STOP);
}
void ConnectivityChecker::CleanUp() {
ASSERT(worker_ == talk_base::Thread::Current());
worker_->Clear(this, MSG_TIMEOUT);
if (proxy_detect_) {
proxy_detect_->Release();
proxy_detect_ = NULL;
}
for (uint32 i = 0; i < sessions_.size(); ++i) {
delete sessions_[i];
}
sessions_.clear();
for (uint32 i = 0; i < ports_.size(); ++i) {
delete ports_[i];
}
ports_.clear();
}
bool ConnectivityChecker::AddNic(const talk_base::IPAddress& ip,
const talk_base::SocketAddress& proxy_addr) {
NicMap::iterator i = nics_.find(NicId(ip, proxy_addr));
if (i != nics_.end()) {
// Already have it.
return false;
}
uint32 now = talk_base::Time();
NicInfo info;
info.ip = ip;
info.proxy_info = GetProxyInfo();
info.stun.start_time_ms = now;
nics_.insert(std::pair<NicId, NicInfo>(NicId(ip, proxy_addr), info));
return true;
}
void ConnectivityChecker::SetProxyInfo(const talk_base::ProxyInfo& proxy_info) {
port_allocator_->set_proxy(user_agent_, proxy_info);
AllocatePorts();
}
talk_base::ProxyInfo ConnectivityChecker::GetProxyInfo() const {
talk_base::ProxyInfo proxy_info;
if (proxy_detect_) {
proxy_info = proxy_detect_->proxy();
}
return proxy_info;
}
void ConnectivityChecker::CheckNetworks() {
network_manager_->SignalNetworksChanged.connect(
this, &ConnectivityChecker::OnNetworksChanged);
network_manager_->StartUpdating();
}
void ConnectivityChecker::OnMessage(talk_base::Message *msg) {
switch (msg->message_id) {
case MSG_START:
ASSERT(worker_ == talk_base::Thread::Current());
worker_->PostDelayed(timeout_ms_, this, MSG_TIMEOUT);
CheckNetworks();
break;
case MSG_STOP:
// We were stopped, just close down without signaling.
OnCheckDone(false);
break;
case MSG_TIMEOUT:
// Close down and signal results.
OnCheckDone(true);
break;
case MSG_SIGNAL_RESULTS:
ASSERT(main_ == talk_base::Thread::Current());
SignalCheckDone(this);
break;
default:
LOG(LS_ERROR) << "Unknown message: " << msg->message_id;
}
}
void ConnectivityChecker::OnCheckDone(bool signal_results) {
// Clean up memory allocated by the worker thread.
CleanUp();
if (signal_results) {
main_->Post(this, MSG_SIGNAL_RESULTS);
}
}
void ConnectivityChecker::OnProxyDetect(talk_base::SignalThread* thread) {
ASSERT(worker_ == talk_base::Thread::Current());
if (proxy_detect_->proxy().type != talk_base::PROXY_NONE) {
SetProxyInfo(proxy_detect_->proxy());
}
}
void ConnectivityChecker::OnRequestDone(talk_base::AsyncHttpRequest* request) {
ASSERT(worker_ == talk_base::Thread::Current());
// Since we don't know what nic were actually used for the http request,
// for now, just use the first one.
std::vector<talk_base::Network*> networks;
network_manager_->GetNetworks(&networks);
if (networks.empty()) {
LOG(LS_ERROR) << "No networks while registering http start.";
return;
}
talk_base::ProxyInfo proxy_info = request->proxy();
NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address));
if (i != nics_.end()) {
int port = request->port();
uint32 now = talk_base::Time();
NicInfo* nic_info = &i->second;
if (port == talk_base::HTTP_DEFAULT_PORT) {
nic_info->http.rtt = now - nic_info->http.start_time_ms;
} else if (port == talk_base::HTTP_SECURE_PORT) {
nic_info->https.rtt = now - nic_info->https.start_time_ms;
} else {
LOG(LS_ERROR) << "Got response with unknown port: " << port;
}
} else {
LOG(LS_ERROR) << "No nic info found while receiving response.";
}
}
void ConnectivityChecker::OnConfigReady(
const PortConfiguration* config,
const talk_base::ProxyInfo& proxy_info) {
ASSERT(worker_ == talk_base::Thread::Current());
CreateRelayPorts(config, proxy_info);
}
void ConnectivityChecker::OnRelayAddressReady(Port* port) {
ASSERT(worker_ == talk_base::Thread::Current());
RelayPort* relay_port = reinterpret_cast<RelayPort*>(port);
const ProtocolAddress* address = relay_port->ServerAddress(0);
talk_base::IPAddress ip = port->network()->ip();
NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
if (i != nics_.end()) {
// We have it already, add the new information.
NicInfo* nic_info = &i->second;
ConnectInfo* connect_info = NULL;
if (address) {
switch (address->proto) {
case PROTO_UDP:
connect_info = &nic_info->udp;
break;
case PROTO_TCP:
connect_info = &nic_info->tcp;
break;
case PROTO_SSLTCP:
connect_info = &nic_info->ssltcp;
break;
default:
LOG(LS_ERROR) << " relay address with bad protocol added";
}
if (connect_info) {
connect_info->rtt =
talk_base::TimeSince(connect_info->start_time_ms);
}
}
} else {
LOG(LS_ERROR) << " got relay address for non-existing nic";
}
}
void ConnectivityChecker::OnStunAddressReady(Port* port) {
ASSERT(worker_ == talk_base::Thread::Current());
const std::vector<Candidate> candidates = port->candidates();
Candidate c = candidates[0];
talk_base::IPAddress ip = port->network()->ip();
NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
if (i != nics_.end()) {
// We have it already, add the new information.
uint32 now = talk_base::Time();
NicInfo* nic_info = &i->second;
nic_info->external_address = c.address();
nic_info->stun_server_address = static_cast<StunPort*>(port)->server_addr();
nic_info->stun.rtt = now - nic_info->stun.start_time_ms;
} else {
LOG(LS_ERROR) << "Got stun address for non-existing nic";
}
}
void ConnectivityChecker::OnStunAddressError(Port* port) {
ASSERT(worker_ == talk_base::Thread::Current());
LOG(LS_ERROR) << "Stun address error.";
talk_base::IPAddress ip = port->network()->ip();
NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
if (i != nics_.end()) {
// We have it already, add the new information.
NicInfo* nic_info = &i->second;
nic_info->stun_server_address = static_cast<StunPort*>(port)->server_addr();
}
}
void ConnectivityChecker::OnRelayAddressError(Port* port) {
ASSERT(worker_ == talk_base::Thread::Current());
LOG(LS_ERROR) << "Relay address error.";
}
void ConnectivityChecker::OnNetworksChanged() {
ASSERT(worker_ == talk_base::Thread::Current());
std::vector<talk_base::Network*> networks;
network_manager_->GetNetworks(&networks);
if (networks.empty()) {
LOG(LS_ERROR) << "Machine has no networks; nothing to do";
return;
}
AllocatePorts();
}
HttpPortAllocator* ConnectivityChecker::CreatePortAllocator(
talk_base::NetworkManager* network_manager,
const std::string& user_agent,
const std::string& relay_token) {
return new TestHttpPortAllocator(network_manager, user_agent, relay_token);
}
StunPort* ConnectivityChecker::CreateStunPort(
const PortConfiguration* config, talk_base::Network* network) {
return StunPort::Create(worker_,
socket_factory_.get(),
network,
network->ip(),
0,
0,
config->stun_address);
}
RelayPort* ConnectivityChecker::CreateRelayPort(
const PortConfiguration* config, talk_base::Network* network) {
return RelayPort::Create(worker_,
socket_factory_.get(),
network,
network->ip(),
port_allocator_->min_port(),
port_allocator_->max_port(),
config->username,
config->password,
config->magic_cookie);
}
void ConnectivityChecker::CreateRelayPorts(
const PortConfiguration* config,
const talk_base::ProxyInfo& proxy_info) {
PortConfiguration::RelayList::const_iterator relay;
std::vector<talk_base::Network*> networks;
network_manager_->GetNetworks(&networks);
if (networks.empty()) {
LOG(LS_ERROR) << "Machine has no networks; no relay ports created.";
return;
}
for (relay = config->relays.begin();
relay != config->relays.end(); ++relay) {
for (uint32 i = 0; i < networks.size(); ++i) {
NicMap::iterator iter = nics_.find(NicId(networks[i]->ip(),
proxy_info.address));
if (iter != nics_.end()) {
// TODO: Now setting the same start time for all protocols.
// This might affect accuracy, but since we are mainly looking for
// connect failures or number that stick out, this is good enough.
uint32 now = talk_base::Time();
NicInfo* nic_info = &iter->second;
nic_info->udp.start_time_ms = now;
nic_info->tcp.start_time_ms = now;
nic_info->ssltcp.start_time_ms = now;
// Add the addresses of this protocol.
PortConfiguration::PortList::const_iterator relay_port;
for (relay_port = relay->ports.begin();
relay_port != relay->ports.end();
++relay_port) {
RelayPort* port = CreateRelayPort(config, networks[i]);
port->AddServerAddress(*relay_port);
port->AddExternalAddress(*relay_port);
nic_info->media_server_address = port->ServerAddress(0)->address;
// Listen to network events.
port->SignalAddressReady.connect(
this, &ConnectivityChecker::OnRelayAddressReady);
port->SignalAddressError.connect(
this, &ConnectivityChecker::OnRelayAddressError);
port->set_proxy(user_agent_, proxy_info);
// Start fetching an address for this port.
port->PrepareAddress();
ports_.push_back(port);
}
} else {
LOG(LS_ERROR) << "Failed to find nic info when creating relay ports.";
}
}
}
}
void ConnectivityChecker::AllocatePorts() {
PortConfiguration config(stun_address_,
talk_base::CreateRandomString(16),
talk_base::CreateRandomString(16),
"");
std::vector<talk_base::Network*> networks;
network_manager_->GetNetworks(&networks);
if (networks.empty()) {
LOG(LS_ERROR) << "Machine has no networks; no ports will be allocated";
return;
}
talk_base::ProxyInfo proxy_info = GetProxyInfo();
bool allocateRelayPorts = false;
for (uint32 i = 0; i < networks.size(); ++i) {
if (AddNic(networks[i]->ip(), proxy_info.address)) {
Port* port = CreateStunPort(&config, networks[i]);
// Listen to network events.
port->SignalAddressReady.connect(
this, &ConnectivityChecker::OnStunAddressReady);
port->SignalAddressError.connect(
this, &ConnectivityChecker::OnStunAddressError);
port->set_proxy(user_agent_, proxy_info);
port->PrepareAddress();
ports_.push_back(port);
allocateRelayPorts = true;
}
}
// If any new ip/proxy combinations were added, send a relay allocate.
if (allocateRelayPorts) {
AllocateRelayPorts();
}
// Initiate proxy detection.
InitiateProxyDetection();
}
void ConnectivityChecker::InitiateProxyDetection() {
// Only start if we haven't been started before.
if (!proxy_detect_) {
proxy_detect_ = new talk_base::AutoDetectProxy(user_agent_);
talk_base::Url<char> host_url("/", "relay.google.com",
talk_base::HTTP_DEFAULT_PORT);
host_url.set_secure(true);
proxy_detect_->set_server_url(host_url.url());
proxy_detect_->SignalWorkDone.connect(
this, &ConnectivityChecker::OnProxyDetect);
proxy_detect_->Start();
}
}
void ConnectivityChecker::AllocateRelayPorts() {
// Currently we are using the 'default' nic for http(s) requests.
TestHttpPortAllocatorSession* allocator_session =
reinterpret_cast<TestHttpPortAllocatorSession*>(
port_allocator_->CreateSession(kSessionNameRtp, kSessionTypeVideo));
allocator_session->set_proxy(port_allocator_->proxy());
allocator_session->SignalConfigReady.connect(
this, &ConnectivityChecker::OnConfigReady);
allocator_session->SignalRequestDone.connect(
this, &ConnectivityChecker::OnRequestDone);
// Try both http and https.
RegisterHttpStart(talk_base::HTTP_SECURE_PORT);
allocator_session->SendSessionRequest("relay.l.google.com",
talk_base::HTTP_SECURE_PORT);
RegisterHttpStart(talk_base::HTTP_DEFAULT_PORT);
allocator_session->SendSessionRequest("relay.l.google.com",
talk_base::HTTP_DEFAULT_PORT);
sessions_.push_back(allocator_session);
}
void ConnectivityChecker::RegisterHttpStart(int port) {
// Since we don't know what nic were actually used for the http request,
// for now, just use the first one.
std::vector<talk_base::Network*> networks;
network_manager_->GetNetworks(&networks);
if (networks.empty()) {
LOG(LS_ERROR) << "No networks while registering http start.";
return;
}
talk_base::ProxyInfo proxy_info = GetProxyInfo();
NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address));
if (i != nics_.end()) {
uint32 now = talk_base::Time();
NicInfo* nic_info = &i->second;
if (port == talk_base::HTTP_DEFAULT_PORT) {
nic_info->http.start_time_ms = now;
} else if (port == talk_base::HTTP_SECURE_PORT) {
nic_info->https.start_time_ms = now;
} else {
LOG(LS_ERROR) << "Registering start time for unknown port: " << port;
}
} else {
LOG(LS_ERROR) << "Error, no nic info found while registering http start.";
}
}
} // namespace talk_base