implement handling ALTERNATE-SERVER response from turn protocol as
specified in RFC 5766, also created 2 test cases for both the normal
redirection case as well as when a pingpong situation happens, the
allocation should fail

BUG=1986 TURN ALTERNATE-SERVER support
R=juberti@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/21249004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@6985 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/p2p/base/stun.cc b/talk/p2p/base/stun.cc
index be96b76..061fd9a 100644
--- a/talk/p2p/base/stun.cc
+++ b/talk/p2p/base/stun.cc
@@ -41,6 +41,7 @@
 
 namespace cricket {
 
+const char STUN_ERROR_REASON_TRY_ALTERNATE_SERVER[] = "Try Alternate Server";
 const char STUN_ERROR_REASON_BAD_REQUEST[] = "Bad Request";
 const char STUN_ERROR_REASON_UNAUTHORIZED[] = "Unauthorized";
 const char STUN_ERROR_REASON_FORBIDDEN[] = "Forbidden";
@@ -401,7 +402,7 @@
     case STUN_ATTR_NONCE:               return STUN_VALUE_BYTE_STRING;
     case STUN_ATTR_XOR_MAPPED_ADDRESS:  return STUN_VALUE_XOR_ADDRESS;
     case STUN_ATTR_SOFTWARE:            return STUN_VALUE_BYTE_STRING;
-    case STUN_ATTR_ALTERNATE_SERVER:    return STUN_VALUE_BYTE_STRING;
+    case STUN_ATTR_ALTERNATE_SERVER:    return STUN_VALUE_ADDRESS;
     case STUN_ATTR_FINGERPRINT:         return STUN_VALUE_UINT32;
     case STUN_ATTR_RETRANSMIT_COUNT:    return STUN_VALUE_UINT32;
     default:                            return STUN_VALUE_UNKNOWN;
diff --git a/talk/p2p/base/stun.h b/talk/p2p/base/stun.h
index b22b51e..c4f522b 100644
--- a/talk/p2p/base/stun.h
+++ b/talk/p2p/base/stun.h
@@ -63,7 +63,7 @@
   STUN_ATTR_NONCE                       = 0x0015,  // ByteString
   STUN_ATTR_XOR_MAPPED_ADDRESS          = 0x0020,  // XorAddress
   STUN_ATTR_SOFTWARE                    = 0x8022,  // ByteString
-  STUN_ATTR_ALTERNATE_SERVER            = 0x8023,  // ByteString
+  STUN_ATTR_ALTERNATE_SERVER            = 0x8023,  // Address
   STUN_ATTR_FINGERPRINT                 = 0x8028,  // UInt32
   STUN_ATTR_RETRANSMIT_COUNT            = 0xFF00   // UInt32
 };
@@ -104,6 +104,7 @@
 };
 
 // Strings for the error codes above.
+extern const char STUN_ERROR_REASON_TRY_ALTERNATE_SERVER[];
 extern const char STUN_ERROR_REASON_BAD_REQUEST[];
 extern const char STUN_ERROR_REASON_UNAUTHORIZED[];
 extern const char STUN_ERROR_REASON_UNKNOWN_ATTRIBUTE[];
diff --git a/talk/p2p/base/testturnserver.h b/talk/p2p/base/testturnserver.h
index e2c0ccb..6c30afe 100644
--- a/talk/p2p/base/testturnserver.h
+++ b/talk/p2p/base/testturnserver.h
@@ -29,6 +29,7 @@
 #define TALK_P2P_BASE_TESTTURNSERVER_H_
 
 #include <string>
+#include <vector>
 
 #include "talk/p2p/base/basicpacketsocketfactory.h"
 #include "talk/p2p/base/stun.h"
@@ -41,6 +42,27 @@
 static const char kTestRealm[] = "example.org";
 static const char kTestSoftware[] = "TestTurnServer";
 
+class TestTurnRedirector : public TurnRedirectInterface {
+ public:
+  explicit TestTurnRedirector(const std::vector<rtc::SocketAddress>& addresses)
+      : alternate_server_addresses_(addresses),
+        iter_(alternate_server_addresses_.begin()) {
+  }
+
+  virtual bool ShouldRedirect(const rtc::SocketAddress&,
+                              rtc::SocketAddress* out) {
+    if (!out || iter_ == alternate_server_addresses_.end()) {
+      return false;
+    }
+    *out = *iter_++;
+    return true;
+  }
+
+ private:
+  const std::vector<rtc::SocketAddress>& alternate_server_addresses_;
+  std::vector<rtc::SocketAddress>::const_iterator iter_;
+};
+
 class TestTurnServer : public TurnAuthInterface {
  public:
   TestTurnServer(rtc::Thread* thread,
@@ -61,6 +83,10 @@
 
   TurnServer* server() { return &server_; }
 
+  void set_redirect_hook(TurnRedirectInterface* redirect_hook) {
+    server_.set_redirect_hook(redirect_hook);
+  }
+
   void AddInternalSocket(const rtc::SocketAddress& int_addr,
                          ProtocolType proto) {
     rtc::Thread* thread = rtc::Thread::Current();
diff --git a/talk/p2p/base/turnport.cc b/talk/p2p/base/turnport.cc
index 3faacd1..6ab0e2b 100644
--- a/talk/p2p/base/turnport.cc
+++ b/talk/p2p/base/turnport.cc
@@ -78,6 +78,7 @@
  private:
   // Handles authentication challenge from the server.
   void OnAuthChallenge(StunMessage* response, int code);
+  void OnTryAlternate(StunMessage* response, int code);
   void OnUnknownAttribute(StunMessage* response);
 
   TurnPort* port_;
@@ -253,6 +254,9 @@
       return;
     }
 
+    // Insert the current address to prevent redirection pingpong.
+    attempted_server_addresses_.insert(server_address_.address);
+
     LOG_J(LS_INFO, this) << "Trying to connect to TURN server via "
                          << ProtoToString(server_address_.proto) << " @ "
                          << server_address_.address.ToSensitiveString();
@@ -458,6 +462,38 @@
   }
 }
 
+
+// Update current server address port with the alternate server address port.
+bool TurnPort::SetAlternateServer(const rtc::SocketAddress& address) {
+  // Check if we have seen this address before and reject if we did.
+  AttemptedServerSet::iterator iter = attempted_server_addresses_.find(address);
+  if (iter != attempted_server_addresses_.end()) {
+    LOG_J(LS_WARNING, this) << "Redirection to ["
+                            << address.ToSensitiveString()
+                            << "] ignored, allocation failed.";
+    return false;
+  }
+
+  // If protocol family of server address doesn't match with local, return.
+  if (!IsCompatibleAddress(address)) {
+    LOG(LS_WARNING) << "Server IP address family does not match with "
+                    << "local host address family type";
+    return false;
+  }
+
+  LOG_J(LS_INFO, this) << "Redirecting from TURN server ["
+                       << server_address_.address.ToSensitiveString()
+                       << "] to TURN server ["
+                       << address.ToSensitiveString()
+                       << "]";
+  server_address_ = ProtocolAddress(address, server_address_.proto,
+                                    server_address_.secure);
+
+  // Insert the current address to prevent redirection pingpong.
+  attempted_server_addresses_.insert(server_address_.address);
+  return true;
+}
+
 void TurnPort::ResolveTurnAddress(const rtc::SocketAddress& address) {
   if (resolver_)
     return;
@@ -805,6 +841,9 @@
     case STUN_ERROR_UNAUTHORIZED:       // Unauthrorized.
       OnAuthChallenge(response, error_code->code());
       break;
+    case STUN_ERROR_TRY_ALTERNATE:
+      OnTryAlternate(response, error_code->code());
+      break;
     default:
       LOG_J(LS_WARNING, port_) << "Allocate response error, code="
                                << error_code->code();
@@ -849,6 +888,57 @@
   port_->SendRequest(new TurnAllocateRequest(port_), 0);
 }
 
+void TurnAllocateRequest::OnTryAlternate(StunMessage* response, int code) {
+  // TODO(guoweis): Currently, we only support UDP redirect
+  if (port_->server_address().proto != PROTO_UDP) {
+    LOG_J(LS_WARNING, port_) << "Receiving 300 Alternate Server on non-UDP "
+                         << "allocating request from ["
+                         << port_->server_address().address.ToSensitiveString()
+                         << "], failed as currently not supported";
+    port_->OnAllocateError();
+    return;
+  }
+
+  // According to RFC 5389 section 11, there are use cases where
+  // authentication of response is not possible, we're not validating
+  // message integrity.
+
+  // Get the alternate server address attribute value.
+  const StunAddressAttribute* alternate_server_attr =
+      response->GetAddress(STUN_ATTR_ALTERNATE_SERVER);
+  if (!alternate_server_attr) {
+    LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_ALTERNATE_SERVER "
+                             << "attribute in try alternate error response";
+    port_->OnAllocateError();
+    return;
+  }
+  if (!port_->SetAlternateServer(alternate_server_attr->GetAddress())) {
+    port_->OnAllocateError();
+    return;
+  }
+
+  // Check the attributes.
+  const StunByteStringAttribute* realm_attr =
+      response->GetByteString(STUN_ATTR_REALM);
+  if (realm_attr) {
+    LOG_J(LS_INFO, port_) << "Applying STUN_ATTR_REALM attribute in "
+                          << "try alternate error response.";
+    port_->set_realm(realm_attr->GetString());
+  }
+
+  const StunByteStringAttribute* nonce_attr =
+      response->GetByteString(STUN_ATTR_NONCE);
+  if (nonce_attr) {
+    LOG_J(LS_INFO, port_) << "Applying STUN_ATTR_NONCE attribute in "
+                          << "try alternate error response.";
+    port_->set_nonce(nonce_attr->GetString());
+  }
+
+  // Send another allocate request to alternate server,
+  // with the received realm and nonce values.
+  port_->SendRequest(new TurnAllocateRequest(port_), 0);
+}
+
 TurnRefreshRequest::TurnRefreshRequest(TurnPort* port)
     : StunRequest(new TurnMessage()),
       port_(port) {
diff --git a/talk/p2p/base/turnport.h b/talk/p2p/base/turnport.h
index d73b11d..b9ec3b0 100644
--- a/talk/p2p/base/turnport.h
+++ b/talk/p2p/base/turnport.h
@@ -30,6 +30,7 @@
 
 #include <stdio.h>
 #include <list>
+#include <set>
 #include <string>
 
 #include "talk/p2p/base/port.h"
@@ -157,6 +158,7 @@
 
   typedef std::list<TurnEntry*> EntryList;
   typedef std::map<rtc::Socket::Option, int> SocketOptionsMap;
+  typedef std::set<rtc::SocketAddress> AttemptedServerSet;
 
   virtual void OnMessage(rtc::Message* pmsg);
 
@@ -170,6 +172,7 @@
     }
   }
 
+  bool SetAlternateServer(const rtc::SocketAddress& address);
   void ResolveTurnAddress(const rtc::SocketAddress& address);
   void OnResolveResult(rtc::AsyncResolverInterface* resolver);
 
@@ -207,6 +210,7 @@
 
   ProtocolAddress server_address_;
   RelayCredentials credentials_;
+  AttemptedServerSet attempted_server_addresses_;
 
   rtc::AsyncPacketSocket* socket_;
   SocketOptionsMap socket_options_;
diff --git a/talk/p2p/base/turnport_unittest.cc b/talk/p2p/base/turnport_unittest.cc
index 44dc64f..14befa9 100644
--- a/talk/p2p/base/turnport_unittest.cc
+++ b/talk/p2p/base/turnport_unittest.cc
@@ -64,6 +64,8 @@
 static const SocketAddress kTurnTcpIntAddr("99.99.99.4",
                                            cricket::TURN_SERVER_PORT);
 static const SocketAddress kTurnUdpExtAddr("99.99.99.5", 0);
+static const SocketAddress kTurnAlternateUdpIntAddr(
+    "99.99.99.6", cricket::TURN_SERVER_PORT);
 static const SocketAddress kTurnUdpIPv6IntAddr(
     "2400:4030:1:2c00:be30:abcd:efab:cdef", cricket::TURN_SERVER_PORT);
 static const SocketAddress kTurnUdpIPv6ExtAddr(
@@ -445,6 +447,103 @@
   ASSERT_EQ(0U, turn_port_->Candidates().size());
 }
 
+// Test try-alternate-server feature.
+TEST_F(TurnPortTest, TestTurnAlternateServer) {
+  std::vector<rtc::SocketAddress> redirect_addresses;
+  redirect_addresses.push_back(kTurnAlternateUdpIntAddr);
+
+  cricket::TestTurnRedirector redirector(redirect_addresses);
+  turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr,
+                                 cricket::PROTO_UDP);
+  turn_server_.set_redirect_hook(&redirector);
+  CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr);
+
+  // Retrieve the address before we run the state machine.
+  const SocketAddress old_addr = turn_port_->server_address().address;
+
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_WAIT(turn_ready_, kTimeout);
+  // Retrieve the address again, the turn port's address should be
+  // changed.
+  const SocketAddress new_addr = turn_port_->server_address().address;
+  EXPECT_NE(old_addr, new_addr);
+  ASSERT_EQ(1U, turn_port_->Candidates().size());
+  EXPECT_EQ(kTurnUdpExtAddr.ipaddr(),
+            turn_port_->Candidates()[0].address().ipaddr());
+  EXPECT_NE(0, turn_port_->Candidates()[0].address().port());
+}
+
+// Test that we fail when we redirect to an address different from
+// current IP family.
+TEST_F(TurnPortTest, TestTurnAlternateServerV4toV6) {
+  std::vector<rtc::SocketAddress> redirect_addresses;
+  redirect_addresses.push_back(kTurnUdpIPv6IntAddr);
+
+  cricket::TestTurnRedirector redirector(redirect_addresses);
+  turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr,
+                                 cricket::PROTO_UDP);
+  turn_server_.set_redirect_hook(&redirector);
+  CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr);
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_WAIT(turn_error_, kTimeout);
+}
+
+// Test that we fail to handle alternate-server response over TCP protocol.
+TEST_F(TurnPortTest, TestTurnAlternateServerTcp) {
+  std::vector<rtc::SocketAddress> redirect_addresses;
+  redirect_addresses.push_back(kTurnAlternateUdpIntAddr);
+
+  cricket::TestTurnRedirector redirector(redirect_addresses);
+  turn_server_.set_redirect_hook(&redirector);
+  turn_server_.AddInternalSocket(kTurnTcpIntAddr, cricket::PROTO_TCP);
+  CreateTurnPort(kTurnUsername, kTurnPassword, kTurnTcpProtoAddr);
+
+  turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, cricket::PROTO_TCP);
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_WAIT(turn_error_, kTimeout);
+}
+
+// Test try-alternate-server catches the case of pingpong.
+TEST_F(TurnPortTest, TestTurnAlternateServerPingPong) {
+  std::vector<rtc::SocketAddress> redirect_addresses;
+  redirect_addresses.push_back(kTurnAlternateUdpIntAddr);
+  redirect_addresses.push_back(kTurnUdpIntAddr);
+
+  cricket::TestTurnRedirector redirector(redirect_addresses);
+
+  turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr,
+                                 cricket::PROTO_UDP);
+  turn_server_.set_redirect_hook(&redirector);
+  CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr);
+
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_WAIT(turn_error_, kTimeout);
+  ASSERT_EQ(0U, turn_port_->Candidates().size());
+  rtc::SocketAddress address;
+  // Verify that we have exhausted all alternate servers instead of
+  // failure caused by other errors.
+  EXPECT_FALSE(redirector.ShouldRedirect(address, &address));
+}
+
+// Test try-alternate-server catch the case of repeated server.
+TEST_F(TurnPortTest, TestTurnAlternateServerDetectRepetition) {
+  std::vector<rtc::SocketAddress> redirect_addresses;
+  redirect_addresses.push_back(kTurnAlternateUdpIntAddr);
+  redirect_addresses.push_back(kTurnAlternateUdpIntAddr);
+
+  cricket::TestTurnRedirector redirector(redirect_addresses);
+
+  turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr,
+                                 cricket::PROTO_UDP);
+  turn_server_.set_redirect_hook(&redirector);
+  CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr);
+
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_WAIT(turn_error_, kTimeout);
+  ASSERT_EQ(0U, turn_port_->Candidates().size());
+}
+
+
 // Run TurnConnectionTest with one-time-use nonce feature.
 // Here server will send a 438 STALE_NONCE error message for
 // every TURN transaction.
@@ -515,4 +614,3 @@
   EXPECT_EQ(last_fd_count, GetFDCount());
 }
 #endif
-
diff --git a/talk/p2p/base/turnserver.cc b/talk/p2p/base/turnserver.cc
index abc065a..08c060d 100644
--- a/talk/p2p/base/turnserver.cc
+++ b/talk/p2p/base/turnserver.cc
@@ -208,6 +208,7 @@
     : thread_(thread),
       nonce_key_(rtc::CreateRandomString(kNonceKeySize)),
       auth_hook_(NULL),
+      redirect_hook_(NULL),
       enable_otu_nonce_(false) {
 }
 
@@ -316,6 +317,15 @@
     return;
   }
 
+  if (redirect_hook_ != NULL && msg.type() == STUN_ALLOCATE_REQUEST) {
+    rtc::SocketAddress address;
+    if (redirect_hook_->ShouldRedirect(conn->src(), &address)) {
+      SendErrorResponseWithAlternateServer(
+          conn, &msg, address);
+      return;
+    }
+  }
+
   // Look up the key that we'll use to validate the M-I. If we have an
   // existing allocation, the key will already be cached.
   Allocation* allocation = FindAllocation(conn);
@@ -334,7 +344,6 @@
   }
 
   if (!allocation && msg.type() == STUN_ALLOCATE_REQUEST) {
-    // This is a new allocate request.
     HandleAllocateRequest(conn, &msg, key);
   } else if (allocation &&
              (msg.type() != STUN_ALLOCATE_REQUEST ||
@@ -551,6 +560,17 @@
   SendStun(conn, &resp);
 }
 
+void TurnServer::SendErrorResponseWithAlternateServer(
+    Connection* conn, const StunMessage* msg,
+    const rtc::SocketAddress& addr) {
+  TurnMessage resp;
+  InitErrorResponse(msg, STUN_ERROR_TRY_ALTERNATE,
+                    STUN_ERROR_REASON_TRY_ALTERNATE_SERVER, &resp);
+  VERIFY(resp.AddAttribute(new StunAddressAttribute(
+      STUN_ATTR_ALTERNATE_SERVER, addr)));
+  SendStun(conn, &resp);
+}
+
 void TurnServer::SendStun(Connection* conn, StunMessage* msg) {
   rtc::ByteBuffer buf;
   // Add a SOFTWARE attribute if one is set.
diff --git a/talk/p2p/base/turnserver.h b/talk/p2p/base/turnserver.h
index 4798232..553d00c 100644
--- a/talk/p2p/base/turnserver.h
+++ b/talk/p2p/base/turnserver.h
@@ -63,6 +63,14 @@
                       std::string* key) = 0;
 };
 
+// An interface enables Turn Server to control redirection behavior.
+class TurnRedirectInterface {
+ public:
+  virtual bool ShouldRedirect(const rtc::SocketAddress& address,
+                              rtc::SocketAddress* out) = 0;
+  virtual ~TurnRedirectInterface() {}
+};
+
 // The core TURN server class. Give it a socket to listen on via
 // AddInternalServerSocket, and a factory to create external sockets via
 // SetExternalSocketFactory, and it's ready to go.
@@ -83,6 +91,10 @@
   // Sets the authentication callback; does not take ownership.
   void set_auth_hook(TurnAuthInterface* auth_hook) { auth_hook_ = auth_hook; }
 
+  void set_redirect_hook(TurnRedirectInterface* redirect_hook) {
+    redirect_hook_ = redirect_hook;
+  }
+
   void set_enable_otu_nonce(bool enable) { enable_otu_nonce_ = enable; }
 
   // Starts listening for packets from internal clients.
@@ -155,6 +167,11 @@
                                           const StunMessage* req,
                                           int code,
                                           const std::string& reason);
+
+  void SendErrorResponseWithAlternateServer(Connection* conn,
+                                            const StunMessage* req,
+                                            const rtc::SocketAddress& addr);
+
   void SendStun(Connection* conn, StunMessage* msg);
   void Send(Connection* conn, const rtc::ByteBuffer& buf);
 
@@ -171,14 +188,17 @@
   std::string realm_;
   std::string software_;
   TurnAuthInterface* auth_hook_;
+  TurnRedirectInterface* redirect_hook_;
   // otu - one-time-use. Server will respond with 438 if it's
   // sees the same nonce in next transaction.
   bool enable_otu_nonce_;
+
   InternalSocketMap server_sockets_;
   ServerSocketMap server_listen_sockets_;
   rtc::scoped_ptr<rtc::PacketSocketFactory>
       external_socket_factory_;
   rtc::SocketAddress external_addr_;
+
   AllocationMap allocations_;
 };