fteqw/plugins/jabber/jingle.c
Spoike 696c7e8260 playdemo should now play older protocol versions.
some more tweaks for xonotic
rcon-over-xmpp, because I can. Server might see your rcon password, so watch out for that.
qcc tweaks.
updated q1qvm api stuff to api version 15.
android port updated. egl now handled by native code, which means we now have proper control over everything and can default to gles2. requires android 2.0+. vulkan-on-android renderer added, but not tested.

git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5153 fc73d0e0-1445-4013-8a0c-d673dee63da5
2017-10-12 12:02:25 +00:00

1373 lines
No EOL
42 KiB
C

#include "xmpp.h"
#ifdef JINGLE
static struct c2c_s *JCL_JingleAddContentToSession(jclient_t *jcl, struct c2c_s *c2c, const char *with, bresource_t *bres, qboolean creator, const char *sid, const char *cname, int method, int mediatype)
{
struct icestate_s *ice = NULL;
char generatedname[64];
char stunhost[256];
int c;
if (!bres)
return NULL;
//make sure we can add more contents to this session
//block dupe content names.
if (c2c)
{
if (c2c->contents == sizeof(c2c->content) / sizeof(c2c->content[0]))
return NULL;
for (c = 0; c < c2c->contents; c++)
if (!strcmp(c2c->content[c].name, cname))
return NULL;
}
if (piceapi)
ice = piceapi->ICE_Create(NULL, sid, with, method, mediatype);
if (ice)
{
piceapi->ICE_Get(ice, "sid", generatedname, sizeof(generatedname));
sid = generatedname;
//the controlling role MUST be assumed by the initiator and the controlled role MUST be assumed by the responder
piceapi->ICE_Set(ice, "controller", creator?"1":"0");
if (creator && mediatype == ICEP_VOICE)
{
//note: the engine will ignore codecs it does not support.
piceapi->ICE_Set(ice, "codec96", "opus@48000");
piceapi->ICE_Set(ice, "codec97", "speex@16000"); //wide
piceapi->ICE_Set(ice, "codec98", "speex@8000"); //narrow
piceapi->ICE_Set(ice, "codec99", "speex@32000"); //ultrawide
piceapi->ICE_Set(ice, "codec0", "pcmu@8000");
piceapi->ICE_Set(ice, "codec8", "pcma@8000");
}
}
else
return NULL; //no way to get the local ip otherwise, which means things won't work proper
if (!c2c)
{
c2c = malloc(sizeof(*c2c) + strlen(sid));
memset(c2c, 0, sizeof(*c2c));
c2c->next = jcl->c2c;
jcl->c2c = c2c;
strcpy(c2c->sid, sid); //safe due to trailing space.
c2c->with = strdup(with);
c2c->peercaps = bres->caps;
c2c->creator = creator;
}
Q_strlcpy(c2c->content[c2c->contents].name, cname, sizeof(c2c->content[c2c->contents].name));
c2c->content[c2c->contents].method = method;
c2c->content[c2c->contents].mediatype = mediatype;
//copy out the interesting parameters
c2c->content[c2c->contents].ice = ice;
c2c->contents++;
//query dns to see if there's a stunserver hosted by the same domain
//nslookup -querytype=SRV _stun._udp.example.com
//for msn, live.com has one, messanger.live.com has one, but messenger.live.com does NOT. seriously, the typo has more services. wtf microsoft?
//google doesn't provide a stun srv entry
//facebook doesn't provide a stun srv entry
Q_snprintf(stunhost, sizeof(stunhost), "_stun._udp.%s", jcl->domain);
if (NET_DNSLookup_SRV(stunhost, stunhost, sizeof(stunhost)))
piceapi->ICE_Set(ice, "stunip", stunhost);
else
{
//there is no real way to query stun servers from the xmpp server.
//while there is some extdisco extension (aka: the 'standard' way), it has some huge big fat do-not-implement message (and googletalk does not implement it).
//google provide their own jingleinfo extension. Which also has some huge fat 'do-not-implement' message. hardcoding the address should provide equivelent longevity. :(
//google also don't provide stun srv records.
//so we're basically screwed if we want to work with the googletalk xmpp service long term.
//more methods are best, I suppose, but I'm lazy.
//yes, hardcoding means that other services might 'borrow' googles' stun servers.
piceapi->ICE_Set(ice, "stunport", "19302");
piceapi->ICE_Set(ice, "stunip", "stun.l.google.com");
}
return c2c;
}
static qboolean JCL_JingleAcceptAck(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
{
int i;
struct c2c_s *c2c;
if (tree)
{
for (c2c = jcl->c2c; c2c; c2c = c2c->next)
{
if (c2c == iq->usrptr)
{
for (i = 0; i < c2c->contents; i++)
{
if (c2c->content[i].ice)
piceapi->ICE_Set(c2c->content[i].ice, "state", STRINGIFY(ICE_CONNECTING));
}
}
}
}
return true;
}
static void JCL_PopulateAudioDescription(xmltree_t *description, struct icestate_s *ice)
{
xmltree_t *payload;
int i;
int pcma = -1, pcmu = -1;
for (i = 0; i <= 127; i++)
{
char codecname[64];
char argn[64];
Q_snprintf(argn, sizeof(argn), "codec%i", i);
if (piceapi->ICE_Get(ice, argn, codecname, sizeof(codecname)))
{
if (!strcasecmp(codecname, "speex@8000") || !strcasecmp(codecname, "speex@16000") || !strcasecmp(codecname, "speex@32000"))
{ //speex narrowband
payload = XML_CreateNode(description, "payload-type", "", "");
XML_AddParameter(payload, "channels", "1");
XML_AddParameter(payload, "clockrate", codecname+6);
XML_AddParameter(payload, "id", argn+5);
XML_AddParameter(payload, "name", "speex");
}
else if (!strcasecmp(codecname, "opus") || !strcasecmp(codecname, "opus@48000"))
{ //opus codec. implicitly at 48khz
payload = XML_CreateNode(description, "payload-type", "", "");
XML_AddParameter(payload, "channels", "1");
XML_AddParameter(payload, "id", argn+5);
XML_AddParameter(payload, "name", "opus");
}
else if (!strcasecmp(codecname, "pcma@8000") || !strcasecmp(codecname, "pcmu@8000"))
{ //pcma/pcmu.
//these get flagged to ensure they appear last, because they're not very good, esp compared to opus,
// however they are simple and more widely distributed on traditional voice services,
// so they're an important fallback
if (!strcasecmp(codecname, "pcma@8000") && pcma < 0)
pcma = i;
else if (!strcasecmp(codecname, "pcmu@8000") && pcmu < 0)
pcmu = i;
else
{
payload = XML_CreateNode(description, "payload-type", "", "");
XML_AddParameter(payload, "channels", "1");
XML_AddParameter(payload, "clockrate", codecname+5);
XML_AddParameter(payload, "id", argn+5);
codecname[4] = 0;
XML_AddParameter(payload, "name", codecname);
}
}
}
}
if (pcma>=0)
{
payload = XML_CreateNode(description, "payload-type", "", "");
XML_AddParameter(payload, "channels", "1");
XML_AddParameter(payload, "clockrate", "8000");
XML_AddParameteri(payload, "id", pcma);
XML_AddParameter(payload, "name", "pcma");
}
if (pcmu>=0)
{
payload = XML_CreateNode(description, "payload-type", "", "");
XML_AddParameter(payload, "channels", "1");
XML_AddParameter(payload, "clockrate", "8000");
XML_AddParameteri(payload, "id", pcmu);
XML_AddParameter(payload, "name", "pcmu");
}
}
enum
{
JE_ACKNOWLEDGE,
JE_UNSUPPORTED,
JE_OUTOFORDER,
JE_TIEBREAK,
JE_UNKNOWNSESSION,
JE_UNSUPPORTEDINFO
};
static void JCL_JingleError(jclient_t *jcl, xmltree_t *tree, const char *from, const char *id, int type)
{
switch(type)
{
case JE_ACKNOWLEDGE:
JCL_AddClientMessagef(jcl,
"<iq type='result' to='%s' id='%s' />", from, id);
break;
case JE_UNSUPPORTED:
JCL_AddClientMessagef(jcl,
"<iq id='%s' to='%s' type='error'>"
"<error type='cancel'>"
"<bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"</error>"
"</iq>", id, from);
break;
case JE_UNKNOWNSESSION:
JCL_AddClientMessagef(jcl,
"<iq id='%s' to='%s' type='error'>"
"<error type='modify'>"
"<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"<unknown-session xmlns='urn:xmpp:jingle:errors:1'/>"
"</error>"
"</iq>", id, from);
break;
case JE_UNSUPPORTEDINFO:
JCL_AddClientMessagef(jcl,
"<iq id='%s' to='%s' type='error'>"
"<error type='modify'>"
"<feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"<unsupported-info xmlns='urn:xmpp:jingle:errors:1'/>"
"</error>"
"</iq>", id, from);
break;
}
}
/*
sends a jingle message to the peer.
action should be one of multiple things:
session-terminate - totally not acceptable. this also closes the c2c
session-accept - details are okay. this also begins ice polling (on iq ack, once we're sure the peer got our message)
(internally generated) transport-replace - details are okay, except we want a different transport method.
*/
static qboolean JCL_JingleSend(jclient_t *jcl, struct c2c_s *c2c, char *action)
{
qboolean result;
xmltree_t *jingle;
int c;
// struct icestate_s *ice = c2c->ice;
qboolean wasaccept = false;
int transportmode = ICEM_ICE;
#ifdef VOIP_LEGACY
int cap = c2c->peercaps;
#endif
if (!c2c->contents)
action = "session-terminate";
#ifdef VOIP_LEGACY
if ((cap & CAP_GOOGLE_VOICE) && !(cap & CAP_VOICE))
{
//legacy crap for google compatibility.
if (!strcmp(action, "session-initiate"))
{
xmltree_t *session = XML_CreateNode(NULL, "session", "http://www.google.com/session", "");
xmltree_t *description = XML_CreateNode(session, "description", "http://www.google.com/session/phone", "");
XML_AddParameter(session, "id", c2c->sid);
XML_AddParameter(session, "initiator", jcl->jid);
XML_AddParameter(session, "type", "initiate");
for (c = 0; c < c2c->contents; c++)
JCL_PopulateAudioDescription(description, c2c->content[c].ice);
JCL_SendIQNode(jcl, NULL, "set", c2c->with, session, true);
}
else if (!strcmp(action, "session-accept"))
{
xmltree_t *session = XML_CreateNode(NULL, "session", "http://www.google.com/session", "");
xmltree_t *description = XML_CreateNode(session, "description", "http://www.google.com/session/phone", "");
XML_AddParameter(session, "id", c2c->sid);
XML_AddParameter(session, "initiator", c2c->with);
XML_AddParameter(session, "type", "accept");
for (c = 0; c < c2c->contents; c++)
JCL_PopulateAudioDescription(description, c2c->content[c].ice);
JCL_SendIQNode(jcl, JCL_JingleAcceptAck, "set", c2c->with, session, true)->usrptr = c2c;
c2c->accepted = true;
}
else if (!strcmp(action, "transport-info"))
{
/*FIXME
struct icecandinfo_s *ca;
while ((ca = piceapi->ICE_GetLCandidateInfo(ice)))
{
xmltree_t *session = XML_CreateNode(NULL, "session", "http://www.google.com/session", "");
XML_AddParameter(session, "id", c2c->sid);
XML_AddParameter(session, "initiator", c2c->creator?jcl->jid:c2c->with);
XML_AddParameter(session, "type", "candidates");
//one per message, apparently
if (ca)
{
char *ctypename[]={"local", "stun", "stun", "relay"};
char *pref[]={"1", "0.9", "0.5", "0.2"};
xmltree_t *candidate = XML_CreateNode(session, "candidate", "", "");
char pwd[64];
char uname[64];
piceapi->ICE_Get(ice, "lufrag", uname, sizeof(uname));
piceapi->ICE_Get(ice, "lpwd", pwd, sizeof(pwd));
XML_AddParameter(candidate, "address", ca->addr);
XML_AddParameteri(candidate, "port", ca->port);
XML_AddParameter(candidate, "name", (ca->component==1)?"rtp":"rtcp");
XML_AddParameter(candidate, "username", uname);
XML_AddParameter(candidate, "password", pwd);
XML_AddParameter(candidate, "preference", pref[ca->type]); //FIXME: c->priority
XML_AddParameter(candidate, "protocol", "udp");
XML_AddParameter(candidate, "type", ctypename[ca->type]);
XML_AddParameteri(candidate, "generation", ca->generation);
XML_AddParameteri(candidate, "network", ca->network);
}
JCL_SendIQNode(jcl, NULL, "set", c2c->with, session, true);
}
*/Con_Printf("No idea how to write candidates for gingle\n");
}
else if (!strcmp(action, "session-terminate"))
{
struct c2c_s **link;
xmltree_t *session = XML_CreateNode(NULL, "session", "http://www.google.com/session", "");
XML_AddParameter(session, "id", c2c->sid);
XML_AddParameter(session, "initiator", c2c->creator?jcl->jid:c2c->with);
XML_AddParameter(session, "type", "terminate");
JCL_SendIQNode(jcl, NULL, "set", c2c->with, session, true);
for (link = &jcl->c2c; *link; link = &(*link)->next)
{
if (*link == c2c)
{
*link = c2c->next;
break;
}
}
for (c = 0; c < c2c->contents; c++)
{
if (c2c->content[c].ice)
piceapi->ICE_Close(c2c->content[c].ice);
c2c->content[c].ice = NULL;
}
return false;
}
else
{
return false;
}
return true;
}
#endif
jingle = XML_CreateNode(NULL, "jingle", "urn:xmpp:jingle:1", "");
XML_AddParameter(jingle, "sid", c2c->sid);
if (!strcmp(action, "session-initiate"))
{ //these attributes are meant to only be present in initiate. for call forwarding etc. which we don't properly support.
XML_AddParameter(jingle, "initiator", jcl->fulljid);
}
if (!strcmp(action, "session-terminate"))
{
struct c2c_s **link;
for (link = &jcl->c2c; *link; link = &(*link)->next)
{
if (*link == c2c)
{
*link = c2c->next;
break;
}
}
for (c = 0; c < c2c->contents; c++)
{
if (c2c->content[c].ice)
piceapi->ICE_Close(c2c->content[c].ice);
c2c->content[c].ice = NULL;
}
result = false;
}
else
{
result = true;
for (c = 0; c < c2c->contents; c++)
{
xmltree_t *content = XML_CreateNode(jingle, "content", "", "");
struct icestate_s *ice = c2c->content[c].ice;
XML_AddParameter(content, "name", c2c->content[c].name);
if (!strcmp(action, "session-accept"))
{
if (c2c->content[c].method == transportmode)
XML_AddParameter(jingle, "responder", jcl->fulljid);
else
action = "transport-replace";
}
{
xmltree_t *description;
xmltree_t *transport;
if (transportmode == ICEM_RAW)
{
transport = XML_CreateNode(content, "transport", "urn:xmpp:jingle:transports:raw-udp:1", "");
{
xmltree_t *candidate;
struct icecandinfo_s *b = NULL;
struct icecandinfo_s *c;
while ((c = piceapi->ICE_GetLCandidateInfo(ice)))
{
if (!b || b->priority < c->priority)
b = c;
}
if (b)
{
candidate = XML_CreateNode(transport, "candidate", "", "");
XML_AddParameter(candidate, "ip", b->addr);
XML_AddParameteri(candidate, "port", b->port);
XML_AddParameter(candidate, "id", b->candidateid);
XML_AddParameteri(candidate, "generation", b->generation);
XML_AddParameteri(candidate, "component", b->component);
}
}
}
else if (transportmode == ICEM_ICE)
{
char val[64];
transport = XML_CreateNode(content, "transport", "urn:xmpp:jingle:transports:ice-udp:1", "");
piceapi->ICE_Get(ice, "lufrag", val, sizeof(val));
XML_AddParameter(transport, "ufrag", val);
piceapi->ICE_Get(ice, "lpwd", val, sizeof(val));
XML_AddParameter(transport, "pwd", val);
{
struct icecandinfo_s *c;
while ((c = piceapi->ICE_GetLCandidateInfo(ice)))
{
char *ctypename[]={"host", "srflx", "prflx", "relay"};
xmltree_t *candidate = XML_CreateNode(transport, "candidate", "", "");
XML_AddParameter(candidate, "type", ctypename[c->type]);
XML_AddParameter(candidate, "protocol", "udp"); //is this not just a little bit redundant? ice-udp? seriously?
XML_AddParameteri(candidate, "priority", c->priority);
XML_AddParameteri(candidate, "port", c->port);
XML_AddParameteri(candidate, "network", c->network);
XML_AddParameter(candidate, "ip", c->addr);
XML_AddParameter(candidate, "id", c->candidateid);
XML_AddParameteri(candidate, "generation", c->generation);
XML_AddParameteri(candidate, "foundation", c->foundation);
XML_AddParameteri(candidate, "component", c->component);
}
}
}
if (strcmp(action, "transport-info"))
{
#ifdef VOIP
if (c2c->content[c].mediatype == ICEP_VOICE)
{
XML_AddParameter(content, "senders", "both");
XML_AddParameter(content, "creator", "initiator");
description = XML_CreateNode(content, "description", "urn:xmpp:jingle:apps:rtp:1", "");
XML_AddParameter(description, "media", MEDIATYPE_AUDIO);
JCL_PopulateAudioDescription(description, ice);
}
else if (c2c->content[c].mediatype == ICEP_VIDEO)
{
XML_AddParameter(content, "senders", "both");
XML_AddParameter(content, "creator", "initiator");
description = XML_CreateNode(content, "description", "urn:xmpp:jingle:apps:rtp:1", "");
XML_AddParameter(description, "media", MEDIATYPE_VIDEO);
//JCL_PopulateAudioDescription(description, ice);
}
#endif
else
{
description = XML_CreateNode(content, "description", QUAKEMEDIAXMLNS, "");
XML_AddParameter(description, "media", MEDIATYPE_QUAKE);
if (c2c->content[c].mediatype == ICEP_QWSERVER)
XML_AddParameter(description, "host", "me");
else if (c2c->content[c].mediatype == ICEP_QWCLIENT)
XML_AddParameter(description, "host", "you");
}
}
}
}
if (!strcmp(action, "session-accept"))
c2c->accepted = wasaccept = true;
}
XML_AddParameter(jingle, "action", action);
// Con_Printf("Sending Jingle:\n");
// XML_ConPrintTree(jingle, 1);
JCL_SendIQNode(jcl, wasaccept?JCL_JingleAcceptAck:NULL, "set", c2c->with, jingle, true)->usrptr = c2c;
return result;
}
void JCL_JingleTimeouts(jclient_t *jcl, qboolean killall)
{
int c;
struct c2c_s *c2c;
for (c2c = jcl->c2c; c2c; c2c = c2c->next)
{
for (c = 0; c < c2c->contents; c++)
{
if (c2c->content[c].method == ICEM_ICE)
{
char bah[2];
piceapi->ICE_Get(c2c->content[c].ice, "newlc", bah, sizeof(bah));
if (atoi(bah))
{
Con_DPrintf("Sending updated local addresses\n");
JCL_JingleSend(jcl, c2c, "transport-info");
break;
}
}
}
}
}
void JCL_Join(jclient_t *jcl, const char *target, const char *sid, qboolean allow, int protocol)
{
struct c2c_s *c2c = NULL, **link;
char autotarget[256];
buddy_t *b;
bresource_t *br;
int c;
if (!jcl)
return;
if (!JCL_FindBuddy(jcl, target, &b, &br, true))
{
Con_Printf("user/resource not known\n");
return;
}
if (!br)
br = b->defaultresource;
if (!br)
br = b->resources;
if (!strchr(target, '/'))
{
if (!br)
{
Con_Printf("User name not valid\n");
return;
}
Q_snprintf(autotarget, sizeof(autotarget), "%s/%s", b->accountdomain, br->resource);
target = autotarget;
}
for (link = &jcl->c2c; *link && !c2c; link = &(*link)->next)
{
if (!strcmp((*link)->with, target) && (!sid || !strcmp((*link)->sid, sid)))
{
if (protocol == ICEP_INVALID)
c2c = *link;
else
{
for (c = 0; c < (*link)->contents; c++)
if ((*link)->content[c].mediatype == protocol)
c2c = *link;
}
}
}
if (allow)
{
if (!c2c)
{
if (!sid)
{
char convolink[512], hanguplink[512];
if (protocol == ICEP_INVALID)
protocol = ICEP_QWCLIENT;
c2c = JCL_JingleAddContentToSession(jcl, NULL, target, br, true, sid, "foobar2000", DEFAULTICEMODE, protocol);
JCL_JingleSend(jcl, c2c, "session-initiate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
JCL_GenLink(jcl, hanguplink, sizeof(hanguplink), "jdeny", target, NULL, c2c->sid, "%s", "Hang Up");
XMPP_ConversationPrintf(b->accountdomain, b->name, "%s %s %s.\n", protocol==ICEP_VOICE?"Calling":"Requesting session with", convolink, hanguplink);
}
else
XMPP_ConversationPrintf(b->accountdomain, b->name, "That session has expired.\n");
}
else if (c2c->creator)
{
char convolink[512];
//resend initiate if they've not acked it... I dunno...
JCL_JingleSend(jcl, c2c, "session-initiate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
XMPP_ConversationPrintf(b->accountdomain, b->name, "Restarting session with %s.\n", convolink);
}
else if (c2c->accepted)
XMPP_ConversationPrintf(b->accountdomain, b->name, "That session was already accepted.\n");
else
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "session-accept");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
XMPP_ConversationPrintf(b->accountdomain, b->name, "Accepting session from %s.\n", convolink);
}
}
else
{
if (c2c)
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "session-terminate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
XMPP_ConversationPrintf(b->accountdomain, b->name, "Terminating session with %s.\n", convolink);
}
else
XMPP_ConversationPrintf(b->accountdomain, b->name, "That session has already expired.\n");
}
}
static void JCL_JingleParsePeerPorts(jclient_t *jcl, struct c2c_s *c2c, xmltree_t *inj, const char *from, const char *sid)
{
xmltree_t *incontent;
xmltree_t *intransport;
xmltree_t *incandidate;
struct icecandinfo_s rem;
struct icestate_s *ice;
int i, contid;
const char *cname;
if (!c2c->sid)
return;
if (strcmp(c2c->with, from) || strcmp(c2c->sid, sid))
{
Con_Printf("%s is trying to mess with our connections...\n", from);
return;
}
//a message can contain multiple contents
for (contid = 0; ; contid++)
{
incontent = XML_ChildOfTree(inj, "content", contid);
if (!incontent)
break;
cname = XML_GetParameter(incontent, "name", "");
//find which content this node refers to.
ice = NULL;
for (i = 0; i < c2c->contents; i++)
if (!strcmp(c2c->content[i].name, cname))
ice = c2c->content[i].ice;
if (!ice)
{
//err... this content doesn't exist?
continue;
}
intransport = XML_ChildOfTree(incontent, "transport", 0);
if (!intransport)
continue; //err, I guess it wasn't a transport update then (or related).
piceapi->ICE_Set(ice, "rufrag", XML_GetParameter(intransport, "ufrag", ""));
piceapi->ICE_Set(ice, "rpwd", XML_GetParameter(intransport, "pwd", ""));
for (i = 0; (incandidate = XML_ChildOfTree(intransport, "candidate", i)); i++)
{
const char *s;
memset(&rem, 0, sizeof(rem));
Q_strlcpy(rem.addr, XML_GetParameter(incandidate, "ip", ""), sizeof(rem.addr));
Q_strlcpy(rem.candidateid, XML_GetParameter(incandidate, "id", ""), sizeof(rem.candidateid));
s = XML_GetParameter(incandidate, "type", "");
if (s && !strcmp(s, "srflx"))
rem.type = ICE_SRFLX;
else if (s && !strcmp(s, "prflx"))
rem.type = ICE_PRFLX;
else if (s && !strcmp(s, "relay"))
rem.type = ICE_RELAY;
else
rem.type = ICE_HOST;
rem.port = atoi(XML_GetParameter(incandidate, "port", "0"));
rem.priority = atoi(XML_GetParameter(incandidate, "priority", "0"));
rem.network = atoi(XML_GetParameter(incandidate, "network", "0"));
rem.generation = atoi(XML_GetParameter(incandidate, "generation", "0"));
rem.foundation = atoi(XML_GetParameter(incandidate, "foundation", "0"));
rem.component = atoi(XML_GetParameter(incandidate, "component", "0"));
s = XML_GetParameter(incandidate, "protocol", "udp");
if (s && !strcmp(s, "udp"))
rem.transport = 0;
else
rem.transport = 0;
piceapi->ICE_AddRCandidateInfo(ice, &rem);
}
}
}
#ifdef VOIP_LEGACY
static void JCL_JingleParsePeerPorts_GoogleSession(jclient_t *jcl, struct c2c_s *c2c, xmltree_t *inj, char *from, char *sid)
{
xmltree_t *incandidate;
struct icecandinfo_s rem;
int i;
int c;
if (strcmp(c2c->with, from) || strcmp(c2c->sid, sid))
{
Con_Printf("%s is trying to mess with our connections...\n", from);
return;
}
if (!c2c->sid)
return;
//with google's session api, every session uses a single set of ports.
for (i = 0; (incandidate = XML_ChildOfTree(inj, "candidate", i)); i++)
{
char *s;
memset(&rem, 0, sizeof(rem));
Q_strlcpy(rem.addr, XML_GetParameter(incandidate, "address", ""), sizeof(rem.addr));
// Q_strlcpy(rem.candidateid, XML_GetParameter(incandidate, "id", ""), sizeof(rem.candidateid));
s = XML_GetParameter(incandidate, "type", "");
if (s && !strcmp(s, "stun"))
rem.type = ICE_SRFLX;
else if (s && !strcmp(s, "stun"))
rem.type = ICE_PRFLX;
else if (s && !strcmp(s, "relay"))
rem.type = ICE_RELAY;
else
rem.type = ICE_HOST;
rem.port = atoi(XML_GetParameter(incandidate, "port", "0"));
rem.priority = atoi(XML_GetParameter(incandidate, "priority", "0"));
rem.network = atoi(XML_GetParameter(incandidate, "network", "0"));
rem.generation = atoi(XML_GetParameter(incandidate, "generation", "0"));
rem.foundation = atoi(XML_GetParameter(incandidate, "foundation", "0"));
s = XML_GetParameter(incandidate, "name", "rtp");
if (!strcmp(s, "rtp"))
rem.component = 1;
else if (!strcmp(s, "rtcp"))
rem.component = 2;
else
rem.component = 0;
s = XML_GetParameter(incandidate, "protocol", "udp");
if (s && !strcmp(s, "udp"))
rem.transport = 0;
else
rem.transport = 0;
for (c = 0; c < c2c->contents; c++)
if (c2c->content[c].ice)
piceapi->ICE_AddRCandidateInfo(c2c->content[c].ice, &rem);
}
}
static qboolean JCL_JingleHandleInitiate_GoogleSession(jclient_t *jcl, xmltree_t *inj, char *from)
{
xmltree_t *indescription = XML_ChildOfTree(inj, "description", 0);
char *descriptionxmlns = indescription?indescription->xmlns:"";
char *sid = XML_GetParameter(inj, "id", "");
qboolean accepted = false;
char *response = "terminate";
char *offer = "pwn you";
char *autocvar = "xmpp_autoaccepthax";
char *initiator;
struct c2c_s *c2c = NULL;
int mt = ICEP_INVALID;
buddy_t *b;
bresource_t *br;
//FIXME: add support for session forwarding so that we might forward the connection to the real server. for now we just reject it.
initiator = XML_GetParameter(inj, "initiator", "");
if (strcmp(initiator, from))
return false;
if (indescription && !strcmp(descriptionxmlns, "http://www.google.com/session/phone"))
{
mt = ICEP_VOICE;
offer = "is trying to call you";
autocvar = "xmpp_autoacceptvoice";
}
if (mt == ICEP_INVALID)
return false;
if (!JCL_FindBuddy(jcl, from, &b, &br, true))
return false;
//FIXME: if both people try to establish a connection to the other simultaneously, the higher session id is meant to be canceled, and the lower accepted automagically.
c2c = JCL_JingleAddContentToSession(jcl, NULL, from, br, false,
sid, "the mystical magical content name",
ICEM_ICE,
mt
);
if (c2c)
{
int c = c2c->contents-1;
c2c->peercaps = CAP_GOOGLE_VOICE;
if (mt == ICEP_VOICE)
{
qboolean okay = false;
int i = 0;
xmltree_t *payload;
//chuck it at the engine and see what sticks. at least one must...
while((payload = XML_ChildOfTree(indescription, "payload-type", i++)))
{
char *name = XML_GetParameter(payload, "name", "");
char *clock = XML_GetParameter(payload, "clockrate", "");
char *id = XML_GetParameter(payload, "id", "");
char parm[64];
char val[64];
//note: the engine will ignore codecs it does not support, returning false.
if (!strcasecmp(name, "speex"))
{
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
Q_snprintf(val, sizeof(val), "speex@%i", atoi(clock));
okay |= piceapi->ICE_Set(c2c->content[c].ice, parm, val);
}
else if (!strcasecmp(name, "pcma") || !strcasecmp(name, "pcmu"))
{
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
Q_snprintf(val, sizeof(val), "%s@%i", name, atoi(clock));
okay |= piceapi->ICE_Set(c2c->content[c].ice, parm, val);
}
else if (!strcasecmp(name, "opus"))
{
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
okay |= piceapi->ICE_Set(c2c->content[c].ice, parm, "opus@48000");
}
}
//don't do it if we couldn't successfully set any codecs, because the engine doesn't support the ones that were listed, or something.
//we really ought to give a reason, but we're rude.
if (!okay)
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "terminate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
XMPP_ConversationPrintf(b->accountdomain, b->name, "%s does not support any compatible audio codecs, and is unable to call you.\n", convolink);
return false;
}
}
if (mt != ICEP_INVALID)
{
char convolink[512];
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
if (!pCvar_GetFloat(autocvar))
{
char authlink[512];
char denylink[512];
JCL_GenLink(jcl, authlink, sizeof(authlink), "jauth", from, NULL, sid, "%s", "Accept");
JCL_GenLink(jcl, denylink, sizeof(denylink), "jdeny", from, NULL, sid, "%s", "Reject");
//show a prompt for it, send the reply when the user decides.
XMPP_ConversationPrintf(b->accountdomain, b->name,
"%s %s. %s %s\n", convolink, offer, authlink, denylink);
pCon_SetActive(b->name);
return true;
}
else
{
XMPP_ConversationPrintf(b->accountdomain, b->name, "Auto-accepting session from %s\n", convolink);
response = "accept";
}
}
}
JCL_JingleSend(jcl, c2c, response);
return true;
}
#endif
static struct c2c_s *JCL_JingleHandleInitiate(jclient_t *jcl, xmltree_t *inj, const char *from)
{
const char *sid = XML_GetParameter(inj, "sid", "");
qboolean okay;
const char *initiator;
struct c2c_s *c2c = NULL;
int mt = ICEP_INVALID;
int i, c;
buddy_t *b;
bresource_t *br;
//FIXME: add support for session forwarding so that we might forward the connection to the real server. for now we just reject it.
initiator = XML_GetParameter(inj, "initiator", "");
if (strcmp(initiator, from))
return NULL;
//reject it if we don't know them.
if (!JCL_FindBuddy(jcl, from, &b, &br, false))
return NULL;
for (i = 0; ; i++)
{
xmltree_t *incontent = XML_ChildOfTree(inj, "content", i);
const char *cname = XML_GetParameter(incontent, "name", "");
xmltree_t *intransport = XML_ChildOfTree(incontent, "transport", 0);
xmltree_t *indescription = XML_ChildOfTree(incontent, "description", 0);
const char *transportxmlns = intransport?intransport->xmlns:"";
const char *descriptionxmlns = indescription?indescription->xmlns:"";
const char *descriptionmedia = XML_GetParameter(indescription, "media", "");
if (!incontent)
break;
mt = ICEP_INVALID;
if (incontent && !strcmp(descriptionmedia, MEDIATYPE_QUAKE) && !strcmp(descriptionxmlns, QUAKEMEDIAXMLNS))
{
const char *host = XML_GetParameter(indescription, "host", "you");
if (!strcmp(host, "you"))
mt = ICEP_QWSERVER;
else if (!strcmp(host, "me"))
mt = ICEP_QWCLIENT;
}
if (incontent && !strcmp(descriptionmedia, MEDIATYPE_AUDIO) && !strcmp(descriptionxmlns, "urn:xmpp:jingle:apps:rtp:1"))
mt = ICEP_VOICE;
if (incontent && !strcmp(descriptionmedia, MEDIATYPE_VIDEO) && !strcmp(descriptionxmlns, "urn:xmpp:jingle:apps:rtp:1"))
mt = ICEP_VIDEO;
if (mt == ICEP_INVALID)
continue;
c2c = JCL_JingleAddContentToSession(jcl, NULL, from, br, false, sid, cname,
strcmp(transportxmlns, "urn:xmpp:jingle:transports:raw-udp:1")?ICEM_ICE:ICEM_RAW,
mt
);
if (!c2c)
continue;
c = c2c->contents-1;
okay = false;
if (mt == ICEP_VOICE)
{
int i = 0;
xmltree_t *payload;
//chuck it at the engine and see what sticks. at least one must...
while((payload = XML_ChildOfTree(indescription, "payload-type", i++)))
{
const char *name = XML_GetParameter(payload, "name", "");
const char *clock = XML_GetParameter(payload, "clockrate", "");
const char *id = XML_GetParameter(payload, "id", "");
char parm[64];
char val[64];
//note: the engine will ignore codecs it does not support, returning false.
if (!strcasecmp(name, "speex") || !strcasecmp(name, "pcma") || !strcasecmp(name, "pcmu"))
{
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
Q_snprintf(val, sizeof(val), "%s@%i", name, atoi(clock));
okay |= piceapi->ICE_Set(c2c->content[c].ice, parm, val);
}
else if (!strcasecmp(name, "opus"))
{
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
okay |= piceapi->ICE_Set(c2c->content[c].ice, parm, "opus@48000");
}
}
}
else
okay = true;
//don't do it if we couldn't successfully set any codecs, because the engine doesn't support the ones that were listed, or something.
//we really ought to give a reason, but we're rude.
if (!okay)
{
char convolink[512];
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
XMPP_ConversationPrintf(b->accountdomain, b->name, "%s does not support any compatible codecs, and is unable to call you.\n", convolink);
if (c2c->content[c].ice)
piceapi->ICE_Close(c2c->content[c].ice);
c2c->content[c].ice = NULL;
c2c->contents--;
}
}
if (!c2c)
return NULL;
if (!c2c->contents)
{
if (jcl->c2c == c2c)
{
jcl->c2c = c2c->next;
free(c2c);
}
else
Con_Printf("^1error in "__FILE__" %i\n", __LINE__);
return NULL;
}
//if they speak this, we never want to speak gingle at them!
c2c->peercaps &= ~(CAP_GOOGLE_VOICE);
JCL_JingleParsePeerPorts(jcl, c2c, inj, from, sid);
return c2c;
}
static qboolean JCL_JingleHandleSessionTerminate(jclient_t *jcl, xmltree_t *tree, struct c2c_s *c2c, struct c2c_s **link, buddy_t *b)
{
xmltree_t *reason = XML_ChildOfTree(tree, "reason", 0);
int c;
if (!c2c)
{
XMPP_ConversationPrintf(b->accountdomain, b->name, "Received session-terminate without an active session\n");
return false;
}
if (reason && reason->child)
XMPP_ConversationPrintf(b->accountdomain, b->name, "Session ended: %s\n", reason->child->name);
else
XMPP_ConversationPrintf(b->accountdomain, b->name, "Session ended\n");
//unlink it
for (link = &jcl->c2c; *link; link = &(*link)->next)
{
if (*link == c2c)
{
*link = c2c->next;
break;
}
}
// XML_ConPrintTree(tree, 0);
for (c = 0; c < c2c->contents; c++)
if (c2c->content[c].ice)
piceapi->ICE_Close(c2c->content[c].ice);
free(c2c);
return true;
}
static qboolean JCL_JingleHandleSessionAccept(jclient_t *jcl, xmltree_t *tree, const char *from, struct c2c_s *c2c, buddy_t *b)
{
//peer accepted our session
//make sure it actually was ours, and not theirs. sneaky sneaky.
//will generally contain some port info.
if (!c2c)
{
Con_DPrintf("Unknown session acceptance\n");
return false;
}
else if (!c2c->creator)
{
Con_DPrintf("Peer tried to accept a session that *they* created!\n");
return false;
}
else if (c2c->accepted)
{
//pidgin is buggy and can dupe-accept sessions multiple times.
Con_DPrintf("Duplicate session-accept from peer.\n");
//XML_ConPrintTree(tree, 0);
return false;
}
else
{
const char *responder = XML_GetParameter(tree, "responder", from);
int c;
if (strcmp(responder, from))
{
return false;
}
XMPP_ConversationPrintf(b->accountdomain, b->name, "Session Accepted!\n");
// XML_ConPrintTree(tree, 0);
JCL_JingleParsePeerPorts(jcl, c2c, tree, from, XML_GetParameter(tree, "sid", ""));
c2c->accepted = true;
//if we didn't error out, the ICE stuff is meant to start sending handshakes/media as soon as the connection is accepted
for (c = 0; c < c2c->contents; c++)
if (c2c->content[c].ice)
piceapi->ICE_Set(c2c->content[c].ice, "state", STRINGIFY(ICE_CONNECTING));
}
return true;
}
#ifdef VOIP_LEGACY
qboolean JCL_HandleGoogleSession(jclient_t *jcl, xmltree_t *tree, char *from, char *iqid)
{
char *sid = XML_GetParameter(tree, "id", "");
char *type = XML_GetParameter(tree, "type", "");
struct c2c_s *c2c = NULL, **link;
buddy_t *b;
//validate+find sender
for (link = &jcl->c2c; *link; link = &(*link)->next)
{
if (!strcmp((*link)->sid, sid))
{
c2c = *link;
if (!c2c->accepted)
break;
}
}
if (!JCL_FindBuddy(jcl, from, &b, NULL, true))
return false;
if (c2c && strcmp(c2c->with, from))
{
Con_Printf("%s is trying to mess with our connections...\n", from);
return false;
}
Con_Printf("google session with %s:\n", from);
XML_ConPrintTree(tree, "", 0);
if (!strcmp(type, "accept"))
{
if (!JCL_JingleHandleSessionAccept(jcl, tree, from, c2c, b))
return false;
}
else if (!strcmp(type, "initiate"))
{
if (!JCL_JingleHandleInitiate_GoogleSession(jcl, tree, from))
return false;
}
else if (!strcmp(type, "terminate"))
{
JCL_JingleHandleSessionTerminate(jcl, tree, c2c, link, b);
}
else if (!strcmp(type, "candidates"))
{
if (!c2c)
return false;
JCL_JingleParsePeerPorts_GoogleSession(jcl, c2c, tree, from, sid);
}
else
{
Con_Printf("Unknown google session action: %s\n", type);
// XML_ConPrintTree(tree, 0);
return false;
}
JCL_AddClientMessagef(jcl,
"<iq type='result' to='%s' id='%s' />", from, iqid);
return true;
}
#endif
qboolean JCL_ParseJingle(jclient_t *jcl, xmltree_t *tree, const char *from, const char *id)
{
const char *action = XML_GetParameter(tree, "action", "");
const char *sid = XML_GetParameter(tree, "sid", "");
struct c2c_s *c2c = NULL, **link;
buddy_t *b;
for (link = &jcl->c2c; *link; link = &(*link)->next)
{
if (!strcmp((*link)->sid, sid))
{
c2c = *link;
if (!c2c->accepted)
break;
}
}
if (!JCL_FindBuddy(jcl, from, &b, NULL, true))
return false;
//validate sender
if (c2c && strcmp(c2c->with, from))
{
Con_Printf("%s is trying to mess with our connections...\n", from);
return false;
}
//FIXME: transport-info, transport-replace
if (!strcmp(action, "session-terminate"))
{
if (c2c)
{
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
JCL_JingleHandleSessionTerminate(jcl, tree, c2c, link, b);
}
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "content-accept"))
{
//response from content-add
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "content-add"))
{
//FIXME: must send content-reject
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "content-modify"))
{
//send an error to reject it
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
}
else if (!strcmp(action, "content-reject"))
{
//response from content-add. an error until we actually generate content-adds...
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "content-remove"))
{
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "description-info"))
{
//The description-info action is used to send informational hints about parameters related to the application type, such as the suggested height and width of a video display area or suggested configuration for an audio stream.
//just ack and ignore it
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "security-info"))
{
//The security-info action is used to send information related to establishment or maintenance of security preconditions.
//no security mechanisms supported...
if (c2c)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "session-info"))
{
if (tree->child)
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTEDINFO);
else
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE); //serves as a ping.
}
else if (!strcmp(action, "transport-info"))
{ //peer wants to add ports.
if (c2c)
{
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
JCL_JingleParsePeerPorts(jcl, c2c, tree, from, sid);
}
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
//FIXME: we need to add support for this to downgrade to raw if someone tries calling through a SIP gateway
else if (!strcmp(action, "transport-replace"))
{
if (c2c)
{
if (1)
{
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
JCL_JingleSend(jcl, c2c, "transport-reject");
}
else
{
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
JCL_JingleParsePeerPorts(jcl, c2c, tree, from, sid);
JCL_JingleSend(jcl, c2c, "transport-accept");
}
}
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "transport-reject"))
{
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
JCL_JingleSend(jcl, c2c, "session-terminate");
}
else if (!strcmp(action, "session-accept"))
{
if (c2c)
{
//response from a message we sent.
if (JCL_JingleHandleSessionAccept(jcl, tree, from, c2c, b))
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
else
JCL_JingleError(jcl, tree, from, id, JE_OUTOFORDER);
}
else
JCL_JingleError(jcl, tree, from, id, JE_UNKNOWNSESSION);
}
else if (!strcmp(action, "session-initiate"))
{
// Con_Printf("Peer initiating connection!\n");
// XML_ConPrintTree(tree, 0);
c2c = JCL_JingleHandleInitiate(jcl, tree, from);
if (c2c)
{
qboolean voice = false, video = false, server = false, client = false;
char *offer;
char convolink[512];
int c;
qboolean doprompt = false;
JCL_JingleError(jcl, tree, from, id, JE_ACKNOWLEDGE);
for (c = 0; c < c2c->contents; c++)
{
switch(c2c->content[c].mediatype)
{
case ICEP_INVALID: break;
case ICEP_VOICE: voice = true; doprompt |= !pCvar_GetFloat("xmpp_autoacceptvoice"); break;
case ICEP_VIDEO: video = true; doprompt |= !pCvar_GetFloat("xmpp_autoacceptvoice"); break;
case ICEP_QWSERVER: server = true; doprompt |= !pCvar_GetFloat("xmpp_autoacceptjoins"); break;
case ICEP_QWCLIENT: client = true; doprompt |= !pCvar_GetFloat("xmpp_autoacceptinvites"); break;
default: doprompt |= true; break;
}
}
if (video)
{
if (server)
offer = "wants to join your game (with ^1video^7)";
else if (client)
offer = "wants to invite you to thier game (with ^1video^7)";
else
offer = "is trying to ^1video^7 call you";
}
else if (voice)
{
if (server)
offer = "wants to join your game (with voice)";
else if (client)
offer = "wants to invite you to thier game (with voice)";
else
offer = "is trying to call you";
}
else
{
if (server)
offer = "wants to join your game";
else if (client)
offer = "wants to invite you to thier game";
else
offer = "is trying to waste your time";
}
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
if (doprompt)
{
char authlink[512];
char denylink[512];
JCL_GenLink(jcl, authlink, sizeof(authlink), "jauth", from, NULL, sid, "%s", "Accept");
JCL_GenLink(jcl, denylink, sizeof(denylink), "jdeny", from, NULL, sid, "%s", "Reject");
//show a prompt for it, send the reply when the user decides.
XMPP_ConversationPrintf(b->accountdomain, b->name,
"%s %s. %s %s\n", convolink, offer, authlink, denylink);
pCon_SetActive(b->accountdomain);
}
else
{
XMPP_ConversationPrintf(b->accountdomain, b->name, "Auto-accepting session from %s\n", convolink);
JCL_Join(jcl, from, sid, true, ICEP_INVALID);
}
}
else
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
}
else
{
JCL_JingleError(jcl, tree, from, id, JE_UNSUPPORTED);
}
return true;
}
#endif