mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-25 09:10:49 +01:00
18bfa12042
Currently, it can be demoed using --stun-test and the icedemo from pjsip as the other client. TODOs: Add ability to do a full demo using only 2 clementine instances. Complete session initiation over XMPP. Add local & port forwarded sockets as options.
621 lines
19 KiB
C++
621 lines
19 KiB
C++
/* This file is part of the XMPP Remote Media Extension.
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation; either version 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "connection.h"
|
|
#include "extensions.h"
|
|
#include "mediaplayerhandler.h"
|
|
#include "mediastoragehandler.h"
|
|
#include "remotecontrolhandler.h"
|
|
|
|
#include <QSocketNotifier>
|
|
#include <QTimer>
|
|
#include <QtDebug>
|
|
#include <QVariant>
|
|
|
|
#include <gloox/capabilities.h>
|
|
#include <gloox/client.h>
|
|
#include <gloox/connectionlistener.h>
|
|
#include <gloox/connectiontcpclient.h>
|
|
#include <gloox/disco.h>
|
|
#include <gloox/discohandler.h>
|
|
#include <gloox/loghandler.h>
|
|
#include <gloox/message.h>
|
|
#include <gloox/messagehandler.h>
|
|
#include <gloox/rosterlistener.h>
|
|
#include <gloox/rostermanager.h>
|
|
|
|
#include <qjson/parser.h>
|
|
|
|
namespace xrme {
|
|
|
|
struct Connection::Private : public gloox::ConnectionListener,
|
|
public gloox::LogHandler,
|
|
public gloox::RosterListener,
|
|
public gloox::DiscoHandler,
|
|
public gloox::MessageHandler,
|
|
public gloox::IqHandler {
|
|
Private(Connection* parent)
|
|
: parent_(parent),
|
|
server_(kDefaultServer),
|
|
jid_resource_(kDefaultJIDResource),
|
|
jid_host_(kDefaultJIDHost),
|
|
verbose_(false),
|
|
media_player_(NULL),
|
|
remote_control_(NULL),
|
|
media_storage_(NULL),
|
|
spontaneous_disconnect_(true),
|
|
media_player_extension_(NULL),
|
|
remote_control_extension_(NULL) {}
|
|
|
|
static const char* kDefaultServer;
|
|
static const char* kDefaultJIDResource;
|
|
static const char* kDefaultJIDHost;
|
|
|
|
Connection* parent_;
|
|
|
|
// Stuff the user sets before Connect()
|
|
QString username_;
|
|
QString password_;
|
|
QString agent_name_;
|
|
QString server_;
|
|
QString jid_resource_;
|
|
QString jid_host_;
|
|
bool verbose_;
|
|
|
|
// Interfaces
|
|
MediaPlayerInterface* media_player_;
|
|
RemoteControlInterface* remote_control_;
|
|
MediaStorageInterface* media_storage_;
|
|
QList<Handler*> handlers_;
|
|
|
|
// Stuff that is valid when we're connected.
|
|
QScopedPointer<gloox::Client> client_;
|
|
QScopedPointer<QSocketNotifier> socket_notifier_;
|
|
|
|
MediaPlayerExtension* media_player_extension_;
|
|
RemoteControlExtension* remote_control_extension_;
|
|
|
|
// After discovering a peer we query it to find its capabilities. Only after
|
|
// it replies to the query do we put it in peers_ and emit PeerFound().
|
|
QList<Peer> peers_;
|
|
QList<Peer> querying_peers_;
|
|
|
|
bool has_peer(const QString& jid_resource) const;
|
|
|
|
// We can't destroy the client_ in the onDisconnect() handler, so we have to
|
|
// do it with a QTimer if we get a spontaneous disconnect.
|
|
bool spontaneous_disconnect_;
|
|
|
|
// gloox::MessageHandler
|
|
void handleMessage(gloox::Stanza* stanza, gloox::MessageSession* session = 0);
|
|
|
|
// gloox::ConnectionListener
|
|
void onConnect();
|
|
void onDisconnect(gloox::ConnectionError e);
|
|
bool onTLSConnect(const gloox::CertInfo& info);
|
|
|
|
// gloox::LogHandler
|
|
void handleLog(gloox::LogLevel level, gloox::LogArea area,
|
|
const std::string& message);
|
|
|
|
// gloox::IqHandler
|
|
bool handleIq(const gloox::IQ& stanza);
|
|
void handleIqID(const gloox::IQ& stanza, int context);
|
|
|
|
|
|
// gloox::RosterListener
|
|
void handleItemAdded(const gloox::JID&) {}
|
|
void handleItemSubscribed(const gloox::JID&) {}
|
|
void handleItemRemoved(const gloox::JID&) {}
|
|
void handleItemUpdated(const gloox::JID&) {}
|
|
void handleItemUnsubscribed(const gloox::JID&) {}
|
|
void handleRoster(const gloox::Roster&) {}
|
|
void handleRosterPresence(const gloox::RosterItem& item, const std::string& resource, gloox::Presence::PresenceType presence, const std::string& msg);
|
|
void handleSelfPresence(const gloox::RosterItem&, const std::string&, gloox::Presence::PresenceType, const std::string&);
|
|
bool handleSubscriptionRequest(const gloox::JID&, const std::string&) { return false; }
|
|
bool handleUnsubscriptionRequest(const gloox::JID&, const std::string&) { return false; }
|
|
void handleNonrosterPresence(const gloox::Presence&) {}
|
|
void handleRosterError(const gloox::IQ&) {}
|
|
|
|
|
|
// gloox::DiscoHandler
|
|
void handleDiscoInfo(const gloox::JID&, const gloox::Disco::Info&, int);
|
|
void handleDiscoItems(const gloox::JID&, const gloox::Disco::Items&, int);
|
|
void handleDiscoError(const gloox::JID&, const gloox::Error*, int);
|
|
|
|
// gloox::MessageHandler
|
|
void handleMessage(const gloox::Message& message, gloox::MessageSession* session);
|
|
};
|
|
|
|
const char* Connection::Private::kDefaultServer = "talk.google.com";
|
|
const char* Connection::Private::kDefaultJIDResource = "tomahawkxrmeagent";
|
|
const char* Connection::Private::kDefaultJIDHost = "gmail.com";
|
|
|
|
|
|
Connection::Connection(QObject* parent)
|
|
: QObject(parent),
|
|
d(new Private(this)) {
|
|
}
|
|
|
|
Connection::~Connection() {
|
|
qDeleteAll(d->handlers_);
|
|
}
|
|
|
|
void Connection::set_username(const QString& username) { d->username_ = username; }
|
|
void Connection::set_password(const QString& password) { d->password_ = password; }
|
|
void Connection::set_agent_name(const QString& agent_name) { d->agent_name_ = agent_name; }
|
|
void Connection::set_server(const QString& server) { d->server_ = server; }
|
|
void Connection::set_jid_resource(const QString& resource) { d->jid_resource_ = resource; }
|
|
void Connection::set_jid_host(const QString& host) { d->jid_host_ = host; }
|
|
void Connection::set_verbose(bool verbose) { d->verbose_ = verbose; }
|
|
|
|
QString Connection::username() const { return d->username_; }
|
|
QString Connection::password() const { return d->password_; }
|
|
QString Connection::agent_name() const { return d->agent_name_; }
|
|
QString Connection::server() const { return d->server_; }
|
|
QString Connection::jid_resource() const { return d->jid_resource_; }
|
|
QString Connection::jid_host() const { return d->jid_host_; }
|
|
bool Connection::is_verbose() const { return d->verbose_; }
|
|
|
|
void Connection::SetMediaPlayer(MediaPlayerInterface* iface) {
|
|
if (d->media_player_) {
|
|
qWarning() << "Connection::SetMediaPlayer: this connection already has a"
|
|
" MediaPlayerInterface set";
|
|
return;
|
|
}
|
|
|
|
if (!iface) {
|
|
qWarning() << "Connection::SetMediaPlayer called with NULL interface";
|
|
return;
|
|
}
|
|
|
|
d->media_player_ = iface;
|
|
d->handlers_ << new MediaPlayerHandler(iface);
|
|
}
|
|
|
|
void Connection::SetRemoteControl(RemoteControlInterface* iface) {
|
|
if (d->media_player_) {
|
|
qWarning() << "Connection::RemoteControlInterface: this connection already"
|
|
" has a RemoteControlInterface set";
|
|
return;
|
|
}
|
|
|
|
if (!iface) {
|
|
qWarning() << "Connection::SetRemoteControl called with NULL interface";
|
|
return;
|
|
}
|
|
|
|
d->remote_control_ = iface;
|
|
d->handlers_ << new RemoteControlHandler(iface);
|
|
}
|
|
|
|
void Connection::SetMediaStorage(MediaStorageInterface* iface) {
|
|
if (d->media_storage_) {
|
|
qWarning() << "Connection::MediaStorageInterface: this connection already"
|
|
" has a RemoteControlInterface set";
|
|
return;
|
|
}
|
|
|
|
if (!iface) {
|
|
qWarning() << "Connection::SetMediaStorage called with NULL interface";
|
|
return;
|
|
}
|
|
|
|
d->media_storage_ = iface;
|
|
d->handlers_ << new MediaStorageHandler(iface);
|
|
}
|
|
|
|
bool Connection::is_connected() const {
|
|
return (d->client_ && d->client_->state() == gloox::StateConnected);
|
|
}
|
|
|
|
QString Connection::jid() const {
|
|
if (is_connected()) {
|
|
return QString::fromUtf8(d->client_->jid().full().c_str());
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
Connection::PeerList Connection::peers() const {
|
|
if (is_connected()) {
|
|
return d->peers_;
|
|
}
|
|
return PeerList();
|
|
}
|
|
|
|
Connection::PeerList Connection::peers(Peer::Capability cap) const {
|
|
PeerList ret;
|
|
foreach (const Peer& peer, peers()) {
|
|
if (peer.caps_ & cap) {
|
|
ret << peer;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool Connection::Connect() {
|
|
if (d->username_.isEmpty() || d->password_.isEmpty() || d->agent_name_.isEmpty()) {
|
|
qWarning() << __PRETTY_FUNCTION__
|
|
<< ": A required field (username/password/agent_name) was empty";
|
|
return false;
|
|
}
|
|
|
|
// Construct the JID - append the default host if the user didn't provide one
|
|
QString jid = d->username_;
|
|
if (!jid.contains('@')) {
|
|
jid.append("@" + d->jid_host_);
|
|
}
|
|
jid.append("/" + d->jid_resource_);
|
|
|
|
// Create a new connection
|
|
d->client_.reset(new gloox::Client(gloox::JID(jid.toUtf8().constData()),
|
|
d->password_.toUtf8().constData()));
|
|
gloox::ConnectionTCPClient* connection = new gloox::ConnectionTCPClient(
|
|
d->client_.data(), d->client_->logInstance(),
|
|
d->server_.toUtf8().constData());
|
|
d->client_->setConnectionImpl(connection);
|
|
|
|
// Add listeners
|
|
d->client_->registerConnectionListener(d.data());
|
|
d->client_->rosterManager()->registerRosterListener(d.data());
|
|
d->client_->logInstance().registerLogHandler(
|
|
gloox::LogLevelDebug, gloox::LogAreaAll, d.data());
|
|
d->client_->registerMessageHandler(d.data());
|
|
|
|
// Setup disco
|
|
d->client_->disco()->setIdentity(
|
|
"client", "bot", d->agent_name_.toUtf8().constData());
|
|
d->client_->disco()->setVersion(d->agent_name_.toUtf8().constData(), std::string());
|
|
d->client_->disco()->addFeature(kXmlnsXrme);
|
|
|
|
d->media_player_extension_ = new MediaPlayerExtension;
|
|
d->remote_control_extension_ = new RemoteControlExtension;
|
|
d->client_->registerStanzaExtension(d->media_player_extension_);
|
|
d->client_->registerStanzaExtension(d->remote_control_extension_);
|
|
d->client_->registerStanzaExtension(new MediaStorageExtension);
|
|
d->client_->registerStanzaExtension(new SIPNegotiation);
|
|
|
|
d->client_->registerIqHandler(d.data(), SIPNegotiation::kExtensionType);
|
|
|
|
// Initialise the handlers
|
|
foreach (Handler* handler, d->handlers_) {
|
|
handler->Init(this, d->client_.data());
|
|
}
|
|
|
|
// Set presence
|
|
d->client_->setPresence(gloox::Presence::Available, 1);
|
|
|
|
// Tomahawk support
|
|
d->client_->disco()->addFeature("tomahawk:player");
|
|
gloox::Capabilities* caps = new gloox::Capabilities;
|
|
caps->setNode("http://tomahawk-player.org/");
|
|
d->client_->presence().addExtension(caps);
|
|
|
|
//d->client_->setSASLMechanisms(gloox::SaslMechGoogleToken);
|
|
|
|
// Connect
|
|
if (!d->client_->connect(false)) {
|
|
d->client_.reset();
|
|
foreach (Handler* handler, d->handlers_) {
|
|
handler->Reset();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Listen on the connection's socket
|
|
d->socket_notifier_.reset(new QSocketNotifier(
|
|
connection->socket(), QSocketNotifier::Read));
|
|
connect(d->socket_notifier_.data(), SIGNAL(activated(int)),
|
|
SLOT(SocketReadyReceive()));
|
|
|
|
return true;
|
|
}
|
|
|
|
void Connection::Disconnect() {
|
|
if (is_connected()) {
|
|
d->spontaneous_disconnect_ = false;
|
|
d->client_->disconnect();
|
|
d->client_.reset();
|
|
d->spontaneous_disconnect_ = true;
|
|
}
|
|
}
|
|
|
|
void Connection::SocketReadyReceive() {
|
|
d->client_->recv();
|
|
}
|
|
|
|
void Connection::Private::onConnect() {
|
|
parent_->RefreshPeers();
|
|
emit parent_->Connected();
|
|
}
|
|
|
|
void Connection::Private::onDisconnect(gloox::ConnectionError e) {
|
|
QString error_text;
|
|
switch (e) {
|
|
case gloox::ConnNoError:
|
|
case gloox::ConnUserDisconnected:
|
|
break;
|
|
case gloox::ConnStreamError:
|
|
error_text = QString::fromUtf8(client_->streamErrorText().c_str());
|
|
break;
|
|
case gloox::ConnAuthenticationFailed:
|
|
error_text = QString("Authentication error (%1)").arg(client_->authError());
|
|
break;
|
|
default:
|
|
error_text = QString("Unknown error (%1)").arg(e);
|
|
break;
|
|
}
|
|
|
|
foreach (Handler* handler, handlers_) {
|
|
handler->Reset();
|
|
}
|
|
|
|
socket_notifier_->setEnabled(false);
|
|
socket_notifier_.reset();
|
|
peers_.clear();
|
|
querying_peers_.clear();
|
|
|
|
emit parent_->Disconnected(error_text);
|
|
|
|
if (spontaneous_disconnect_) {
|
|
QTimer::singleShot(0, parent_, SLOT(CleanupClient()));
|
|
}
|
|
}
|
|
|
|
void Connection::CleanupClient() {
|
|
d->client_.reset();
|
|
}
|
|
|
|
bool Connection::Private::onTLSConnect(const gloox::CertInfo& info) {
|
|
return true;
|
|
}
|
|
|
|
void Connection::Private::handleLog(gloox::LogLevel level, gloox::LogArea area,
|
|
const std::string& message) {
|
|
if (!verbose_) {
|
|
return;
|
|
}
|
|
|
|
QString prefix = "---";
|
|
if (area == gloox::LogAreaXmlIncoming) {
|
|
prefix = "<<<";
|
|
} else if (area == gloox::LogAreaXmlOutgoing) {
|
|
prefix = ">>>";
|
|
}
|
|
|
|
qDebug() << "XMPP" << prefix.toAscii().constData() << message.c_str();
|
|
}
|
|
|
|
void Connection::RefreshPeers() {
|
|
// Clear the lists
|
|
d->peers_.clear();
|
|
d->querying_peers_.clear();
|
|
|
|
// Query presence
|
|
qDebug() << "Sending presence query";
|
|
gloox::Presence presence(
|
|
gloox::Presence::Available, d->client_->jid().bareJID());
|
|
d->client_->send(presence);
|
|
}
|
|
|
|
Connection::Peer::Peer()
|
|
: caps_(0) {
|
|
}
|
|
|
|
void Connection::Private::handleSelfPresence(
|
|
const gloox::RosterItem& item,
|
|
const std::string& res,
|
|
gloox::Presence::PresenceType presence,
|
|
const std::string&) {
|
|
QString resource = QString::fromUtf8(res.c_str());
|
|
|
|
qDebug() << "Resource presence changed:" << resource << presence;
|
|
|
|
switch (presence) {
|
|
case gloox::Presence::Error:
|
|
case gloox::Presence::Invalid:
|
|
case gloox::Presence::Unavailable:
|
|
// The peer went offline - did we know about him?
|
|
qDebug() << "Peer unavailable" << resource;
|
|
|
|
for (int i=0 ; i<querying_peers_.count() ; ++i) {
|
|
if (querying_peers_[i].jid_resource_ == resource) {
|
|
querying_peers_.takeAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i=0 ; i<peers_.count() ; ++i) {
|
|
if (peers_[i].jid_resource_ == resource) {
|
|
emit parent_->PeerRemoved(peers_.takeAt(i));
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// The peer came online
|
|
if (!has_peer(resource)) {
|
|
qDebug() << "Got presence from" << resource;
|
|
|
|
// This is a peer on our own bare JID, and we haven't seen it before
|
|
gloox::JID full_jid(item.jid());
|
|
full_jid.setResource(res);
|
|
|
|
Peer peer;
|
|
peer.jid_resource_ = resource;
|
|
querying_peers_ << peer;
|
|
|
|
client_->disco()->getDiscoInfo(full_jid, std::string(), this, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Connection::Private::handleRosterPresence(
|
|
const gloox::RosterItem& item, const std::string& res,
|
|
gloox::Presence::PresenceType presence, const std::string& message) {
|
|
// Forward incorrect calls of this with our JID.
|
|
if (item.jid() == client_->jid().bare()) {
|
|
handleSelfPresence(item, res, presence, message);
|
|
}
|
|
// Ignore presence from anyone else
|
|
}
|
|
|
|
bool Connection::Private::has_peer(const QString& jid_resource) const {
|
|
foreach (const Peer& peer, peers_) {
|
|
if (peer.jid_resource_ == jid_resource) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach (const Peer& peer, querying_peers_) {
|
|
if (peer.jid_resource_ == jid_resource) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Connection::Private::handleDiscoInfo(
|
|
const gloox::JID& from, const gloox::Disco::Info& info, int context) {
|
|
// Is this from our own bare JID?
|
|
if (from.bareJID() != client_->jid().bareJID()) {
|
|
return;
|
|
}
|
|
|
|
QString resource = QString::fromUtf8(from.resource().c_str());
|
|
|
|
qDebug() << "Got disco info from" << resource;
|
|
|
|
// Are we currently querying this peer?
|
|
int querying_peer_index = -1;
|
|
for (int i=0 ; i<querying_peers_.count() ; ++i) {
|
|
if (querying_peers_[i].jid_resource_ == resource) {
|
|
querying_peer_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if (querying_peer_index == -1) {
|
|
return;
|
|
}
|
|
|
|
// Remove this peer from the querying list and try to fill in his info.
|
|
Peer peer = querying_peers_.takeAt(querying_peer_index);
|
|
|
|
const gloox::StringList& features = info.features();
|
|
if (features.empty()) {
|
|
return;
|
|
}
|
|
|
|
const gloox::Disco::IdentityList& identities = info.identities();
|
|
if (identities.empty()) {
|
|
return;
|
|
}
|
|
|
|
// Fill in the name.
|
|
peer.agent_name_ = QString::fromUtf8((*identities.begin())->name().c_str());
|
|
|
|
// Fill in the list of capabilities.
|
|
for (gloox::StringList::const_iterator it = features.begin();
|
|
it != features.end(); ++it) {
|
|
const std::string& feature_name = *it;
|
|
if (feature_name == kXmlnsXrmeMediaPlayer) {
|
|
peer.caps_ |= Peer::MediaPlayer;
|
|
}
|
|
if (feature_name == kXmlnsXrmeRemoteControl) {
|
|
peer.caps_ |= Peer::RemoteControl;
|
|
}
|
|
if (feature_name == kXmlnsXrmeMediaStorage) {
|
|
peer.caps_ |= Peer::MediaStorage;
|
|
}
|
|
}
|
|
|
|
// No recognised capabilities? Discard the peer.
|
|
if (peer.caps_ == Peer::None) {
|
|
return;
|
|
}
|
|
|
|
peers_ << peer;
|
|
emit parent_->PeerFound(peer);
|
|
|
|
qDebug() << "Peer found:" << peer.agent_name_ << peer.jid_resource_ << peer.caps_;
|
|
}
|
|
|
|
void Connection::Private::handleDiscoItems(
|
|
const gloox::JID&, const gloox::Disco::Items&, int) {
|
|
|
|
}
|
|
|
|
void Connection::Private::handleDiscoError(
|
|
const gloox::JID& from, const gloox::Error* error, int context) {
|
|
QString resource = QString::fromUtf8(from.resource().c_str());
|
|
|
|
// Remove this peer if we're currently querying it
|
|
for (int i=0 ; i<querying_peers_.count() ; ++i) {
|
|
if (querying_peers_[i].jid_resource_ == resource) {
|
|
querying_peers_.removeAt(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Connection::Private::handleMessage(const gloox::Message& message, gloox::MessageSession* session) {
|
|
qDebug() << Q_FUNC_INFO << message.tag()->xml().c_str();
|
|
const std::string body = message.body();
|
|
qDebug() << body.c_str();
|
|
|
|
QJson::Parser parser;
|
|
bool ok = false;
|
|
QVariant result = parser.parse(QByteArray::fromRawData(body.data(), body.size()), &ok);
|
|
if (ok) {
|
|
qDebug() << result;
|
|
|
|
emit parent_->TomahawkSIPReceived(result);
|
|
}
|
|
}
|
|
|
|
bool Connection::Private::handleIq(const gloox::IQ& iq) {
|
|
gloox::Tag* xrme = iq.tag()->findChild("xrme");
|
|
if (!xrme) {
|
|
return false;
|
|
}
|
|
|
|
gloox::Tag* sip = xrme->findChild("sip");
|
|
const std::string& user_fragment = sip->findAttribute("ufrag");
|
|
const std::string& password = sip->findAttribute("password");
|
|
|
|
gloox::TagList candidates = sip->findChildren("candidate");
|
|
for (gloox::TagList::const_iterator it = candidates.begin();
|
|
it != candidates.end(); ++it) {
|
|
gloox::Tag* candidate = *it;
|
|
const std::string& address = candidate->findAttribute("address");
|
|
const std::string& port = candidate->findAttribute("port");
|
|
const std::string& type = candidate->findAttribute("type");
|
|
const std::string& component = candidate->findAttribute("component");
|
|
const std::string& priority = candidate->findAttribute("priority");
|
|
const std::string& foundation = candidate->findAttribute("foundation");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Connection::Private::handleIqID(const gloox::IQ& iq, int context) {
|
|
|
|
}
|
|
|
|
} // namespace xrme
|