| /* |
| * libjingle |
| * Copyright 2004--2005, Google Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "talk/p2p/base/session.h" |
| #include "talk/base/common.h" |
| #include "talk/base/logging.h" |
| #include "talk/base/helpers.h" |
| #include "talk/xmpp/constants.h" |
| #include "talk/xmpp/jid.h" |
| #include "talk/p2p/base/sessionclient.h" |
| #include "talk/p2p/base/transport.h" |
| #include "talk/p2p/base/transportchannelproxy.h" |
| #include "talk/p2p/base/p2ptransport.h" |
| #include "talk/p2p/base/constants.h" |
| |
| namespace { |
| |
| const uint32 MSG_TIMEOUT = 1; |
| const uint32 MSG_ERROR = 2; |
| const uint32 MSG_STATE = 3; |
| |
| // This will be initialized at run time to hold the list of default transports. |
| std::string* gDefaultTransports = NULL; |
| size_t gNumDefaultTransports = 0; |
| |
| } // namespace |
| |
| namespace cricket { |
| |
| Session::Session(SessionManager *session_manager, const std::string& name, |
| const SessionID& id, const std::string& session_type, |
| SessionClient* client) { |
| ASSERT(session_manager->signaling_thread()->IsCurrent()); |
| ASSERT(client != NULL); |
| session_manager_ = session_manager; |
| name_ = name; |
| id_ = id; |
| session_type_ = session_type; |
| client_ = client; |
| error_ = ERROR_NONE; |
| state_ = STATE_INIT; |
| initiator_ = false; |
| description_ = NULL; |
| remote_description_ = NULL; |
| transport_ = NULL; |
| compatibility_mode_ = false; |
| } |
| |
| Session::~Session() { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| ASSERT(state_ != STATE_DEINIT); |
| state_ = STATE_DEINIT; |
| SignalState(this, state_); |
| |
| delete description_; |
| delete remote_description_; |
| |
| for (ChannelMap::iterator iter = channels_.begin(); |
| iter != channels_.end(); |
| ++iter) { |
| iter->second->SignalDestroyed(iter->second); |
| delete iter->second; |
| } |
| |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| delete *iter; |
| } |
| |
| delete transport_; |
| } |
| |
| bool Session::Initiate(const std::string &to, |
| std::vector<buzz::XmlElement*>* extra_xml, |
| const SessionDescription *description) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Only from STATE_INIT |
| if (state_ != STATE_INIT) |
| return false; |
| |
| // Setup for signaling. |
| remote_name_ = to; |
| initiator_ = true; |
| description_ = description; |
| |
| // Make sure we have transports to negotiate. |
| CreateTransports(); |
| |
| // Send the initiate message, including the application and transport offers. |
| XmlElements elems; |
| elems.push_back(client_->TranslateSessionDescription(description)); |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| buzz::XmlElement* elem = (*iter)->CreateTransportOffer(); |
| elems.push_back(elem); |
| } |
| |
| if (extra_xml != NULL) { |
| std::vector<buzz::XmlElement*>::iterator iter = extra_xml->begin(); |
| for (std::vector<buzz::XmlElement*>::iterator iter = extra_xml->begin(); |
| iter != extra_xml->end(); |
| ++iter) { |
| elems.push_back(new buzz::XmlElement(**iter)); |
| } |
| } |
| |
| SendSessionMessage("initiate", elems); |
| |
| SetState(Session::STATE_SENTINITIATE); |
| |
| // We speculatively start attempting connection of the P2P transports. |
| ConnectDefaultTransportChannels(true); |
| return true; |
| } |
| |
| void Session::ConnectDefaultTransportChannels(bool create) { |
| Transport* transport = GetTransport(kNsP2pTransport); |
| if (transport) { |
| for (ChannelMap::iterator iter = channels_.begin(); |
| iter != channels_.end(); |
| ++iter) { |
| ASSERT(create != transport->HasChannel(iter->first)); |
| if (create) { |
| transport->CreateChannel(iter->first, session_type()); |
| } |
| } |
| transport->ConnectChannels(); |
| } |
| } |
| |
| void Session::CreateDefaultTransportChannel(const std::string& name) { |
| // This method is only relevant when we have created the default transport |
| // but not received a transport-accept. |
| ASSERT(transport_ == NULL); |
| ASSERT(state_ == STATE_SENTINITIATE); |
| |
| Transport* p2p_transport = GetTransport(kNsP2pTransport); |
| if (p2p_transport) { |
| ASSERT(!p2p_transport->HasChannel(name)); |
| p2p_transport->CreateChannel(name, session_type()); |
| } |
| } |
| |
| bool Session::Accept(const SessionDescription *description) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Only if just received initiate |
| if (state_ != STATE_RECEIVEDINITIATE) |
| return false; |
| |
| // Setup for signaling. |
| initiator_ = false; |
| description_ = description; |
| |
| // If we haven't selected a transport, wait for ChooseTransport to complete |
| if (transport_ == NULL) |
| return true; |
| |
| // Send the accept message. |
| XmlElements elems; |
| elems.push_back(client_->TranslateSessionDescription(description_)); |
| SendSessionMessage("accept", elems); |
| SetState(Session::STATE_SENTACCEPT); |
| |
| return true; |
| } |
| |
| bool Session::Reject() { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Reject is sent in response to an initiate or modify, to reject the |
| // request |
| if (state_ != STATE_RECEIVEDINITIATE && state_ != STATE_RECEIVEDMODIFY) |
| return false; |
| |
| // Setup for signaling. |
| initiator_ = false; |
| |
| // Send the reject message. |
| SendSessionMessage("reject", XmlElements()); |
| SetState(STATE_SENTREJECT); |
| |
| return true; |
| } |
| |
| bool Session::Redirect(const std::string & target) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Redirect is sent in response to an initiate or modify, to redirect the |
| // request |
| if (state_ != STATE_RECEIVEDINITIATE) |
| return false; |
| |
| // Setup for signaling. |
| initiator_ = false; |
| |
| // Send a redirect message to the given target. We include an element that |
| // names the redirector (us), which may be useful to the other side. |
| |
| buzz::XmlElement* target_elem = new buzz::XmlElement(QN_REDIRECT_TARGET); |
| target_elem->AddAttr(buzz::QN_NAME, target); |
| |
| buzz::XmlElement* cookie = new buzz::XmlElement(QN_REDIRECT_COOKIE); |
| buzz::XmlElement* regarding = new buzz::XmlElement(QN_REDIRECT_REGARDING); |
| regarding->AddAttr(buzz::QN_NAME, name_); |
| cookie->AddElement(regarding); |
| |
| XmlElements elems; |
| elems.push_back(target_elem); |
| elems.push_back(cookie); |
| SendSessionMessage("redirect", elems); |
| |
| // A redirect puts us in the same state as reject. It just sends a different |
| // kind of reject message, if you like. |
| SetState(STATE_SENTREDIRECT); |
| |
| return true; |
| } |
| |
| bool Session::Terminate() { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Either side can terminate, at any time. |
| switch (state_) { |
| case STATE_SENTTERMINATE: |
| case STATE_RECEIVEDTERMINATE: |
| return false; |
| |
| case STATE_SENTREDIRECT: |
| // We must not send terminate if we redirect. |
| break; |
| |
| case STATE_SENTREJECT: |
| case STATE_RECEIVEDREJECT: |
| // We don't need to send terminate if we sent or received a reject... |
| // it's implicit. |
| break; |
| |
| default: |
| SendSessionMessage("terminate", XmlElements()); |
| break; |
| } |
| |
| SetState(STATE_SENTTERMINATE); |
| return true; |
| } |
| |
| void Session::SendInfoMessage(const XmlElements& elems) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| SendSessionMessage("info", elems); |
| } |
| |
| void Session::SetPotentialTransports(const std::string names[], size_t length) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| for (size_t i = 0; i < length; ++i) { |
| Transport* transport = NULL; |
| if (names[i] == kNsP2pTransport) { |
| transport = new P2PTransport(session_manager_); |
| } else { |
| ASSERT(false); |
| } |
| |
| if (transport) { |
| ASSERT(transport->name() == names[i]); |
| potential_transports_.push_back(transport); |
| transport->SignalConnecting.connect( |
| this, &Session::OnTransportConnecting); |
| transport->SignalWritableState.connect( |
| this, &Session::OnTransportWritable); |
| transport->SignalRequestSignaling.connect( |
| this, &Session::OnTransportRequestSignaling); |
| transport->SignalTransportMessage.connect( |
| this, &Session::OnTransportSendMessage); |
| transport->SignalTransportError.connect( |
| this, &Session::OnTransportSendError); |
| transport->SignalChannelGone.connect( |
| this, &Session::OnTransportChannelGone); |
| } |
| } |
| } |
| |
| Transport* Session::GetTransport(const std::string& name) { |
| if (transport_ != NULL) { |
| if (name == transport_->name()) |
| return transport_; |
| } else { |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| if (name == (*iter)->name()) |
| return *iter; |
| } |
| } |
| return NULL; |
| } |
| |
| TransportChannel* Session::CreateChannel(const std::string& name) { |
| //ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT(channels_.find(name) == channels_.end()); |
| TransportChannelProxy* channel = new TransportChannelProxy(name, session_type_); |
| channels_[name] = channel; |
| if (transport_) { |
| ASSERT(!transport_->HasChannel(name)); |
| channel->SetImplementation(transport_->CreateChannel(name, session_type_)); |
| } else if (state_ == STATE_SENTINITIATE) { |
| // In this case, we have already speculatively created the default |
| // transport. We should create this channel as well so that it may begin |
| // early connection. |
| CreateDefaultTransportChannel(name); |
| } |
| return channel; |
| } |
| |
| TransportChannel* Session::GetChannel(const std::string& name) { |
| ChannelMap::iterator iter = channels_.find(name); |
| return (iter != channels_.end()) ? iter->second : NULL; |
| } |
| |
| void Session::DestroyChannel(TransportChannel* channel) { |
| ChannelMap::iterator iter = channels_.find(channel->name()); |
| ASSERT(iter != channels_.end()); |
| ASSERT(channel == iter->second); |
| channels_.erase(iter); |
| channel->SignalDestroyed(channel); |
| delete channel; |
| } |
| |
| TransportChannelImpl* Session::GetImplementation(TransportChannel* channel) { |
| ChannelMap::iterator iter = channels_.find(channel->name()); |
| return (iter != channels_.end()) ? iter->second->impl() : NULL; |
| } |
| |
| void Session::CreateTransports() { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT((state_ == STATE_INIT) |
| || (state_ == STATE_RECEIVEDINITIATE)); |
| if (potential_transports_.empty()) { |
| if (gDefaultTransports == NULL) { |
| gNumDefaultTransports = 1; |
| gDefaultTransports = new std::string[1]; |
| gDefaultTransports[0] = kNsP2pTransport; |
| } |
| SetPotentialTransports(gDefaultTransports, gNumDefaultTransports); |
| } |
| } |
| |
| bool Session::ChooseTransport(const buzz::XmlElement* stanza) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT(state_ == STATE_RECEIVEDINITIATE); |
| ASSERT(transport_ == NULL); |
| |
| // Make sure we have decided on our own transports. |
| CreateTransports(); |
| |
| // Retrieve the session message. |
| const buzz::XmlElement* session = stanza->FirstNamed(QN_SESSION); |
| ASSERT(session != NULL); |
| |
| // Try the offered transports until we find one that we support. |
| bool found_offer = false; |
| const buzz::XmlElement* elem = session->FirstElement(); |
| while (elem) { |
| if (elem->Name().LocalPart() == "transport") { |
| found_offer = true; |
| Transport* transport = GetTransport(elem->Name().Namespace()); |
| if (transport && transport->OnTransportOffer(elem)) { |
| SetTransport(transport); |
| break; |
| } |
| } |
| elem = elem->NextElement(); |
| } |
| |
| // If the offer did not include any transports, then we are talking to an |
| // old client. In that case, we turn on compatibility mode, and we assume |
| // an offer containing just P2P, which is all that old clients support. |
| if (!found_offer) { |
| compatibility_mode_ = true; |
| |
| Transport* transport = GetTransport(kNsP2pTransport); |
| ASSERT(transport != NULL); |
| |
| scoped_ptr<buzz::XmlElement> transport_offer( |
| new buzz::XmlElement(kQnP2pTransport, true)); |
| bool valid = transport->OnTransportOffer(transport_offer.get()); |
| ASSERT(valid); |
| if (valid) |
| SetTransport(transport); |
| } |
| |
| if (!transport_) { |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_NOT_ACCEPTABLE, "modify", |
| "no supported transport in offer", NULL); |
| return false; |
| } |
| |
| // Get the description of the transport we picked. |
| buzz::XmlElement* answer = transport_->CreateTransportAnswer(); |
| ASSERT(answer->Name() == buzz::QName(transport_->name(), "transport")); |
| |
| // Send a transport-accept message telling the other side our decision, |
| // unless this is an old client that is not expecting one. |
| if (!compatibility_mode_) { |
| XmlElements elems; |
| elems.push_back(answer); |
| SendSessionMessage("transport-accept", elems); |
| } |
| |
| // If the user wants to accept, allow that now |
| if (description_) { |
| Accept(description_); |
| } |
| |
| return true; |
| } |
| |
| void Session::SetTransport(Transport* transport) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT(transport_ == NULL); |
| transport_ = transport; |
| |
| // Drop the transports that were not selected. |
| bool found = false; |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| if (*iter == transport_) { |
| found = true; |
| } else { |
| delete *iter; |
| } |
| } |
| potential_transports_.clear(); |
| |
| // We require the selected transport to be one of the potential transports |
| ASSERT(found); |
| |
| // Create implementations for all of the channels if they don't exist. |
| for (ChannelMap::iterator iter = channels_.begin(); |
| iter != channels_.end(); |
| ++iter) { |
| TransportChannelProxy* channel = iter->second; |
| TransportChannelImpl* impl = transport_->GetChannel(channel->name()); |
| if (impl == NULL) |
| impl = transport_->CreateChannel(channel->name(), session_type()); |
| ASSERT(impl != NULL); |
| channel->SetImplementation(impl); |
| } |
| |
| // Have this transport start connecting if it is not already. |
| // (We speculatively connect the most common transport right away.) |
| transport_->ConnectChannels(); |
| } |
| |
| void Session::SetState(State state) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| if (state != state_) { |
| state_ = state; |
| SignalState(this, state_); |
| session_manager_->signaling_thread()->Post(this, MSG_STATE); |
| } |
| } |
| |
| void Session::SetError(Error error) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| if (error != error_) { |
| error_ = error; |
| SignalError(this, error); |
| if (error_ != ERROR_NONE) |
| session_manager_->signaling_thread()->Post(this, MSG_ERROR); |
| } |
| } |
| |
| void Session::OnSignalingReady() { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| // Forward this to every transport. Those that did not request it should |
| // ignore this call. |
| if (transport_ != NULL) { |
| transport_->OnSignalingReady(); |
| } else { |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| (*iter)->OnSignalingReady(); |
| } |
| } |
| } |
| |
| void Session::OnTransportConnecting(Transport* transport) { |
| // This is an indication that we should begin watching the writability |
| // state of the transport. |
| OnTransportWritable(transport); |
| } |
| |
| void Session::OnTransportWritable(Transport* transport) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT((NULL == transport_) || (transport == transport_)); |
| |
| // If the transport is not writable, start a timer to make sure that it |
| // becomes writable within a reasonable amount of time. If it does not, we |
| // terminate since we can't actually send data. If the transport is writable, |
| // cancel the timer. Note that writability transitions may occur repeatedly |
| // during the lifetime of the session. |
| |
| session_manager_->signaling_thread()->Clear(this, MSG_TIMEOUT); |
| if (transport->HasChannels() && !transport->writable()) { |
| session_manager_->signaling_thread()->PostDelayed( |
| session_manager_->session_timeout() * 1000, this, MSG_TIMEOUT); |
| } |
| } |
| |
| void Session::OnTransportRequestSignaling(Transport* transport) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| SignalRequestSignaling(this); |
| } |
| |
| void Session::OnTransportSendMessage(Transport* transport, |
| const XmlElements& elems) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| for (size_t i = 0; i < elems.size(); ++i) |
| ASSERT(elems[i]->Name() == buzz::QName(transport->name(), "transport")); |
| |
| if (compatibility_mode_) { |
| // In backward compatibility mode, we send a candidates message. |
| XmlElements candidates; |
| for (size_t i = 0; i < elems.size(); ++i) { |
| for (const buzz::XmlElement* elem = elems[i]->FirstElement(); |
| elem != NULL; |
| elem = elem->NextElement()) { |
| ASSERT(elem->Name() == kQnP2pCandidate); |
| |
| // Convert this candidate to an old style candidate (namespace change) |
| buzz::XmlElement* legacy_candidate = new buzz::XmlElement(*elem); |
| legacy_candidate->SetName(kQnLegacyCandidate); |
| candidates.push_back(legacy_candidate); |
| } |
| delete elems[i]; |
| } |
| |
| SendSessionMessage("candidates", candidates); |
| } else { |
| // If we haven't finished negotiation, then we may later discover that we |
| // need compatibility mode, in which case, we will need to re-send these. |
| if ((transport_ == NULL) && (transport->name() == kNsP2pTransport)) { |
| for (size_t i = 0; i < elems.size(); ++i) |
| candidates_.push_back(new buzz::XmlElement(*elems[i])); |
| } |
| |
| SendSessionMessage("transport-info", elems); |
| } |
| } |
| |
| void Session::OnTransportSendError(Transport* transport, |
| const buzz::XmlElement* stanza, |
| const buzz::QName& name, |
| const std::string& type, |
| const std::string& text, |
| const buzz::XmlElement* extra_info) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| SignalErrorMessage(this, stanza, name, type, text, extra_info); |
| } |
| |
| void Session::OnTransportChannelGone(Transport* transport, |
| const std::string& name) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| SignalChannelGone(this, name); |
| } |
| |
| void Session::SendSessionMessage( |
| const std::string& type, const std::vector<buzz::XmlElement*>& elems) { |
| scoped_ptr<buzz::XmlElement> iq(new buzz::XmlElement(buzz::QN_IQ)); |
| iq->SetAttr(buzz::QN_TO, remote_name_); |
| iq->SetAttr(buzz::QN_TYPE, buzz::STR_SET); |
| |
| buzz::XmlElement *session = new buzz::XmlElement(QN_SESSION, true); |
| session->AddAttr(buzz::QN_TYPE, type); |
| session->AddAttr(buzz::QN_ID, id_.id_str()); |
| session->AddAttr(QN_INITIATOR, id_.initiator()); |
| |
| for (size_t i = 0; i < elems.size(); ++i) |
| session->AddElement(elems[i]); |
| |
| iq->AddElement(session); |
| SignalOutgoingMessage(this, iq.get()); |
| } |
| |
| void Session::SendAcknowledgementMessage(const buzz::XmlElement* stanza) { |
| scoped_ptr<buzz::XmlElement> ack(new buzz::XmlElement(buzz::QN_IQ)); |
| ack->SetAttr(buzz::QN_TO, remote_name_); |
| ack->SetAttr(buzz::QN_ID, stanza->Attr(buzz::QN_ID)); |
| ack->SetAttr(buzz::QN_TYPE, "result"); |
| |
| SignalOutgoingMessage(this, ack.get()); |
| } |
| |
| void Session::OnIncomingMessage(const buzz::XmlElement* stanza) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| ASSERT(stanza->Name() == buzz::QN_IQ); |
| buzz::Jid remote(remote_name_); |
| buzz::Jid from(stanza->Attr(buzz::QN_FROM)); |
| ASSERT(state_ == STATE_INIT || from == remote); |
| |
| const buzz::XmlElement* session = stanza->FirstNamed(QN_SESSION); |
| ASSERT(session != NULL); |
| |
| if (stanza->Attr(buzz::QN_TYPE) != buzz::STR_SET) { |
| ASSERT(false); |
| return; |
| } |
| |
| ASSERT(session->HasAttr(buzz::QN_TYPE)); |
| std::string type = session->Attr(buzz::QN_TYPE); |
| |
| bool valid = false; |
| |
| if (type == "initiate") { |
| valid = OnInitiateMessage(stanza, session); |
| } else if (type == "accept") { |
| valid = OnAcceptMessage(stanza, session); |
| } else if (type == "reject") { |
| valid = OnRejectMessage(stanza, session); |
| } else if (type == "redirect") { |
| valid = OnRedirectMessage(stanza, session); |
| } else if (type == "info") { |
| valid = OnInfoMessage(stanza, session); |
| } else if (type == "transport-accept") { |
| valid = OnTransportAcceptMessage(stanza, session); |
| } else if (type == "transport-info") { |
| valid = OnTransportInfoMessage(stanza, session); |
| } else if (type == "terminate") { |
| valid = OnTerminateMessage(stanza, session); |
| } else if (type == "candidates") { |
| // This is provided for backward compatibility. |
| // TODO: Remove this once old candidates are gone. |
| valid = OnCandidatesMessage(stanza, session); |
| } else { |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", |
| "unknown session message type", NULL); |
| } |
| |
| // If the message was not valid, we should have sent back an error above. |
| // If it was valid, then we send an acknowledgement here. |
| if (valid) |
| SendAcknowledgementMessage(stanza); |
| } |
| |
| void Session::OnFailedSend(const buzz::XmlElement* orig_stanza, |
| const buzz::XmlElement* error_stanza) { |
| ASSERT(session_manager_->signaling_thread()->IsCurrent()); |
| |
| const buzz::XmlElement* orig_session = orig_stanza->FirstNamed(QN_SESSION); |
| ASSERT(orig_session != NULL); |
| |
| std::string error_type = "cancel"; |
| |
| const buzz::XmlElement* error = error_stanza->FirstNamed(buzz::QN_ERROR); |
| ASSERT(error != NULL); |
| if (error) { |
| ASSERT(error->HasAttr(buzz::QN_TYPE)); |
| error_type = error->Attr(buzz::QN_TYPE); |
| |
| LOG(LERROR) << "Session error:\n" << error->Str() << "\n" |
| << "in response to:\n" << orig_session->Str(); |
| } |
| |
| bool fatal_error = false; |
| |
| ASSERT(orig_session->HasAttr(buzz::QN_TYPE)); |
| if ((orig_session->Attr(buzz::QN_TYPE) == "transport-info") |
| || (orig_session->Attr(buzz::QN_TYPE) == "candidates")) { |
| // Transport messages frequently generate errors because they are sent right |
| // when we detect a network failure. For that reason, we ignore such |
| // errors, because if we do not establish writability again, we will |
| // terminate anyway. The exceptions are transport-specific error tags, |
| // which we pass on to the respective transport. |
| for (const buzz::XmlElement* elem = error->FirstElement(); |
| NULL != elem; elem = elem->NextElement()) { |
| if (Transport* transport = GetTransport(elem->Name().Namespace())) { |
| if (!transport->OnTransportError(orig_session, elem)) { |
| fatal_error = true; |
| break; |
| } |
| } |
| } |
| } else if ((error_type != "continue") && (error_type != "wait")) { |
| // We do not set an error if the other side said it is okay to continue |
| // (possibly after waiting). These errors can be ignored. |
| fatal_error = true; |
| } |
| |
| if (fatal_error) { |
| SetError(ERROR_RESPONSE); |
| } |
| } |
| |
| bool Session::OnInitiateMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| if (!CheckState(stanza, STATE_INIT)) |
| return false; |
| if (!FindRemoteSessionDescription(stanza, session)) |
| return false; |
| |
| initiator_ = false; |
| remote_name_ = stanza->Attr(buzz::QN_FROM); |
| SetState(STATE_RECEIVEDINITIATE); |
| return true; |
| } |
| |
| bool Session::OnAcceptMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| if (!CheckState(stanza, STATE_SENTINITIATE)) |
| return false; |
| if (!FindRemoteSessionDescription(stanza, session)) |
| return false; |
| |
| SetState(STATE_RECEIVEDACCEPT); |
| return true; |
| } |
| |
| bool Session::OnRejectMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| if (!CheckState(stanza, STATE_SENTINITIATE)) |
| return false; |
| |
| SetState(STATE_RECEIVEDREJECT); |
| return true; |
| } |
| |
| bool Session::OnRedirectMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| if (!CheckState(stanza, STATE_SENTINITIATE)) |
| return false; |
| |
| const buzz::XmlElement *redirect_target; |
| if (!FindRequiredElement(stanza, session, QN_REDIRECT_TARGET, |
| &redirect_target)) |
| return false; |
| |
| if (!FindRequiredAttribute(stanza, redirect_target, buzz::QN_NAME, |
| &remote_name_)) |
| return false; |
| |
| const buzz::XmlElement* redirect_cookie = |
| session->FirstNamed(QN_REDIRECT_COOKIE); |
| |
| XmlElements elems; |
| elems.push_back(client_->TranslateSessionDescription(description_)); |
| if (redirect_cookie) |
| elems.push_back(new buzz::XmlElement(*redirect_cookie)); |
| SendSessionMessage("initiate", elems); |
| |
| // Clear the connection timeout (if any). We will start the connection |
| // timer from scratch when SignalConnecting fires. |
| session_manager_->signaling_thread()->Clear(this, MSG_TIMEOUT); |
| |
| // Reset all of the sockets back into the initial state. |
| for (TransportList::iterator iter = potential_transports_.begin(); |
| iter != potential_transports_.end(); |
| ++iter) { |
| (*iter)->ResetChannels(); |
| } |
| |
| ConnectDefaultTransportChannels(false); |
| return true; |
| } |
| |
| bool Session::OnInfoMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| XmlElements elems; |
| for (const buzz::XmlElement* elem = session->FirstElement(); |
| elem != NULL; |
| elem = elem->NextElement()) { |
| elems.push_back(new buzz::XmlElement(*elem)); |
| } |
| |
| SignalInfoMessage(this, elems); |
| return true; |
| } |
| |
| bool Session::OnTransportAcceptMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| if (!CheckState(stanza, STATE_SENTINITIATE)) |
| return false; |
| |
| Transport* transport = NULL; |
| const buzz::XmlElement* transport_elem = NULL; |
| |
| for(const buzz::XmlElement* elem = session->FirstElement(); |
| elem != NULL; |
| elem = elem->NextElement()) { |
| if (elem->Name().LocalPart() == "transport") { |
| Transport* transport = GetTransport(elem->Name().Namespace()); |
| if (transport) { |
| if (transport_elem) { // trying to accept two transport? |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_BAD_REQUEST, |
| "modify", "transport-accept has two answers", |
| NULL); |
| return false; |
| } |
| |
| transport_elem = elem; |
| if (!transport->OnTransportAnswer(transport_elem)) { |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_BAD_REQUEST, |
| "modify", "transport-accept is not acceptable", |
| NULL); |
| return false; |
| } |
| SetTransport(transport); |
| } |
| } |
| } |
| |
| if (!transport_elem) { |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_NOT_ALLOWED, "modify", |
| "no supported transport in answer", NULL); |
| return false; |
| } |
| |
| // If we discovered that we need compatibility mode and we have sent some |
| // candidates already (using transport-info), then we need to re-send them |
| // using the candidates message. |
| if (compatibility_mode_ && (candidates_.size() > 0)) { |
| ASSERT(transport_ != NULL); |
| ASSERT(transport_->name() == kNsP2pTransport); |
| OnTransportSendMessage(transport_, candidates_); |
| } else { |
| for (size_t i = 0; i < candidates_.size(); ++i) |
| delete candidates_[i]; |
| } |
| candidates_.clear(); |
| |
| return true; |
| } |
| |
| bool Session::OnTransportInfoMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| for(const buzz::XmlElement* elem = session->FirstElement(); |
| elem != NULL; |
| elem = elem->NextElement()) { |
| if (elem->Name().LocalPart() == "transport") { |
| Transport* transport = GetTransport(elem->Name().Namespace()); |
| if (transport) { |
| if (!transport->OnTransportMessage(elem, stanza)) |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| bool Session::OnTerminateMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| for (const buzz::XmlElement *elem = session->FirstElement(); |
| elem != NULL; |
| elem = elem->NextElement()) { |
| // elem->Name().LocalPart() is the reason for termination |
| SignalReceivedTerminateReason(this, elem->Name().LocalPart()); |
| // elem->FirstElement() might contain a debug string for termination |
| const buzz::XmlElement *debugElement = elem->FirstElement(); |
| if (debugElement != NULL) { |
| LOG(LS_VERBOSE) << "Received error on call: " |
| << debugElement->Name().LocalPart(); |
| } |
| } |
| SetState(STATE_RECEIVEDTERMINATE); |
| return true; |
| } |
| |
| bool Session::OnCandidatesMessage(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| // If we don't have a transport, then this is the first candidates message. |
| // We first create a fake transport-accept message in order to finish the |
| // negotiation and create a transport. |
| if (!transport_) { |
| compatibility_mode_ = true; |
| |
| scoped_ptr<buzz::XmlElement> transport_accept( |
| new buzz::XmlElement(QN_SESSION)); |
| transport_accept->SetAttr(buzz::QN_TYPE, "transport-accept"); |
| |
| buzz::XmlElement* transport_offer = |
| new buzz::XmlElement(kQnP2pTransport, true); |
| transport_accept->AddElement(transport_offer); |
| |
| // It is okay to pass the original stanza here. That is only used if we |
| // send an error message. Normal processing looks only at transport_accept. |
| bool valid = OnTransportAcceptMessage(stanza, transport_accept.get()); |
| ASSERT(valid); |
| } |
| |
| ASSERT(transport_ != NULL); |
| ASSERT(transport_->name() == kNsP2pTransport); |
| |
| // Wrap the candidates in a transport element as they would appear in a |
| // transport-info message and send this to the transport. |
| scoped_ptr<buzz::XmlElement> transport_info( |
| new buzz::XmlElement(kQnP2pTransport, true)); |
| for (const buzz::XmlElement* elem = session->FirstNamed(kQnLegacyCandidate); |
| elem != NULL; |
| elem = elem->NextNamed(kQnLegacyCandidate)) { |
| buzz::XmlElement* new_candidate = new buzz::XmlElement(*elem); |
| new_candidate->SetName(kQnP2pCandidate); |
| transport_info->AddElement(new_candidate); |
| } |
| return transport_->OnTransportMessage(transport_info.get(), stanza); |
| } |
| |
| bool Session::CheckState(const buzz::XmlElement* stanza, State state) { |
| ASSERT(state_ == state); |
| if (state_ != state) { |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_NOT_ALLOWED, "modify", |
| "message not allowed in current state", NULL); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Session::FindRequiredElement(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* parent, |
| const buzz::QName& name, |
| const buzz::XmlElement** elem) { |
| *elem = parent->FirstNamed(name); |
| if (*elem == NULL) { |
| std::string text; |
| text += "element '" + parent->Name().Merged() + |
| "' missing required child '" + name.Merged() + "'"; |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", |
| text, NULL); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Session::FindRemoteSessionDescription(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* session) { |
| buzz::QName qn_session(session_type_, "description"); |
| const buzz::XmlElement* desc; |
| if (!FindRequiredElement(stanza, session, qn_session, &desc)) |
| return false; |
| remote_description_ = client_->CreateSessionDescription(desc); |
| return true; |
| } |
| |
| bool Session::FindRequiredAttribute(const buzz::XmlElement* stanza, |
| const buzz::XmlElement* elem, |
| const buzz::QName& name, |
| std::string* value) { |
| if (!elem->HasAttr(name)) { |
| std::string text; |
| text += "element '" + elem->Name().Merged() + |
| "' missing required attribute '" + name.Merged() + "'"; |
| SignalErrorMessage(this, stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", |
| text, NULL); |
| return false; |
| } else { |
| *value = elem->Attr(name); |
| return true; |
| } |
| } |
| |
| void Session::OnMessage(talk_base::Message *pmsg) { |
| switch(pmsg->message_id) { |
| case MSG_TIMEOUT: |
| // Session timeout has occured. |
| SetError(ERROR_TIME); |
| break; |
| |
| case MSG_ERROR: |
| // Any of the defined errors is most likely fatal. |
| Terminate(); |
| break; |
| |
| case MSG_STATE: |
| switch (state_) { |
| case STATE_SENTACCEPT: |
| case STATE_RECEIVEDACCEPT: |
| SetState(STATE_INPROGRESS); |
| ASSERT(transport_ != NULL); |
| break; |
| |
| case STATE_SENTREJECT: |
| case STATE_SENTREDIRECT: |
| case STATE_RECEIVEDREJECT: |
| Terminate(); |
| break; |
| |
| case STATE_SENTTERMINATE: |
| case STATE_RECEIVEDTERMINATE: |
| session_manager_->DestroySession(this); |
| break; |
| |
| default: |
| // Explicitly ignoring some states here. |
| break; |
| } |
| break; |
| } |
| } |
| |
| } // namespace cricket |