229eb39bfc
git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@4416 fc73d0e0-1445-4013-8a0c-d673dee63da5
3283 lines
87 KiB
C
3283 lines
87 KiB
C
//Released under the terms of the gpl as this file uses a bit of quake derived code. All sections of the like are marked as such
|
|
|
|
#include "../plugin.h"
|
|
#include <time.h>
|
|
#include "../../engine/common/netinc.h"
|
|
#include "xml.h"
|
|
|
|
//#define NOICE
|
|
#define VOIP_SPEEX
|
|
|
|
#ifdef VOIP_SPEEX
|
|
#define VOIP
|
|
#endif
|
|
|
|
#define DEFAULTDOMAIN ""
|
|
#define DEFAULTRESOURCE "Quake"
|
|
#define QUAKEMEDIATYPE "quake"
|
|
#define QUAKEMEDIAXMLNS "fteqw.com:netmedia"
|
|
#define DEFAULTICEMODE ICEM_ICE
|
|
|
|
icefuncs_t *piceapi;
|
|
|
|
|
|
#define Q_strncpyz(o, i, l) do {strncpy(o, i, l-1);o[l-1]='\0';}while(0)
|
|
|
|
#define JCL_BUILD "3"
|
|
|
|
|
|
#define ARGNAMES ,sock,certhostname
|
|
BUILTINR(int, Net_SetTLSClient, (qhandle_t sock, const char *certhostname));
|
|
#undef ARGNAMES
|
|
|
|
#define ARGNAMES ,funcname
|
|
BUILTINR(void *, Plug_GetNativePointer, (const char *funcname));
|
|
#undef ARGNAMES
|
|
|
|
void (*Con_TrySubPrint)(const char *conname, const char *message);
|
|
void Fallback_ConPrint(const char *conname, const char *message)
|
|
{
|
|
pCon_Print(message);
|
|
}
|
|
|
|
void Con_SubPrintf(char *subname, char *format, ...)
|
|
{
|
|
va_list argptr;
|
|
static char string[1024];
|
|
|
|
va_start (argptr, format);
|
|
Q_vsnprintf (string, sizeof(string), format,argptr);
|
|
va_end (argptr);
|
|
|
|
Con_TrySubPrint(subname, string);
|
|
}
|
|
|
|
|
|
//porting zone:
|
|
|
|
|
|
|
|
|
|
#define COLOURGREEN "^2"
|
|
#define COLORWHITE "^7"
|
|
#define COLOURWHITE "^7" // word
|
|
#define COLOURRED "^1"
|
|
#define COLOURYELLOW "^3"
|
|
#define COLOURPURPLE "^5"
|
|
#define COMMANDPREFIX "xmpp"
|
|
#define COMMANDPREFIX2 "jab"
|
|
#define COMMANDPREFIX3 "jabbercl"
|
|
#define playsound(s)
|
|
|
|
|
|
#define TL_NETGETPACKETERROR "NET_GetPacket Error %s\n"
|
|
|
|
static char *JCL_ParseOut (char *data, char *buf, int bufsize) //this is taken out of quake
|
|
{
|
|
int c;
|
|
int len;
|
|
|
|
len = 0;
|
|
buf[0] = 0;
|
|
|
|
if (!data)
|
|
return NULL;
|
|
|
|
// skip whitespace
|
|
while ( (c = *data) <= ' ')
|
|
{
|
|
if (c == 0)
|
|
return NULL; // end of file;
|
|
data++;
|
|
}
|
|
|
|
// handle quoted strings specially
|
|
if (c == '\"')
|
|
{
|
|
data++;
|
|
while (1)
|
|
{
|
|
if (len >= bufsize-1)
|
|
return data;
|
|
|
|
c = *data++;
|
|
if (c=='\"' || !c)
|
|
{
|
|
buf[len] = 0;
|
|
return data;
|
|
}
|
|
buf[len] = c;
|
|
len++;
|
|
}
|
|
}
|
|
|
|
// parse a regular word
|
|
do
|
|
{
|
|
if (len >= bufsize-1)
|
|
return data;
|
|
|
|
buf[len] = c;
|
|
data++;
|
|
len++;
|
|
c = *data;
|
|
} while (c>32);
|
|
|
|
buf[len] = 0;
|
|
return data;
|
|
}
|
|
|
|
|
|
char *JCL_Info_ValueForKey (char *s, const char *key, char *valuebuf, int valuelen)
|
|
{
|
|
char pkey[1024];
|
|
char *o;
|
|
|
|
if (*s == '\\')
|
|
s++;
|
|
while (1)
|
|
{
|
|
o = pkey;
|
|
while (*s != '\\')
|
|
{
|
|
if (!*s)
|
|
{
|
|
*valuebuf='\0';
|
|
return valuebuf;
|
|
}
|
|
*o++ = *s++;
|
|
if (o+2 >= pkey+sizeof(pkey)) //hrm. hackers at work..
|
|
{
|
|
*valuebuf='\0';
|
|
return valuebuf;
|
|
}
|
|
}
|
|
*o = 0;
|
|
s++;
|
|
|
|
o = valuebuf;
|
|
|
|
while (*s != '\\' && *s)
|
|
{
|
|
if (!*s)
|
|
{
|
|
*valuebuf='\0';
|
|
return valuebuf;
|
|
}
|
|
*o++ = *s++;
|
|
|
|
if (o+2 >= valuebuf+valuelen) //hrm. hackers at work..
|
|
{
|
|
*valuebuf='\0';
|
|
return valuebuf;
|
|
}
|
|
}
|
|
*o = 0;
|
|
|
|
if (!strcmp (key, pkey) )
|
|
return valuebuf;
|
|
|
|
if (!*s)
|
|
{
|
|
*valuebuf='\0';
|
|
return valuebuf;
|
|
}
|
|
s++;
|
|
}
|
|
}
|
|
|
|
|
|
void RenameConsole(char *totrim);
|
|
void JCL_Command(char *consolename);
|
|
void JCL_LoadConfig(void);
|
|
void JCL_WriteConfig(void);
|
|
|
|
qintptr_t JCL_ExecuteCommand(qintptr_t *args)
|
|
{
|
|
char cmd[256];
|
|
pCmd_Argv(0, cmd, sizeof(cmd));
|
|
if (!strcmp(cmd, COMMANDPREFIX) || !strcmp(cmd, COMMANDPREFIX2) || !strcmp(cmd, COMMANDPREFIX3))
|
|
{
|
|
if (!args[0])
|
|
JCL_Command("");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
qintptr_t JCL_ConsoleLink(qintptr_t *args);
|
|
qintptr_t JCL_ConExecuteCommand(qintptr_t *args);
|
|
|
|
qintptr_t JCL_Frame(qintptr_t *args);
|
|
|
|
qintptr_t Plug_Init(qintptr_t *args)
|
|
{
|
|
if ( Plug_Export("Tick", JCL_Frame) &&
|
|
Plug_Export("ExecuteCommand", JCL_ExecuteCommand))
|
|
{
|
|
CHECKBUILTIN(Net_SetTLSClient);
|
|
if (!BUILTINISVALID(Net_SetTLSClient))
|
|
Con_Printf("XMPP Plugin Loaded ^1without^7 TLS\n");
|
|
else
|
|
Con_Printf("XMPP Plugin Loaded. For help, use: ^[/"COMMANDPREFIX" /help^]\n");
|
|
|
|
Plug_Export("ConsoleLink", JCL_ConsoleLink);
|
|
|
|
if (!Plug_Export("ConExecuteCommand", JCL_ConExecuteCommand))
|
|
{
|
|
Con_Printf("XMPP plugin in single-console mode\n");
|
|
Con_TrySubPrint = Fallback_ConPrint;
|
|
}
|
|
else
|
|
Con_TrySubPrint = pCon_SubPrint;
|
|
|
|
pCmd_AddCommand(COMMANDPREFIX);
|
|
pCmd_AddCommand(COMMANDPREFIX2);
|
|
pCmd_AddCommand(COMMANDPREFIX3);
|
|
|
|
//flags&1 == archive
|
|
pCvar_Register("xmpp_nostatus", "0", 0, "xmpp");
|
|
pCvar_Register("xmpp_autoacceptjoins", "0", 0, "xmpp");
|
|
pCvar_Register("xmpp_autoacceptinvites", "0", 0, "xmpp");
|
|
pCvar_Register("xmpp_autoacceptvoice", "0", 0, "xmpp");
|
|
pCvar_Register("xmpp_debug", "0", 0, "xmpp");
|
|
pCvar_Register("xmpp_disabletls", "0", CVAR_NOTFROMSERVER, "xmpp");
|
|
pCvar_Register("xmpp_allowplainauth", "0", CVAR_NOTFROMSERVER, "xmpp");
|
|
|
|
CHECKBUILTIN(Plug_GetNativePointer);
|
|
if (BUILTINISVALID(Plug_GetNativePointer))
|
|
piceapi = pPlug_GetNativePointer(ICE_API_CURRENT);
|
|
|
|
JCL_LoadConfig();
|
|
return 1;
|
|
}
|
|
else
|
|
Con_Printf("JCL Client Plugin failed\n");
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//\r\n is used to end a line.
|
|
//meaning \0s are valid.
|
|
//but never used cos it breaks strings
|
|
|
|
|
|
#define JCL_MAXMSGLEN 10000
|
|
|
|
#define CAP_QUERIED 1 //a query is pending or something.
|
|
#define CAP_VOICE 2 //supports voice
|
|
#define CAP_INVITE 4 //supports game invites.
|
|
#define CAP_POKE 8 //can be slapped.
|
|
|
|
typedef struct bresource_s
|
|
{
|
|
char bstatus[128]; //basic status
|
|
char fstatus[128]; //full status
|
|
char server[256];
|
|
int servertype; //0=none, 1=already a client, 2=joinable
|
|
|
|
unsigned int caps;
|
|
|
|
struct bresource_s *next;
|
|
|
|
char resource[1];
|
|
} bresource_t;
|
|
typedef struct buddy_s
|
|
{
|
|
bresource_t *resources;
|
|
bresource_t *defaultresource; //this is the one that last replied
|
|
int defaulttimestamp;
|
|
qboolean friended;
|
|
|
|
char name[256];
|
|
|
|
struct buddy_s *next;
|
|
char accountdomain[1]; //no resource on there
|
|
} buddy_t;
|
|
typedef struct jclient_s
|
|
{
|
|
char server[64];
|
|
int port;
|
|
|
|
enum
|
|
{
|
|
JCL_DEAD, //not connected. connection died or something.
|
|
JCL_AUTHING, //connected, but not able to send any info on it other than to auth
|
|
JCL_ACTIVE //we're connected, we got a buddy list and everything
|
|
} status;
|
|
unsigned int timeout; //reconnect/ping timer
|
|
|
|
qhandle_t socket;
|
|
|
|
//we buffer output for times when the outgoing socket is full.
|
|
//mostly this only happens at the start of the connection when the socket isn't actually open yet.
|
|
char *outbuf;
|
|
int outbufpos;
|
|
int outbuflen;
|
|
int outbufmax;
|
|
|
|
char bufferedinmessage[JCL_MAXMSGLEN+1]; //servers are required to be able to handle messages no shorter than a specific size.
|
|
//which means we need to be able to handle messages when they get to us.
|
|
//servers can still handle larger messages if they choose, so this might not be enough.
|
|
int bufferedinammount;
|
|
|
|
char defaultdest[256];
|
|
|
|
char domain[256];
|
|
char username[256];
|
|
char password[256];
|
|
char resource[256];
|
|
char jid[256]; //this is our full username@domain/resource string
|
|
char localalias[256];//this is what's shown infront of outgoing messages. >> by default until we can get our name.
|
|
|
|
int tagdepth;
|
|
int openbracket;
|
|
int instreampos;
|
|
|
|
qboolean tlsconnect; //use the old tls method on port 5223.
|
|
qboolean connected; //fully on server and authed and everything.
|
|
qboolean issecure; //tls enabled (either upgraded or initially)
|
|
qboolean streamdebug; //echo the stream to subconsoles
|
|
|
|
qboolean preapproval; //server supports presence preapproval
|
|
|
|
char curquakeserver[2048];
|
|
char defaultnamespace[2048]; //should be 'jabber:client' or blank (and spammy with all the extra xmlns attribs)
|
|
|
|
struct iq_s
|
|
{
|
|
struct iq_s *next;
|
|
char id[64];
|
|
int timeout;
|
|
qboolean (*callback) (struct jclient_s *jcl, struct subtree_s *tree, struct iq_s *iq);
|
|
void *usrptr;
|
|
} *pendingiqs;
|
|
|
|
struct c2c_s
|
|
{
|
|
struct c2c_s *next;
|
|
enum iceproto_e mediatype;
|
|
enum icemode_e method; //ICE_RAW or ICE_ICE. this is what the peer asked for. updated if we degrade it.
|
|
qboolean accepted; //connection is going
|
|
qboolean creator; //true if we're the creator.
|
|
|
|
struct icestate_s *ice;
|
|
char *peeraddr;
|
|
int peerport;
|
|
|
|
char *with;
|
|
char sid[1];
|
|
} *c2c;
|
|
|
|
buddy_t *buddies;
|
|
} jclient_t;
|
|
jclient_t *jclient;
|
|
int jclient_curtime;
|
|
|
|
struct subtree_s;
|
|
|
|
void JCL_AddClientMessagef(jclient_t *jcl, char *fmt, ...);
|
|
qboolean JCL_FindBuddy(jclient_t *jcl, char *jid, buddy_t **buddy, bresource_t **bres);
|
|
void JCL_GeneratePresence(jclient_t *jcl, qboolean force);
|
|
struct iq_s *JCL_SendIQf(jclient_t *jcl, qboolean (*callback) (jclient_t *jcl, struct subtree_s *tree, struct iq_s *iq), char *iqtype, char *target, char *fmt, ...);
|
|
struct iq_s *JCL_SendIQNode(jclient_t *jcl, qboolean (*callback) (jclient_t *jcl, xmltree_t *tree, struct iq_s *iq), char *iqtype, char *target, xmltree_t *node, qboolean destroynode);
|
|
|
|
char *TrimResourceFromJid(char *jid)
|
|
{
|
|
char *slash;
|
|
slash = strchr(jid, '/');
|
|
if (slash)
|
|
{
|
|
*slash = '\0';
|
|
return slash+1;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct c2c_s *JCL_JingleCreateSession(jclient_t *jcl, char *with, qboolean creator, char *sid, int method, int mediatype)
|
|
{
|
|
struct icestate_s *ice = NULL;
|
|
struct c2c_s *c2c;
|
|
char generatedname[64];
|
|
|
|
if (piceapi)
|
|
ice = piceapi->ICE_Create(NULL, sid, with, method, mediatype);
|
|
if (ice)
|
|
{
|
|
piceapi->ICE_Get(ice, "sid", generatedname, sizeof(generatedname));
|
|
sid = generatedname;
|
|
|
|
piceapi->ICE_Set(ice, "codec96", "speex@8000");
|
|
piceapi->ICE_Set(ice, "codec97", "speex@16000");
|
|
piceapi->ICE_Set(ice, "codec98", "opus");
|
|
}
|
|
else
|
|
return NULL; //no way to get the local ip otherwise, which means things won't work proper
|
|
|
|
c2c = malloc(sizeof(*c2c) + strlen(sid));
|
|
memset(c2c, 0, sizeof(*c2c));
|
|
c2c->next = jcl->c2c;
|
|
jcl->c2c = c2c;
|
|
strcpy(c2c->sid, sid);
|
|
|
|
c2c->mediatype = mediatype;
|
|
c2c->creator = creator;
|
|
c2c->method = method;
|
|
|
|
//FIXME: we need to query this from the server.
|
|
//however, google don't implement the 'standard' way
|
|
//the 'standard' way contains a huge big fat do-not-implement message.
|
|
//and google's way equally says 'don't implement'... so...
|
|
//as I kinda expect most users to use google's network anyway, I *hope* they won't mind too much. I doubt they'll get much traffic anyway. its just stun.
|
|
piceapi->ICE_Set(ice, "stunport", "19302");
|
|
piceapi->ICE_Set(ice, "stunip", "stun.l.google.com");
|
|
|
|
//copy out the interesting parameters
|
|
c2c->with = strdup(with);
|
|
c2c->ice = ice;
|
|
return c2c;
|
|
}
|
|
static qboolean JCL_JingleAcceptAck(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
struct c2c_s *c2c;
|
|
if (tree)
|
|
{
|
|
for (c2c = jcl->c2c; c2c; c2c = c2c->next)
|
|
{
|
|
if (c2c == iq->usrptr)
|
|
{
|
|
if (c2c->ice)
|
|
piceapi->ICE_Set(c2c->ice, "state", STRINGIFY(ICE_CONNECTING));
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/*
|
|
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.
|
|
*/
|
|
qboolean JCL_JingleSend(jclient_t *jcl, struct c2c_s *c2c, char *action)
|
|
{
|
|
qboolean result;
|
|
xmltree_t *jingle;
|
|
struct icestate_s *ice = c2c->ice;
|
|
qboolean wasaccept = false;
|
|
int transportmode = ICEM_ICE;
|
|
|
|
if (!ice)
|
|
action = "session-terminate";
|
|
|
|
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->jid);
|
|
}
|
|
|
|
if (!strcmp(action, "session-terminate"))
|
|
{
|
|
struct c2c_s **link;
|
|
for (link = &jcl->c2c; *link; link = &(*link)->next)
|
|
{
|
|
if (*link == c2c)
|
|
{
|
|
*link = c2c->next;
|
|
break;
|
|
}
|
|
}
|
|
if (c2c->ice)
|
|
piceapi->ICE_Close(c2c->ice);
|
|
|
|
result = false;
|
|
}
|
|
else
|
|
{
|
|
xmltree_t *content = XML_CreateNode(jingle, "content", "", "");
|
|
|
|
result = true;
|
|
|
|
if (!strcmp(action, "session-accept"))
|
|
{
|
|
if (c2c->method == transportmode)
|
|
{
|
|
XML_AddParameter(jingle, "responder", jcl->jid);
|
|
c2c->accepted = wasaccept = true;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
#ifdef VOIP
|
|
if (c2c->mediatype == ICEP_VOICE)
|
|
{
|
|
xmltree_t *payload;
|
|
int i;
|
|
|
|
XML_AddParameter(content, "senders", "both");
|
|
XML_AddParameter(content, "name", "audio-session");
|
|
XML_AddParameter(content, "creator", "initiator");
|
|
|
|
description = XML_CreateNode(content, "description", "urn:xmpp:jingle:apps:rtp:1", "");
|
|
XML_AddParameter(description, "media", "audio");
|
|
|
|
for (i = 96; i <= 127; i++)
|
|
{
|
|
char codecname[64];
|
|
char argn[64];
|
|
Q_snprintf(argn, sizeof(argn), "codec%i", i);
|
|
piceapi->ICE_Get(ice, argn, codecname, sizeof(codecname));
|
|
|
|
if (!strcmp(codecname, "speex@8000"))
|
|
{ //speex narrowband
|
|
payload = XML_CreateNode(description, "payload-type", "", "");
|
|
XML_AddParameter(payload, "channels", "1");
|
|
XML_AddParameter(payload, "clockrate", "8000");
|
|
XML_AddParameter(payload, "id", argn+5);
|
|
XML_AddParameter(payload, "name", "SPEEX");
|
|
}
|
|
else if (!strcmp(codecname, "speex@16000"))
|
|
{ //speex wideband
|
|
payload = XML_CreateNode(description, "payload-type", "", "");
|
|
XML_AddParameter(payload, "channels", "1");
|
|
XML_AddParameter(payload, "clockrate", "16000");
|
|
XML_AddParameter(payload, "id", argn+5);
|
|
XML_AddParameter(payload, "name", "SPEEX");
|
|
}
|
|
else if (!strcmp(codecname, "speex@32000"))
|
|
{ //speex ultrawideband
|
|
payload = XML_CreateNode(description, "payload-type", "", "");
|
|
XML_AddParameter(payload, "channels", "1");
|
|
XML_AddParameter(payload, "clockrate", "32000");
|
|
XML_AddParameter(payload, "id", argn+5);
|
|
XML_AddParameter(payload, "name", "SPEEX");
|
|
}
|
|
else if (!strcmp(codecname, "opus"))
|
|
{ //opus codec.
|
|
payload = XML_CreateNode(description, "payload-type", "", "");
|
|
XML_AddParameter(payload, "channels", "1");
|
|
XML_AddParameter(payload, "id", argn+5);
|
|
XML_AddParameter(payload, "name", "OPUS");
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
description = XML_CreateNode(content, "description", QUAKEMEDIAXMLNS, "");
|
|
XML_AddParameter(description, "media", QUAKEMEDIATYPE);
|
|
if (c2c->mediatype == ICEP_QWSERVER)
|
|
XML_AddParameter(description, "host", "me");
|
|
else if (c2c->mediatype == ICEP_QWCLIENT)
|
|
XML_AddParameter(description, "host", "you");
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
struct c2c_s *c2c;
|
|
for (c2c = jcl->c2c; c2c; c2c = c2c->next)
|
|
{
|
|
struct icecandinfo_s *lc;
|
|
if (c2c->method == ICEM_ICE)
|
|
{
|
|
char bah[2];
|
|
piceapi->ICE_Get(c2c->ice, "newlc", bah, sizeof(bah));
|
|
if (atoi(bah))
|
|
{
|
|
Con_DPrintf("Sending updated local addresses\n");
|
|
JCL_JingleSend(jcl, c2c, "transport-info");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void JCL_Join(jclient_t *jcl, char *target, char *sid, qboolean allow, int protocol)
|
|
{
|
|
struct c2c_s *c2c = NULL, **link;
|
|
xmltree_t *jingle;
|
|
struct icestate_s *ice;
|
|
char autotarget[256];
|
|
buddy_t *b;
|
|
bresource_t *br;
|
|
if (!jcl)
|
|
return;
|
|
|
|
JCL_FindBuddy(jcl, target, &b, &br);
|
|
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; link = &(*link)->next)
|
|
{
|
|
if (!strcmp((*link)->with, target) && (!sid || !strcmp((*link)->sid, sid)) && ((*link)->mediatype == protocol || protocol == ICEP_INVALID))
|
|
{
|
|
c2c = *link;
|
|
break;
|
|
}
|
|
}
|
|
if (allow)
|
|
{
|
|
if (!c2c)
|
|
{
|
|
if (!sid)
|
|
{
|
|
c2c = JCL_JingleCreateSession(jcl, target, true, sid, DEFAULTICEMODE, ((protocol == ICEP_INVALID)?ICEP_QWCLIENT:protocol));
|
|
JCL_JingleSend(jcl, c2c, "session-initiate");
|
|
|
|
Con_SubPrintf(b->name, "%s ^[[%s]\\xmpp\\%s^] ^[[Hang Up]\\xmppact\\jdeny\\xmpp\\%s\\xmppsid\\%s^].\n", protocol==ICEP_VOICE?"Calling":"Requesting session with", target, target, target, c2c->sid);
|
|
}
|
|
else
|
|
Con_SubPrintf(b->name, "That session has expired.\n");
|
|
}
|
|
else if (c2c->creator)
|
|
{
|
|
//resend initiate if they've not acked it... I dunno...
|
|
JCL_JingleSend(jcl, c2c, "session-initiate");
|
|
Con_SubPrintf(b->name, "Restarting session with ^[[%s]\\xmpp\\%s^].\n", target, target);
|
|
}
|
|
else if (c2c->accepted)
|
|
Con_SubPrintf(b->name, "That session was already accepted.\n");
|
|
else
|
|
{
|
|
JCL_JingleSend(jcl, c2c, "session-accept");
|
|
Con_SubPrintf(b->name, "Accepting session from ^[[%s]\\xmpp\\%s^].\n", target, target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (c2c)
|
|
{
|
|
JCL_JingleSend(jcl, c2c, "session-terminate");
|
|
Con_SubPrintf(b->name, "Terminating session with ^[[%s]\\xmpp\\%s^].\n", target, target);
|
|
}
|
|
else
|
|
Con_SubPrintf(b->name, "That session has already expired.\n");
|
|
}
|
|
}
|
|
|
|
void JCL_JingleParsePeerPorts(jclient_t *jcl, struct c2c_s *c2c, xmltree_t *inj, char *from)
|
|
{
|
|
xmltree_t *incontent = XML_ChildOfTree(inj, "content", 0);
|
|
xmltree_t *intransport = XML_ChildOfTree(incontent, "transport", 0);
|
|
xmltree_t *incandidate;
|
|
struct icecandinfo_s rem;
|
|
int i;
|
|
|
|
if (strcmp(c2c->with, from) || strcmp(c2c->sid, XML_GetParameter(inj, "sid", "")))
|
|
{
|
|
Con_Printf("%s is trying to mess with our connections...\n", from);
|
|
return;
|
|
}
|
|
|
|
if (!c2c->sid)
|
|
return;
|
|
|
|
if (!intransport)
|
|
return;
|
|
|
|
if (!c2c->ice)
|
|
return;
|
|
|
|
piceapi->ICE_Set(c2c->ice, "rufrag", XML_GetParameter(intransport, "ufrag", ""));
|
|
piceapi->ICE_Set(c2c->ice, "rpwd", XML_GetParameter(intransport, "pwd", ""));
|
|
|
|
for (i = 0; (incandidate = XML_ChildOfTree(intransport, "candidate", i)); i++)
|
|
{
|
|
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(c2c->ice, &rem);
|
|
}
|
|
}
|
|
qboolean JCL_JingleHandleInitiate(jclient_t *jcl, xmltree_t *inj, char *from)
|
|
{
|
|
/*inj contains something like:
|
|
<jingle sid='purplea84196dc' responder='me@example.com/Quake' initiator='them@example.com/Quake' action='session-initiate' xmlns='urn:xmpp:jingle:1'>
|
|
<content senders='both' name='audio-session' creator='initiator'>
|
|
<transport ufrag='SES2' pwd='3XkwverVxJLy2lRXD1lOAb' xmlns='urn:xmpp:jingle:transports:ice-udp:1'>
|
|
<candidate type='host' protocol='udp' priority='2013266431' port='53177' network='0' ip='192.168.0.182' id='purplea84196de' generation='0' foundation='1' component='1'/>
|
|
<candidate type='host' protocol='udp' priority='2013266430' port='36480' network='0' ip='192.168.0.182' id='purplea84196dd' generation='0' foundation='1' component='2'/>
|
|
</transport>
|
|
<description media='audio' xmlns='urn:xmpp:jingle:apps:rtp:1'>
|
|
<payload-type channels='1' clockrate='8000' id='104' name='SPEEX'/>
|
|
<payload-type channels='1' clockrate='16000' id='103' name='SPEEX'/>
|
|
</description>
|
|
</content>
|
|
</jingle>
|
|
*/
|
|
|
|
xmltree_t *incontent = XML_ChildOfTree(inj, "content", 0);
|
|
xmltree_t *intransport = XML_ChildOfTree(incontent, "transport", 0);
|
|
xmltree_t *indescription = XML_ChildOfTree(incontent, "description", 0);
|
|
char *transportxmlns = intransport?intransport->xmlns:"";
|
|
char *descriptionxmlns = indescription?indescription->xmlns:"";
|
|
char *descriptionmedia = XML_GetParameter(indescription, "media", "");
|
|
char *sid = XML_GetParameter(inj, "sid", "");
|
|
|
|
xmltree_t *jingle;
|
|
struct icestate_s *ice;
|
|
qboolean accepted = false;
|
|
enum icemode_e imode;
|
|
char *response = "session-terminate";
|
|
char *offer = "pwn you";
|
|
char *autocvar = "xmpp_autoaccepthax";
|
|
char *initiator;
|
|
|
|
struct c2c_s *c2c = NULL;
|
|
int mt = ICEP_INVALID;
|
|
buddy_t *b;
|
|
|
|
//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 (incontent && !strcmp(descriptionmedia, QUAKEMEDIATYPE) && !strcmp(descriptionxmlns, QUAKEMEDIAXMLNS))
|
|
{
|
|
char *host = XML_GetParameter(indescription, "host", "you");
|
|
if (!strcmp(host, "you"))
|
|
{
|
|
mt = ICEP_QWSERVER;
|
|
offer = "join your game";
|
|
autocvar = "xmpp_autoacceptjoins";
|
|
}
|
|
else if (!strcmp(host, "me"))
|
|
{
|
|
mt = ICEP_QWCLIENT;
|
|
offer = "invite you to thier game";
|
|
autocvar = "xmpp_autoacceptinvites";
|
|
}
|
|
}
|
|
if (incontent && !strcmp(descriptionmedia, "audio") && !strcmp(descriptionxmlns, "urn:xmpp:jingle:apps:rtp:1"))
|
|
{
|
|
mt = ICEP_VOICE;
|
|
offer = "have a natter with you";
|
|
autocvar = "xmpp_autoacceptvoice";
|
|
}
|
|
if (mt == ICEP_INVALID)
|
|
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_JingleCreateSession(jcl, from, false,
|
|
sid,
|
|
strcmp(transportxmlns, "urn:xmpp:jingle:transports:raw-udp:1")?ICEM_ICE:ICEM_RAW,
|
|
mt
|
|
);
|
|
if (!c2c)
|
|
return false;
|
|
|
|
|
|
JCL_FindBuddy(jcl, from, &b, NULL);
|
|
|
|
if (c2c->mediatype == 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];
|
|
if (!strcmp(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->ice, parm, val);
|
|
}
|
|
else if (!strcmp(name, "OPUS"))
|
|
{
|
|
Q_snprintf(parm, sizeof(parm), "codec%i", atoi(id));
|
|
okay |= piceapi->ICE_Set(c2c->ice, parm, "opus");
|
|
}
|
|
}
|
|
//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.
|
|
if (!okay)
|
|
{
|
|
JCL_JingleSend(jcl, c2c, "session-terminate");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
JCL_JingleParsePeerPorts(jcl, c2c, inj, from);
|
|
|
|
if (c2c->mediatype != ICEP_INVALID)
|
|
{
|
|
if (!pCvar_GetFloat(autocvar))
|
|
{
|
|
//show a prompt for it, send the reply when the user decides.
|
|
Con_SubPrintf(b->name,
|
|
"^[[%s]\\xmpp\\%s^] wants to %s. "
|
|
"^[[Authorise]\\xmppact\\jauth\\xmpp\\%s\\xmppsid\\%s^] "
|
|
"^[[Deny]\\xmppact\\jdeny\\xmpp\\%s\\xmppsid\\%s^]\n",
|
|
from, from,
|
|
offer,
|
|
from, sid,
|
|
from, sid);
|
|
pCon_SetActive(b->name);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Con_SubPrintf(b->name, "Auto-accepting session from ^[[%s]\\xmpp\\%s^]\n", from, from);
|
|
response = "session-accept";
|
|
}
|
|
}
|
|
|
|
JCL_JingleSend(jcl, c2c, response);
|
|
return true;
|
|
}
|
|
|
|
qboolean JCL_ParseJingle(jclient_t *jcl, xmltree_t *tree, char *from, char *id)
|
|
{
|
|
char *action = XML_GetParameter(tree, "action", "");
|
|
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;
|
|
}
|
|
}
|
|
|
|
JCL_FindBuddy(jcl, from, &b, NULL);
|
|
|
|
//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"))
|
|
{
|
|
xmltree_t *reason = XML_ChildOfTree(tree, "reason", 0);
|
|
if (!c2c)
|
|
{
|
|
Con_Printf("Received session-terminate without an active session\n");
|
|
return false;
|
|
}
|
|
|
|
if (reason && reason->child)
|
|
Con_SubPrintf(b->name, "Session ended: %s\n", reason->child->name);
|
|
else
|
|
Con_SubPrintf(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);
|
|
|
|
if (c2c->ice)
|
|
piceapi->ICE_Close(c2c->ice);
|
|
free(c2c);
|
|
}
|
|
//content-accept
|
|
//content-add
|
|
//content-modify
|
|
//content-reject
|
|
//content-remove
|
|
//description-info
|
|
//security-info
|
|
//
|
|
else if (!strcmp(action, "transport-info"))
|
|
{ //peer wants to add ports.
|
|
if (c2c)
|
|
JCL_JingleParsePeerPorts(jcl, c2c, tree, from);
|
|
else
|
|
Con_DPrintf("Received transport-info without an active session\n");
|
|
}
|
|
//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_JingleSend(jcl, c2c, "transport-reject");
|
|
else
|
|
{
|
|
JCL_JingleParsePeerPorts(jcl, c2c, tree, from);
|
|
JCL_JingleSend(jcl, c2c, "transport-accept");
|
|
}
|
|
}
|
|
}
|
|
else if (!strcmp(action, "transport-reject"))
|
|
{
|
|
JCL_JingleSend(jcl, c2c, "session-terminate");
|
|
}
|
|
else if (!strcmp(action, "session-accept"))
|
|
{
|
|
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
|
|
{
|
|
char *responder = XML_GetParameter(tree, "responder", from);
|
|
if (strcmp(responder, from))
|
|
{
|
|
return false;
|
|
}
|
|
Con_SubPrintf(b->name, "Session Accepted!\n");
|
|
// XML_ConPrintTree(tree, 0);
|
|
|
|
JCL_JingleParsePeerPorts(jcl, c2c, tree, from);
|
|
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
|
|
if (c2c->ice)
|
|
piceapi->ICE_Set(c2c->ice, "state", STRINGIFY(ICE_CONNECTING));
|
|
}
|
|
}
|
|
else if (!strcmp(action, "session-initiate"))
|
|
{
|
|
// Con_Printf("Peer initiating connection!\n");
|
|
// XML_ConPrintTree(tree, 0);
|
|
|
|
if (!JCL_JingleHandleInitiate(jcl, tree, from))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("Unknown jingle action: %s\n", action);
|
|
// XML_ConPrintTree(tree, 0);
|
|
}
|
|
|
|
JCL_AddClientMessagef(jcl,
|
|
"<iq type='result' to='%s' id='%s' />", from, id);
|
|
return true;
|
|
}
|
|
|
|
|
|
qintptr_t JCL_ConsoleLink(qintptr_t *args)
|
|
{
|
|
char text[256];
|
|
char link[256];
|
|
char who[256];
|
|
char what[256];
|
|
// pCmd_Argv(0, text, sizeof(text));
|
|
pCmd_Argv(1, link, sizeof(link));
|
|
|
|
JCL_Info_ValueForKey(link, "xmpp", who, sizeof(who));
|
|
JCL_Info_ValueForKey(link, "xmppact", what, sizeof(what));
|
|
|
|
if (!*who && !*what)
|
|
return false;
|
|
|
|
if (!strcmp(what, "pauth"))
|
|
{
|
|
//we should friend them too.
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='subscribed'/>", who);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "pdeny"))
|
|
{
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='unsubscribed'/>", who);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "jauth"))
|
|
{
|
|
JCL_Info_ValueForKey(link, "xmppsid", what, sizeof(what));
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_Join(jclient, who, what, true, ICEP_INVALID);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "jdeny"))
|
|
{
|
|
JCL_Info_ValueForKey(link, "xmppsid", what, sizeof(what));
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_Join(jclient, who, what, false, ICEP_INVALID);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "join"))
|
|
{
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_Join(jclient, who, NULL, true, ICEP_QWCLIENT);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "invite"))
|
|
{
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_Join(jclient, who, NULL, true, ICEP_QWSERVER);
|
|
return true;
|
|
}
|
|
else if (!strcmp(what, "call"))
|
|
{
|
|
if (jclient && jclient->status == JCL_ACTIVE)
|
|
JCL_Join(jclient, who, NULL, true, ICEP_VOICE);
|
|
return true;
|
|
}
|
|
else if ((*who && !*what) || !strcmp(what, "msg"))
|
|
{
|
|
if (jclient)
|
|
{
|
|
char *f;
|
|
buddy_t *b;
|
|
bresource_t *br;
|
|
|
|
JCL_FindBuddy(jclient, *who?who:jclient->defaultdest, &b, &br);
|
|
f = b->name;
|
|
b->defaultresource = br;
|
|
|
|
if (BUILTINISVALID(Con_SubPrint))
|
|
pCon_SubPrint(f, "");
|
|
if (BUILTINISVALID(Con_SetActive))
|
|
pCon_SetActive(f);
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("Unsupported xmpp action (%s) in link\n", what);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
qintptr_t JCL_ConExecuteCommand(qintptr_t *args)
|
|
{
|
|
buddy_t *b;
|
|
char consolename[256];
|
|
if (!jclient)
|
|
{
|
|
char buffer[256];
|
|
pCmd_Argv(0, buffer, sizeof(buffer));
|
|
Con_SubPrintf(buffer, "You were disconnected\n");
|
|
return true;
|
|
}
|
|
pCmd_Argv(0, consolename, sizeof(consolename));
|
|
for (b = jclient->buddies; b; b = b->next)
|
|
{
|
|
if (!strcmp(b->name, consolename))
|
|
{
|
|
if (b->defaultresource)
|
|
Q_snprintf(jclient->defaultdest, sizeof(jclient->defaultdest), "%s/%s", b->accountdomain, b->defaultresource->resource);
|
|
else
|
|
Q_snprintf(jclient->defaultdest, sizeof(jclient->defaultdest), "%s", b->accountdomain);
|
|
break;
|
|
}
|
|
}
|
|
JCL_Command(consolename);
|
|
return true;
|
|
}
|
|
|
|
void JCL_FlushOutgoing(jclient_t *jcl)
|
|
{
|
|
int sent;
|
|
if (!jcl || !jcl->outbuflen)
|
|
return;
|
|
|
|
sent = pNet_Send(jcl->socket, jcl->outbuf + jcl->outbufpos, jcl->outbuflen); //FIXME: This needs rewriting to cope with errors.
|
|
if (sent > 0)
|
|
{
|
|
//and print it on some subconsole if we're debugging
|
|
if (jcl->streamdebug)
|
|
{
|
|
char t = jcl->outbuf[jcl->outbufpos+sent];
|
|
jcl->outbuf[jcl->outbufpos+sent] = 0;
|
|
Con_SubPrintf("xmppout", COLOURYELLOW "%s\n", jcl->outbuf + jcl->outbufpos);
|
|
jcl->outbuf[jcl->outbufpos+sent] = t;
|
|
}
|
|
|
|
jcl->outbufpos += sent;
|
|
jcl->outbuflen -= sent;
|
|
}
|
|
// else
|
|
// Con_Printf("Unable to send anything\n");
|
|
}
|
|
void JCL_AddClientMessage(jclient_t *jcl, char *msg, int datalen)
|
|
{
|
|
//handle overflows
|
|
if (jcl->outbufpos+jcl->outbuflen+datalen > jcl->outbufmax)
|
|
{
|
|
if (jcl->outbuflen+datalen <= jcl->outbufmax)
|
|
{
|
|
//can get away with just moving the data
|
|
memmove(jcl->outbuf, jcl->outbuf + jcl->outbufpos, jcl->outbuflen);
|
|
jcl->outbufpos = 0;
|
|
}
|
|
else
|
|
{
|
|
//need to expand the buffer.
|
|
int newmax = (jcl->outbuflen+datalen)*2;
|
|
char *newbuf;
|
|
|
|
if (newmax < jcl->outbuflen)
|
|
newbuf = NULL; //eep... some special kind of evil overflow.
|
|
else
|
|
newbuf = malloc(newmax+1);
|
|
|
|
if (newbuf)
|
|
{
|
|
memcpy(newbuf, jcl->outbuf + jcl->outbufpos, jcl->outbuflen);
|
|
jcl->outbufmax = newmax;
|
|
jcl->outbufpos = 0;
|
|
jcl->outbuf = newbuf;
|
|
}
|
|
else
|
|
datalen = 0; //eep!
|
|
}
|
|
}
|
|
//and write our data to it
|
|
memcpy(jcl->outbuf + jcl->outbufpos + jcl->outbuflen, msg, datalen);
|
|
jcl->outbuflen += datalen;
|
|
|
|
//try and flush it now
|
|
JCL_FlushOutgoing(jcl);
|
|
}
|
|
void JCL_AddClientMessageString(jclient_t *jcl, char *msg)
|
|
{
|
|
JCL_AddClientMessage(jcl, msg, strlen(msg));
|
|
}
|
|
void JCL_AddClientMessagef(jclient_t *jcl, char *fmt, ...)
|
|
{
|
|
va_list argptr;
|
|
char body[2048];
|
|
|
|
va_start (argptr, fmt);
|
|
Q_vsnprintf (body, sizeof(body), fmt, argptr);
|
|
va_end (argptr);
|
|
|
|
JCL_AddClientMessageString(jcl, body);
|
|
}
|
|
qboolean JCL_Reconnect(jclient_t *jcl)
|
|
{
|
|
//destroy any data that never got sent
|
|
free(jcl->outbuf);
|
|
jcl->outbuf = NULL;
|
|
jcl->outbuflen = 0;
|
|
jcl->outbufpos = 0;
|
|
jcl->outbufmax = 0;
|
|
jcl->instreampos = 0;
|
|
jcl->bufferedinammount = 0;
|
|
jcl->tagdepth = 0;
|
|
Q_strlcpy(jcl->localalias, ">>", sizeof(jcl->localalias));
|
|
|
|
|
|
Con_Printf("XMPP: Trying to connect to %s\n", jcl->domain);
|
|
jcl->socket = pNet_TCPConnect(jcl->server, jcl->tlsconnect?5223:5222); //port is only used if the url doesn't contain one. It's a default.
|
|
|
|
//not yet blocking. So no frequent attempts please...
|
|
//non blocking prevents connect from returning worthwhile sensible value.
|
|
if ((int)jcl->socket < 0)
|
|
{
|
|
Con_Printf("JCL_OpenSocket: couldn't connect\n");
|
|
return false;
|
|
}
|
|
|
|
jcl->issecure = false;
|
|
if (jcl->tlsconnect)
|
|
if (pNet_SetTLSClient(jcl->socket, jcl->server)>=0)
|
|
jcl->issecure = true;
|
|
|
|
jcl->status = JCL_AUTHING;
|
|
|
|
JCL_AddClientMessageString(jcl,
|
|
"<?xml version='1.0' ?>"
|
|
"<stream:stream to='");
|
|
JCL_AddClientMessageString(jcl, jcl->domain);
|
|
JCL_AddClientMessageString(jcl, "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");
|
|
|
|
return true;
|
|
}
|
|
jclient_t *JCL_Connect(char *server, qboolean usesecure, char *account, char *password)
|
|
{
|
|
char gamename[64];
|
|
jclient_t *jcl;
|
|
char *domain;
|
|
char *res;
|
|
|
|
res = TrimResourceFromJid(account);
|
|
if (!res)
|
|
{
|
|
//the default resource matches the game that they're trying to play.
|
|
if (pCvar_GetString("fs_gamename", gamename, sizeof(gamename)))
|
|
{
|
|
//strip out any weird chars (namely whitespace)
|
|
char *o;
|
|
for (o = gamename, res = gamename; *res; )
|
|
{
|
|
if (*res == ' ' || *res == '\t')
|
|
res++;
|
|
else
|
|
*o++ = *res++;
|
|
}
|
|
res = gamename;
|
|
}
|
|
}
|
|
|
|
if (usesecure)
|
|
{
|
|
if (!BUILTINISVALID(Net_SetTLSClient))
|
|
{
|
|
Con_Printf("XMPP: TLS is not supported\n");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
domain = strchr(account, '@');
|
|
if (domain)
|
|
{
|
|
*domain = '\0';
|
|
domain++;
|
|
}
|
|
else
|
|
{
|
|
domain = DEFAULTDOMAIN;
|
|
if (domain && *domain)
|
|
Con_Printf("XMPP: domain not specified, assuming %s\n", domain);
|
|
else
|
|
{
|
|
Con_Printf("XMPP: domain not specified\n");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
jcl = malloc(sizeof(jclient_t));
|
|
if (!jcl)
|
|
return NULL;
|
|
|
|
memset(jcl, 0, sizeof(jclient_t));
|
|
|
|
jcl->tlsconnect = usesecure;
|
|
jcl->streamdebug = !!pCvar_GetFloat("xmpp_debug");
|
|
|
|
Q_strlcpy(jcl->server, server, sizeof(jcl->server));
|
|
Q_strlcpy(jcl->username, account, sizeof(jcl->username));
|
|
Q_strlcpy(jcl->domain, domain, sizeof(jcl->domain));
|
|
Q_strlcpy(jcl->password, password, sizeof(jcl->password));
|
|
Q_strlcpy(jcl->resource, (res&&*res)?res:"FTE", sizeof(jcl->password));
|
|
|
|
if (!JCL_Reconnect(jcl))
|
|
{
|
|
free(jcl);
|
|
jcl = NULL;
|
|
}
|
|
|
|
return jcl;
|
|
}
|
|
|
|
char base64[512+1];
|
|
unsigned int base64_len; //current output length
|
|
unsigned int base64_cur; //current pending value
|
|
unsigned int base64_bits;//current pending bits
|
|
char Base64_From64(int byt)
|
|
{
|
|
if (byt >= 0 && byt < 26)
|
|
return 'A' + byt - 0;
|
|
if (byt >= 26 && byt < 52)
|
|
return 'a' + byt - 26;
|
|
if (byt >= 52 && byt < 62)
|
|
return '0' + byt - 52;
|
|
if (byt == 62)
|
|
return '+';
|
|
if (byt == 63)
|
|
return '/';
|
|
return '!';
|
|
}
|
|
void Base64_Byte(unsigned int byt)
|
|
{
|
|
if (base64_len+8>=sizeof(base64)-1)
|
|
return;
|
|
base64_cur |= byt<<(16- base64_bits);//first byte fills highest bits
|
|
base64_bits += 8;
|
|
if (base64_bits == 24)
|
|
{
|
|
base64[base64_len++] = Base64_From64((base64_cur>>18)&63);
|
|
base64[base64_len++] = Base64_From64((base64_cur>>12)&63);
|
|
base64[base64_len++] = Base64_From64((base64_cur>>6)&63);
|
|
base64[base64_len++] = Base64_From64((base64_cur>>0)&63);
|
|
base64[base64_len] = '\0';
|
|
// Con_Printf("base64: %s\n", base64+base64_len-4);
|
|
base64_bits = 0;
|
|
base64_cur = 0;
|
|
}
|
|
}
|
|
|
|
void Base64_Add(char *s, int len)
|
|
{
|
|
unsigned char *us = (unsigned char *)s;
|
|
while(len-->0)
|
|
Base64_Byte(*us++);
|
|
}
|
|
|
|
void Base64_Finish(void)
|
|
{
|
|
//output is always a multiple of four
|
|
|
|
//0(0)->0(0)
|
|
//1(8)->2(12)
|
|
//2(16)->3(18)
|
|
//3(24)->4(24)
|
|
|
|
if (base64_bits != 0)
|
|
{
|
|
base64[base64_len++]=Base64_From64((base64_cur>>18)&63);
|
|
base64[base64_len++]=Base64_From64((base64_cur>>12)&63);
|
|
if (base64_bits == 8)
|
|
{
|
|
base64[base64_len++]= '=';
|
|
base64[base64_len++]= '=';
|
|
}
|
|
else
|
|
{
|
|
base64[base64_len++]=Base64_From64((base64_cur>>6)&63);
|
|
if (base64_bits == 16)
|
|
base64[base64_len++]= '=';
|
|
else
|
|
base64[base64_len++]=Base64_From64((base64_cur>>0)&63);
|
|
}
|
|
}
|
|
base64[base64_len++] = '\0';
|
|
|
|
base64_len = 0; //for next time (use strlen)
|
|
base64_bits = 0;
|
|
base64_cur = 0;
|
|
}
|
|
|
|
//decode a base64 byte to a 0-63 value. Cannot cope with =.
|
|
static int Base64_DecodeByte(char byt)
|
|
{
|
|
if (byt >= 'A' && byt <= 'Z')
|
|
return (byt-'A') + 0;
|
|
if (byt >= 'a' && byt <= 'z')
|
|
return (byt-'a') + 26;
|
|
if (byt >= '0' && byt <= '9')
|
|
return (byt-'0') + 52;
|
|
if (byt == '+')
|
|
return 62;
|
|
if (byt == '/')
|
|
return 63;
|
|
return -1;
|
|
}
|
|
int Base64_Decode(char *out, int outlen, char *src, int srclen)
|
|
{
|
|
int len = 0;
|
|
int result;
|
|
|
|
//4 input chars give 3 output chars
|
|
while(srclen >= 4)
|
|
{
|
|
if (len+3 > outlen)
|
|
break;
|
|
result = Base64_DecodeByte(src[0])<<18;
|
|
result |= Base64_DecodeByte(src[1])<<12;
|
|
out[len++] = (result>>16)&0xff;
|
|
if (src[2] != '=')
|
|
{
|
|
result |= Base64_DecodeByte(src[2])<<6;
|
|
out[len++] = (result>>8)&0xff;
|
|
if (src[3] != '=')
|
|
{
|
|
result |= Base64_DecodeByte(src[3])<<0;
|
|
out[len++] = (result>>0)&0xff;
|
|
}
|
|
}
|
|
if (result & 0xff000000)
|
|
return 0; //some kind of invalid char
|
|
|
|
src += 4;
|
|
srclen -= 4;
|
|
}
|
|
|
|
//some kind of error
|
|
if (srclen)
|
|
return 0;
|
|
|
|
return len;
|
|
}
|
|
|
|
void JCL_ForgetBuddyResource(jclient_t *jcl, buddy_t *buddy, bresource_t *bres)
|
|
{
|
|
bresource_t **link;
|
|
bresource_t *r;
|
|
for (link = &buddy->resources; *link; )
|
|
{
|
|
r = *link;
|
|
if (!bres || bres == r)
|
|
{
|
|
*link = r->next;
|
|
free(r);
|
|
if (bres)
|
|
break;
|
|
}
|
|
else
|
|
link = &r->next;
|
|
}
|
|
}
|
|
void JCL_ForgetBuddy(jclient_t *jcl, buddy_t *buddy, bresource_t *bres)
|
|
{
|
|
buddy_t **link;
|
|
buddy_t *b;
|
|
for (link = &jcl->buddies; *link; )
|
|
{
|
|
b = *link;
|
|
if (!buddy || buddy == b)
|
|
{
|
|
*link = b->next;
|
|
JCL_ForgetBuddyResource(jcl, b, bres);
|
|
free(b);
|
|
if (buddy)
|
|
break;
|
|
}
|
|
else
|
|
link = &b->next;
|
|
}
|
|
}
|
|
|
|
qboolean JCL_FindBuddy(jclient_t *jcl, char *jid, buddy_t **buddy, bresource_t **bres)
|
|
{
|
|
char name[256];
|
|
char *res;
|
|
buddy_t *b;
|
|
bresource_t *r = NULL;
|
|
|
|
Q_strlcpy(name, jid, sizeof(name));
|
|
res = TrimResourceFromJid(name);
|
|
|
|
for (b = jcl->buddies; b; b = b->next)
|
|
{
|
|
if (!strcmp(b->accountdomain, name))
|
|
break;
|
|
}
|
|
if (!b)
|
|
{
|
|
b = malloc(sizeof(*b) + strlen(name));
|
|
memset(b, 0, sizeof(*b));
|
|
b->next = jcl->buddies;
|
|
jcl->buddies = b;
|
|
strcpy(b->accountdomain, name);
|
|
Q_strlcpy(b->name, name, sizeof(b->name)); //default
|
|
}
|
|
*buddy = b;
|
|
if (res && bres)
|
|
{
|
|
for (r = b->resources; r; r = r->next)
|
|
{
|
|
if (!strcmp(r->resource, res))
|
|
break;
|
|
}
|
|
if (!r)
|
|
{
|
|
r = malloc(sizeof(*r) + strlen(res));
|
|
memset(r, 0, sizeof(*r));
|
|
r->next = b->resources;
|
|
b->resources = r;
|
|
strcpy(r->resource, res);
|
|
}
|
|
*bres = r;
|
|
}
|
|
else if (bres)
|
|
*bres = NULL;
|
|
return false;
|
|
}
|
|
|
|
struct iq_s *JCL_SendIQ(jclient_t *jcl, qboolean (*callback) (jclient_t *jcl, xmltree_t *tree, struct iq_s *iq), char *iqtype, char *target, char *body)
|
|
{
|
|
struct iq_s *iq;
|
|
|
|
iq = malloc(sizeof(*iq));
|
|
iq->next = jcl->pendingiqs;
|
|
jcl->pendingiqs = iq;
|
|
Q_snprintf(iq->id, sizeof(iq->id), "%i", rand());
|
|
iq->callback = callback;
|
|
|
|
if (target)
|
|
{
|
|
if (*jcl->jid)
|
|
JCL_AddClientMessagef(jcl, "<iq type='%s' id='%s' from='%s' to='%s'>", iqtype, iq->id, jcl->jid, target);
|
|
else
|
|
JCL_AddClientMessagef(jcl, "<iq type='%s' id='%s' to='%s'>", iqtype, iq->id, target);
|
|
}
|
|
else
|
|
{
|
|
if (*jcl->jid)
|
|
JCL_AddClientMessagef(jcl, "<iq type='%s' id='%s' from='%s'>", iqtype, iq->id, jcl->jid);
|
|
else
|
|
JCL_AddClientMessagef(jcl, "<iq type='%s' id='%s'>", iqtype, iq->id);
|
|
}
|
|
JCL_AddClientMessageString(jcl, body);
|
|
JCL_AddClientMessageString(jcl, "</iq>");
|
|
return iq;
|
|
}
|
|
struct iq_s *JCL_SendIQf(jclient_t *jcl, qboolean (*callback) (jclient_t *jcl, xmltree_t *tree, struct iq_s *iq), char *iqtype, char *target, char *fmt, ...)
|
|
{
|
|
va_list argptr;
|
|
char body[2048];
|
|
|
|
va_start (argptr, fmt);
|
|
Q_vsnprintf (body, sizeof(body), fmt, argptr);
|
|
va_end (argptr);
|
|
|
|
return JCL_SendIQ(jcl, callback, iqtype, target, body);
|
|
}
|
|
struct iq_s *JCL_SendIQNode(jclient_t *jcl, qboolean (*callback) (jclient_t *jcl, xmltree_t *tree, struct iq_s *iq), char *iqtype, char *target, xmltree_t *node, qboolean destroynode)
|
|
{
|
|
struct iq_s *n;
|
|
char *s = XML_GenerateString(node);
|
|
n = JCL_SendIQ(jcl, callback, iqtype, target, s);
|
|
free(s);
|
|
if (destroynode)
|
|
XML_Destroy(node);
|
|
return n;
|
|
}
|
|
static void JCL_RosterUpdate(jclient_t *jcl, xmltree_t *listp)
|
|
{
|
|
xmltree_t *i;
|
|
buddy_t *buddy;
|
|
int cnum = 0;
|
|
while ((i = XML_ChildOfTree(listp, "item", cnum++)))
|
|
{
|
|
char *name = XML_GetParameter(i, "name", "");
|
|
char *jid = XML_GetParameter(i, "jid", "");
|
|
// char *sub = XML_GetParameter(i, "subscription", "");
|
|
JCL_FindBuddy(jcl, jid, &buddy, NULL);
|
|
|
|
if (*name)
|
|
Q_strlcpy(buddy->name, name, sizeof(buddy->name));
|
|
buddy->friended = true;
|
|
}
|
|
}
|
|
static qboolean JCL_RosterReply(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
xmltree_t *c;
|
|
//we're probably connected once we've had this reply.
|
|
jcl->status = JCL_ACTIVE;
|
|
JCL_GeneratePresence(jcl, true);
|
|
c = XML_ChildOfTree(tree, "query", 0);
|
|
if (c)
|
|
{
|
|
JCL_RosterUpdate(jcl, c);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static qboolean JCL_BindReply(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
xmltree_t *c;
|
|
c = XML_ChildOfTree(tree, "bind", 0);
|
|
if (c)
|
|
{
|
|
c = XML_ChildOfTree(c, "jid", 0);
|
|
if (c)
|
|
{
|
|
Q_strlcpy(jcl->jid, c->body, sizeof(jcl->jid));
|
|
Con_Printf("Bound to jid ^[[%s]\\xmpp\\%s^]\n", jcl->jid, jcl->jid);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
static qboolean JCL_VCardReply(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
xmltree_t *vc, *fn, *nickname;
|
|
vc = XML_ChildOfTree(tree, "vCard", 0);
|
|
fn = XML_ChildOfTree(vc, "FN", 0);
|
|
nickname = XML_ChildOfTree(vc, "NICKNAME", 0);
|
|
|
|
if (nickname && *nickname->body)
|
|
Q_strlcpy(jcl->localalias, nickname->body, sizeof(jcl->localalias));
|
|
else if (fn && *fn->body)
|
|
Q_strlcpy(jcl->localalias, fn->body, sizeof(jcl->localalias));
|
|
return true;
|
|
}
|
|
static qboolean JCL_SessionReply(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
JCL_SendIQf(jcl, JCL_RosterReply, "get", NULL, "<query xmlns='jabber:iq:roster'/>");
|
|
JCL_SendIQf(jcl, JCL_VCardReply, "get", NULL, "<vCard xmlns='vcard-temp'/>");
|
|
return true;
|
|
}
|
|
|
|
|
|
static char *caps[] =
|
|
{
|
|
#if 1
|
|
"http://jabber.org/protocol/caps",
|
|
"http://jabber.org/protocol/disco#info",
|
|
// "http://jabber.org/protocol/disco#items",
|
|
|
|
// "http://www.google.com/xmpp/protocol/camera/v1",
|
|
// "http://www.google.com/xmpp/protocol/session",
|
|
// "http://www.google.com/xmpp/protocol/voice/v1",
|
|
// "http://www.google.com/xmpp/protocol/video/v1",
|
|
|
|
"jabber:iq:version",
|
|
"urn:xmpp:jingle:1",
|
|
QUAKEMEDIAXMLNS,
|
|
#ifdef VOIP
|
|
"urn:xmpp:jingle:apps:rtp:1",
|
|
"urn:xmpp:jingle:apps:rtp:audio",
|
|
#endif
|
|
// "urn:xmpp:jingle:apps:rtp:video",//we don't support rtp video chat
|
|
"urn:xmpp:jingle:transports:raw-udp:1",
|
|
#ifndef NOICE
|
|
"urn:xmpp:jingle:transports:ice-udp:1",
|
|
#endif
|
|
#ifndef Q3_VM
|
|
"urn:xmpp:time",
|
|
#endif
|
|
"urn:xmpp:ping", //FIXME: I'm not keen on this. I only added support to stop errors from pidgin when trying to debug.
|
|
"urn:xmpp:attention:0", //poke.
|
|
|
|
#else
|
|
//for testing, this is the list of features pidgin supports (which is the other client I'm testing against).
|
|
|
|
"jabber:iq:last",
|
|
"jabber:iq:oob",
|
|
"urn:xmpp:time",
|
|
"jabber:iq:version",
|
|
"jabber:x:conference",
|
|
"http://jabber.org/protocol/bytestreams",
|
|
"http://jabber.org/protocol/caps",
|
|
"http://jabber.org/protocol/chatstates",
|
|
"http://jabber.org/protocol/disco#info",
|
|
"http://jabber.org/protocol/disco#items",
|
|
"http://jabber.org/protocol/muc",
|
|
"http://jabber.org/protocol/muc#user",
|
|
"http://jabber.org/protocol/si",
|
|
// "http://jabber.org/protocol/si/profile/file-transfer",
|
|
"http://jabber.org/protocol/xhtml-im",
|
|
"urn:xmpp:ping",
|
|
"urn:xmpp:attention:0",
|
|
"urn:xmpp:bob",
|
|
"urn:xmpp:jingle:1",
|
|
"http://www.google.com/xmpp/protocol/session",
|
|
"http://www.google.com/xmpp/protocol/voice/v1",
|
|
"http://www.google.com/xmpp/protocol/video/v1",
|
|
"http://www.google.com/xmpp/protocol/camera/v1",
|
|
"urn:xmpp:jingle:apps:rtp:1",
|
|
"urn:xmpp:jingle:apps:rtp:audio",
|
|
"urn:xmpp:jingle:apps:rtp:video",
|
|
"urn:xmpp:jingle:transports:raw-udp:1",
|
|
"urn:xmpp:jingle:transports:ice-udp:1",
|
|
"urn:xmpp:avatar:metadata",
|
|
"urn:xmpp:avatar:data",
|
|
"urn:xmpp:avatar:metadata+notify",
|
|
"http://jabber.org/protocol/mood",
|
|
"http://jabber.org/protocol/mood+notify",
|
|
"http://jabber.org/protocol/tune",
|
|
"http://jabber.org/protocol/tune+notify",
|
|
"http://jabber.org/protocol/nick",
|
|
"http://jabber.org/protocol/nick+notify",
|
|
"http://jabber.org/protocol/ibb",
|
|
#endif
|
|
NULL
|
|
};
|
|
static void buildcaps(char *out, int outlen)
|
|
{
|
|
int i;
|
|
Q_strncpyz(out, "<identity category='client' type='pc' name='FTEQW'/>", outlen);
|
|
|
|
for (i = 0; caps[i]; i++)
|
|
{
|
|
Q_strlcat(out, "<feature var='", outlen);
|
|
Q_strlcat(out, caps[i], outlen);
|
|
Q_strlcat(out, "'/>", outlen);
|
|
}
|
|
}
|
|
static int qsortcaps(const void *va, const void *vb)
|
|
{
|
|
char *a = *(char**)va;
|
|
char *b = *(char**)vb;
|
|
return strcmp(a, b);
|
|
}
|
|
int SHA1(char *digest, int maxdigestsize, char *string, int stringlen);
|
|
char *buildcapshash(void)
|
|
{
|
|
int i, l;
|
|
char out[8192];
|
|
int outlen = sizeof(out);
|
|
unsigned char digest[64];
|
|
Q_strlcpy(out, "client/pc//FTEQW<", outlen);
|
|
qsort(caps, sizeof(caps)/sizeof(caps[0]) - 1, sizeof(char*), qsortcaps);
|
|
for (i = 0; caps[i]; i++)
|
|
{
|
|
Q_strlcat(out, caps[i], outlen);
|
|
Q_strlcat(out, "<", outlen);
|
|
}
|
|
l = SHA1(digest, sizeof(digest), out, strlen(out));
|
|
for (i = 0; i < l; i++)
|
|
Base64_Byte(digest[i]);
|
|
Base64_Finish();
|
|
return base64;
|
|
}
|
|
|
|
void JCL_ParseIQ(jclient_t *jcl, xmltree_t *tree)
|
|
{
|
|
qboolean unparsable = true;
|
|
char *from;
|
|
// char *to;
|
|
char *id;
|
|
char *f;
|
|
xmltree_t *ot;
|
|
|
|
//FIXME: block from people who we don't know.
|
|
|
|
id = XML_GetParameter(tree, "id", "");
|
|
from = XML_GetParameter(tree, "from", "");
|
|
// to = XML_GetParameter(tree, "to", "");
|
|
|
|
f = XML_GetParameter(tree, "type", "");
|
|
if (!strcmp(f, "get"))
|
|
{
|
|
ot = XML_ChildOfTree(tree, "query", 0);
|
|
if (ot)
|
|
{
|
|
if (from && !strcmp(ot->xmlns, "http://jabber.org/protocol/disco#info"))
|
|
{ //http://xmpp.org/extensions/xep-0030.html
|
|
char msg[2048];
|
|
char *hash;
|
|
unparsable = false;
|
|
|
|
buildcaps(msg, sizeof(msg));
|
|
hash = buildcapshash();
|
|
|
|
JCL_AddClientMessagef(jcl,
|
|
"<iq type='result' to='%s' id='%s'>"
|
|
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://fteqw.com/ftexmppplug#%s'>"
|
|
"%s"
|
|
"</query>"
|
|
"</iq>", from, id, hash, msg);
|
|
}
|
|
else if (from && !strcmp(ot->xmlns, "jabber:iq:version"))
|
|
{ //client->client version request
|
|
char msg[2048];
|
|
unparsable = false;
|
|
|
|
Q_snprintf(msg, sizeof(msg),
|
|
"<iq type='result' to='%s' id='%s'>"
|
|
"<query xmlns='jabber:iq:version'>"
|
|
"<name>FTEQW XMPP</name>"
|
|
"<version>V"JCL_BUILD"</version>"
|
|
#ifdef Q3_VM
|
|
"<os>QVM plugin</os>"
|
|
#else
|
|
//don't specify the os otherwise, as it gives away required base addresses etc for exploits
|
|
#endif
|
|
"</query>"
|
|
"</iq>", from, id);
|
|
|
|
JCL_AddClientMessageString(jcl, msg);
|
|
}
|
|
else if (from && !strcmp(ot->xmlns, "jabber:iq:last"))
|
|
{
|
|
unparsable = false;
|
|
JCL_AddClientMessagef(jcl,
|
|
"<iq type='error' to='%s' id='%s'>"
|
|
"<error type='cancel'>"
|
|
"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
|
|
"</error>"
|
|
"</iq>", from, id);
|
|
}
|
|
/* else if (from && !strcmp(ot->xmlns, "jabber:iq:last"))
|
|
{ //http://xmpp.org/extensions/xep-0012.html
|
|
char msg[2048];
|
|
int idletime = 0;
|
|
unparsable = false;
|
|
|
|
//last activity
|
|
Q_snprintf(msg, sizeof(msg),
|
|
"<iq type='result' to='%s' id='%s'>"
|
|
"<query xmlns='jabber:iq:last' seconds='%i'/>"
|
|
"</iq>", from, id, idletime);
|
|
|
|
JCL_AddClientMessageString(jcl, msg);
|
|
}
|
|
*/
|
|
}
|
|
#ifndef Q3_VM
|
|
ot = XML_ChildOfTree(tree, "time", 0);
|
|
if (ot && !strcmp(ot->xmlns, "urn:xmpp:time"))
|
|
{ //http://xmpp.org/extensions/xep-0202.html
|
|
char msg[2048];
|
|
char tz[256];
|
|
char timestamp[256];
|
|
struct tm * timeinfo;
|
|
int tzh, tzm;
|
|
time_t rawtime;
|
|
time (&rawtime);
|
|
timeinfo = localtime(&rawtime);
|
|
tzh = timeinfo->tm_hour;
|
|
tzm = timeinfo->tm_min;
|
|
timeinfo = gmtime (&rawtime);
|
|
tzh -= timeinfo->tm_hour;
|
|
tzm -= timeinfo->tm_min;
|
|
Q_snprintf(tz, sizeof(tz), "%+i:%i", tzh, tzm);
|
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", timeinfo);
|
|
unparsable = false;
|
|
//strftime
|
|
Q_snprintf(msg, sizeof(msg),
|
|
"<iq type='result' to='%s' id='%s'>"
|
|
"<time xmlns='urn:xmpp:time'>"
|
|
"<tzo>%s</tzo>"
|
|
"<utc>%s</utc>"
|
|
"</time>"
|
|
"</iq>", from, id, tz, timestamp);
|
|
JCL_AddClientMessageString(jcl, msg);
|
|
}
|
|
#endif
|
|
|
|
ot = XML_ChildOfTree(tree, "ping", 0);
|
|
if (ot && !strcmp(ot->xmlns, "urn:xmpp:ping"))
|
|
{
|
|
JCL_AddClientMessagef(jcl, "<iq type='result' to='%s' id='%s' />", from, id);
|
|
}
|
|
|
|
if (unparsable)
|
|
{ //unsupported stuff
|
|
char msg[2048];
|
|
unparsable = false;
|
|
|
|
Con_Printf("Unsupported iq get\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
|
|
//tell them OH NOES, instead of requiring some timeout.
|
|
Q_snprintf(msg, sizeof(msg),
|
|
"<iq type='error' to='%s' id='%s'>"
|
|
"<error type='cancel'>"
|
|
"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
|
|
"</error>"
|
|
"</iq>", from, id);
|
|
JCL_AddClientMessageString(jcl, msg);
|
|
}
|
|
}
|
|
else if (!strcmp(f, "set"))
|
|
{
|
|
xmltree_t *c;
|
|
|
|
c = XML_ChildOfTree(tree, "query", 0);
|
|
if (c && !strcmp(c->xmlns, "jabber:iq:roster"))
|
|
{
|
|
unparsable = false;
|
|
JCL_RosterUpdate(jcl, c);
|
|
}
|
|
|
|
c = XML_ChildOfTree(tree, "jingle", 0);
|
|
if (c && !strcmp(c->xmlns, "urn:xmpp:jingle:1"))
|
|
{
|
|
unparsable = !JCL_ParseJingle(jcl, c, from, id);
|
|
}
|
|
|
|
if (unparsable)
|
|
{
|
|
char msg[2048];
|
|
//tell them OH NOES, instead of requiring some timeout.
|
|
Q_snprintf(msg, sizeof(msg),
|
|
"<iq type='error' to='%s' id='%s'>"
|
|
"<error type='cancel'>"
|
|
"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
|
|
"</error>"
|
|
"</iq>", from, id);
|
|
JCL_AddClientMessageString(jcl, msg);
|
|
unparsable = false;
|
|
}
|
|
}
|
|
else if (!strcmp(f, "result") || !strcmp(f, "error"))
|
|
{
|
|
char *id = XML_GetParameter(tree, "id", "");
|
|
struct iq_s **link, *iq;
|
|
unparsable = false;
|
|
for (link = &jcl->pendingiqs; *link; link = &(*link)->next)
|
|
{
|
|
iq = *link;
|
|
if (!strcmp(iq->id, id))
|
|
break;
|
|
}
|
|
if (*link)
|
|
{
|
|
iq = *link;
|
|
*link = iq->next;
|
|
|
|
if (iq->callback)
|
|
{
|
|
if (!iq->callback(jcl, !strcmp(f, "error")?NULL:tree, iq))
|
|
{
|
|
Con_Printf("Invalid iq result\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
}
|
|
free(iq);
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("Unrecognised iq result\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
}
|
|
|
|
if (unparsable)
|
|
{
|
|
unparsable = false;
|
|
Con_Printf("Unrecognised iq type\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
}
|
|
void JCL_ParseMessage(jclient_t *jcl, xmltree_t *tree)
|
|
{
|
|
xmltree_t *ot;
|
|
qboolean unparsable = true;
|
|
char *f = XML_GetParameter(tree, "from", NULL);
|
|
|
|
if (f && !strcmp(f, jcl->jid))
|
|
unparsable = false;
|
|
else
|
|
{
|
|
if (f)
|
|
{
|
|
buddy_t *b;
|
|
bresource_t *br;
|
|
Q_strlcpy(jcl->defaultdest, f, sizeof(jcl->defaultdest));
|
|
|
|
JCL_FindBuddy(jcl, f, &b, &br);
|
|
f = b->name;
|
|
b->defaultresource = br;
|
|
}
|
|
|
|
if (f)
|
|
{
|
|
ot = XML_ChildOfTree(tree, "composing", 0);
|
|
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
|
|
{
|
|
unparsable = false;
|
|
Con_SubPrintf(f, "%s is typing\r", f);
|
|
}
|
|
ot = XML_ChildOfTree(tree, "paused", 0);
|
|
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
|
|
{
|
|
unparsable = false;
|
|
Con_SubPrintf(f, "%s has stopped typing\r", f);
|
|
}
|
|
ot = XML_ChildOfTree(tree, "inactive", 0);
|
|
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
|
|
{
|
|
unparsable = false;
|
|
Con_SubPrintf(f, "\r", f);
|
|
}
|
|
ot = XML_ChildOfTree(tree, "active", 0);
|
|
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
|
|
{
|
|
unparsable = false;
|
|
Con_SubPrintf(f, "\r", f);
|
|
}
|
|
ot = XML_ChildOfTree(tree, "gone", 0);
|
|
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
|
|
{
|
|
unparsable = false;
|
|
Con_SubPrintf(f, "%s has gone away\r", f);
|
|
}
|
|
}
|
|
|
|
ot = XML_ChildOfTree(tree, "attention", 0);
|
|
if (ot)
|
|
{
|
|
Con_SubPrintf(f, "%s is an attention whore.\n", f);
|
|
pCon_SetActive(f);
|
|
if (BUILTINISVALID(LocalSound))
|
|
pLocalSound("misc/talk.wav");
|
|
}
|
|
|
|
ot = XML_ChildOfTree(tree, "body", 0);
|
|
if (ot)
|
|
{
|
|
unparsable = false;
|
|
if (f)
|
|
{
|
|
if (!strncmp(ot->body, "/me ", 4))
|
|
Con_SubPrintf(f, "*^2%s^7%s\n", f, ot->body+3);
|
|
else
|
|
Con_SubPrintf(f, "^2%s^7: %s\n", f, ot->body);
|
|
}
|
|
else
|
|
Con_Printf("NOTICE: %s\n", ot->body);
|
|
|
|
if (BUILTINISVALID(LocalSound))
|
|
pLocalSound("misc/talk.wav");
|
|
}
|
|
|
|
if (unparsable)
|
|
{
|
|
unparsable = false;
|
|
if (jcl->streamdebug)
|
|
{
|
|
Con_Printf("Received a message without a body\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean JCL_ClientDiscoInfo(jclient_t *jcl, xmltree_t *tree, struct iq_s *iq)
|
|
{
|
|
xmltree_t *query = XML_ChildOfTree(tree, "query", 0);
|
|
xmltree_t *feature;
|
|
char *var;
|
|
int i = 0;
|
|
unsigned int caps = 0;
|
|
qboolean rtp = false;
|
|
qboolean rtpaudio = false;
|
|
qboolean quake = false;
|
|
qboolean ice = false;
|
|
qboolean raw = false;
|
|
qboolean jingle = false;
|
|
buddy_t *b;
|
|
bresource_t *r;
|
|
JCL_FindBuddy(jcl, XML_GetParameter(tree, "from", ""), &b, &r);
|
|
while((feature = XML_ChildOfTree(query, "feature", i++)))
|
|
{
|
|
var = XML_GetParameter(feature, "var", "");
|
|
//check ones we recognise.
|
|
if (!strcmp(var, QUAKEMEDIAXMLNS))
|
|
quake = true;
|
|
if (!strcmp(var, "urn:xmpp:jingle:apps:rtp:audio"))
|
|
rtpaudio = true;
|
|
if (!strcmp(var, "urn:xmpp:jingle:apps:rtp:1"))
|
|
rtp = true; //kinda implied, but ensures version is okay
|
|
if (!strcmp(var, "urn:xmpp:jingle:transports:ice-udp:1"))
|
|
ice = true;
|
|
if (!strcmp(var, "urn:xmpp:jingle:transports:raw-udp:1"))
|
|
raw = true;
|
|
if (!strcmp(var, "urn:xmpp:jingle:1"))
|
|
jingle = true; //kinda implied, but ensures version is okay
|
|
}
|
|
if ((ice||raw) && jingle)
|
|
{
|
|
if (rtpaudio && rtp)
|
|
caps |= CAP_VOICE;
|
|
if (quake)
|
|
caps |= CAP_INVITE;
|
|
}
|
|
|
|
if (b && r)
|
|
r->caps = (r->caps & CAP_QUERIED) | caps;
|
|
return true;
|
|
}
|
|
void JCL_ParsePresence(jclient_t *jcl, xmltree_t *tree)
|
|
{
|
|
buddy_t *buddy;
|
|
bresource_t *bres;
|
|
|
|
char *from = XML_GetParameter(tree, "from", "");
|
|
xmltree_t *show = XML_ChildOfTree(tree, "show", 0);
|
|
xmltree_t *status = XML_ChildOfTree(tree, "status", 0);
|
|
xmltree_t *quake = XML_ChildOfTree(tree, "quake", 0);
|
|
char *type = XML_GetParameter(tree, "type", "");
|
|
char *serverip = NULL;
|
|
char *servermap = NULL;
|
|
|
|
if (quake && !strcmp(quake->xmlns, "fteqw.com:game"))
|
|
{
|
|
serverip = XML_GetParameter(quake, "serverip", NULL);
|
|
servermap = XML_GetParameter(quake, "servermap", NULL);
|
|
}
|
|
|
|
if (type && !strcmp(type, "subscribe"))
|
|
{
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] wants to be your friend! ^[[Authorize]\\xmpp\\%s\\xmppact\\pauth^] ^[[Deny]\\xmpp\\%s\\xmppact\\pdeny^]\n", from, from, from, from);
|
|
}
|
|
else if (type && !strcmp(type, "subscribed"))
|
|
{
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is now your friend!\n", from, from, from, from);
|
|
}
|
|
else if (type && !strcmp(type, "unsubscribe"))
|
|
{
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] has unfriended you\n", from, from);
|
|
}
|
|
else if (type && !strcmp(type, "unsubscribed"))
|
|
{
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is no longer unfriended you\n", from, from);
|
|
}
|
|
else
|
|
{
|
|
JCL_FindBuddy(jcl, from, &buddy, &bres);
|
|
|
|
if (bres)
|
|
{
|
|
if (servermap)
|
|
{
|
|
bres->servertype = 2;
|
|
Q_strlcpy(bres->server, servermap, sizeof(bres->server));
|
|
}
|
|
else if (serverip)
|
|
{
|
|
bres->servertype = 1;
|
|
Q_strlcpy(bres->server, serverip, sizeof(bres->server));
|
|
}
|
|
else
|
|
{
|
|
bres->servertype = 0;
|
|
Q_strlcpy(bres->server, "", sizeof(bres->server));
|
|
}
|
|
Q_strlcpy(bres->fstatus, (status && *status->body)?status->body:"", sizeof(bres->fstatus));
|
|
if (!tree->child)
|
|
{
|
|
Q_strlcpy(bres->bstatus, "offline", sizeof(bres->bstatus));
|
|
bres->caps = 0;
|
|
}
|
|
else
|
|
{
|
|
Q_strlcpy(bres->bstatus, (show && *show->body)?show->body:"present", sizeof(bres->bstatus));
|
|
if (!(bres->caps & CAP_QUERIED))
|
|
{
|
|
bres->caps |= CAP_QUERIED;
|
|
JCL_SendIQ(jcl, JCL_ClientDiscoInfo, "get", from, "<query xmlns='http://jabber.org/protocol/disco#info'/>");
|
|
}
|
|
}
|
|
|
|
if (bres->servertype == 2)
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is now ^[[Playing Quake - %s]\\xmpp\\%s\\xmppact\\join^]\n", buddy->name, from, bres->server, from);
|
|
else if (bres->servertype == 1)
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is now ^[[Playing Quake - %s]\\observe\\%s^]\n", buddy->name, from, bres->server, bres->server);
|
|
else if (*bres->fstatus)
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is now %s: %s\n", buddy->name, from, bres->bstatus, bres->fstatus);
|
|
else
|
|
Con_Printf("^[[%s]\\xmpp\\%s^] is now %s\n", buddy->name, from, bres->bstatus);
|
|
|
|
if (!tree->child)
|
|
{
|
|
//remove this buddy resource
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("Weird presence:\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#define JCL_DONE 0 //no more data available for now.
|
|
#define JCL_CONTINUE 1 //more data needs parsing.
|
|
#define JCL_KILL 2 //some error, needs reconnecting.
|
|
#define JCL_NUKEFROMORBIT 3 //permanent error (or logged on from elsewhere)
|
|
int JCL_ClientFrame(jclient_t *jcl)
|
|
{
|
|
int pos;
|
|
xmltree_t *tree, *ot;
|
|
char *f;
|
|
int ret;
|
|
qboolean unparsable;
|
|
|
|
int olddepth;
|
|
|
|
ret = pNet_Recv(jcl->socket, jcl->bufferedinmessage+jcl->bufferedinammount, sizeof(jcl->bufferedinmessage)-1 - jcl->bufferedinammount);
|
|
if (ret == 0)
|
|
{
|
|
if (!jcl->bufferedinammount) //if we are half way through a message, read any possible conjunctions.
|
|
return JCL_DONE; //nothing more this frame
|
|
}
|
|
if (ret < 0)
|
|
{
|
|
Con_Printf("XMPP: socket error\n");
|
|
return JCL_KILL;
|
|
}
|
|
|
|
if (ret>0)
|
|
{
|
|
jcl->bufferedinammount+=ret;
|
|
jcl->bufferedinmessage[jcl->bufferedinammount] = 0;
|
|
}
|
|
|
|
olddepth = jcl->tagdepth;
|
|
|
|
//we never end parsing in the middle of a < >
|
|
//this means we can filter out the <? ?>, <!-- --> and < /> stuff properly
|
|
for (pos = jcl->instreampos; pos < jcl->bufferedinammount; pos++)
|
|
{
|
|
if (jcl->bufferedinmessage[pos] == '<')
|
|
{
|
|
jcl->instreampos = pos;
|
|
}
|
|
else if (jcl->bufferedinmessage[pos] == '>')
|
|
{
|
|
if (pos < 1)
|
|
break; //erm...
|
|
|
|
if (jcl->bufferedinmessage[pos-1] != '/') //<blah/> is a tag without a body
|
|
{
|
|
if (jcl->bufferedinmessage[jcl->instreampos+1] != '?') //<? blah ?> is a tag without a body
|
|
{
|
|
if (jcl->bufferedinmessage[pos-1] != '?')
|
|
{
|
|
if (jcl->bufferedinmessage[jcl->instreampos+1] == '/') //</blah> is the end of a tag with a body
|
|
jcl->tagdepth--;
|
|
else
|
|
jcl->tagdepth++; //<blah> is the start of a tag with a body
|
|
}
|
|
}
|
|
}
|
|
|
|
jcl->instreampos=pos+1;
|
|
}
|
|
}
|
|
|
|
if (jcl->tagdepth == 1 && olddepth == 0)
|
|
{ //first bit of info
|
|
|
|
pos = 0;
|
|
tree = XML_Parse(jcl->bufferedinmessage, &pos, jcl->instreampos, true, "");
|
|
while (tree && !strcmp(tree->name, "?xml"))
|
|
{
|
|
XML_Destroy(tree);
|
|
tree = XML_Parse(jcl->bufferedinmessage, &pos, jcl->instreampos, true, "");
|
|
}
|
|
|
|
if (jcl->streamdebug)
|
|
{
|
|
char t = jcl->bufferedinmessage[pos];
|
|
jcl->bufferedinmessage[pos] = 0;
|
|
Con_TrySubPrint("xmppin", jcl->bufferedinmessage);
|
|
if (tree)
|
|
Con_TrySubPrint("xmppin", "\n");
|
|
jcl->bufferedinmessage[pos] = t;
|
|
}
|
|
|
|
if (!tree)
|
|
{
|
|
Con_Printf("Not an xml stream\n");
|
|
return JCL_KILL;
|
|
}
|
|
if (strcmp(tree->name, "stream") || strcmp(tree->xmlns, "http://etherx.jabber.org/streams"))
|
|
{
|
|
Con_Printf("Not an xmpp stream\n");
|
|
return JCL_KILL;
|
|
}
|
|
Q_strlcpy(jcl->defaultnamespace, tree->xmlns_dflt, sizeof(jcl->defaultnamespace));
|
|
|
|
ot = tree;
|
|
tree = tree->child;
|
|
ot->child = NULL;
|
|
|
|
// Con_Printf("Discard\n");
|
|
// XML_ConPrintTree(ot, 0);
|
|
XML_Destroy(ot);
|
|
|
|
if (!tree)
|
|
{
|
|
memmove(jcl->bufferedinmessage, jcl->bufferedinmessage+pos, jcl->bufferedinammount - (pos));
|
|
jcl->bufferedinammount-=pos;
|
|
jcl->instreampos-=pos;
|
|
|
|
return JCL_DONE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (jcl->tagdepth != 1)
|
|
{
|
|
if (jcl->tagdepth < 1 && jcl->bufferedinammount==jcl->instreampos)
|
|
{
|
|
Con_Printf("End of XML stream\n");
|
|
return JCL_KILL;
|
|
}
|
|
return JCL_DONE;
|
|
}
|
|
|
|
pos = 0;
|
|
tree = XML_Parse(jcl->bufferedinmessage, &pos, jcl->instreampos, false, jcl->defaultnamespace);
|
|
|
|
if (jcl->streamdebug)
|
|
{
|
|
char t = jcl->bufferedinmessage[pos];
|
|
jcl->bufferedinmessage[pos] = 0;
|
|
Con_TrySubPrint("xmppin", jcl->bufferedinmessage);
|
|
Con_TrySubPrint("xmppin", "\n");
|
|
jcl->bufferedinmessage[pos] = t;
|
|
}
|
|
|
|
if (!tree)
|
|
{
|
|
// Con_Printf("No input tree: %s", jcl->bufferedinmessage);
|
|
return JCL_DONE;
|
|
}
|
|
}
|
|
|
|
// Con_Printf("read\n");
|
|
// XML_ConPrintTree(tree, 0);
|
|
|
|
jcl->timeout = jclient_curtime + 60*1000;
|
|
|
|
unparsable = true;
|
|
if (!strcmp(tree->name, "features"))
|
|
{
|
|
if ((ot=XML_ChildOfTree(tree, "bind", 0)))
|
|
{
|
|
unparsable = false;
|
|
JCL_SendIQf(jcl, JCL_BindReply, "set", NULL, "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>%s</resource></bind>", jcl->resource);
|
|
}
|
|
if ((ot=XML_ChildOfTree(tree, "session", 0)))
|
|
{
|
|
unparsable = false;
|
|
JCL_SendIQf(jcl, JCL_SessionReply, "set", NULL, "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>");
|
|
jcl->connected = true;
|
|
|
|
JCL_WriteConfig();
|
|
|
|
// JCL_AddClientMessageString(jcl, "<iq type='get' to='gmail.com' id='H_2'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>");
|
|
}
|
|
|
|
|
|
if (unparsable)
|
|
{
|
|
if ((!jclient->issecure) && BUILTINISVALID(Net_SetTLSClient) && XML_ChildOfTree(tree, "starttls", 0) != NULL && !pCvar_GetFloat("xmpp_disabletls"))
|
|
{
|
|
Con_Printf("Attempting to switch to TLS\n");
|
|
JCL_AddClientMessageString(jcl, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />");
|
|
unparsable = false;
|
|
}
|
|
else if ((ot=XML_ChildOfTree(tree, "mechanisms", 0)))
|
|
{
|
|
qboolean canplain = false;
|
|
// qboolean canmd5 = false;
|
|
// qboolean canscramsha1 = false;
|
|
// qboolean canxoath2 = false;
|
|
|
|
for(ot = ot->child; ot; ot = ot->sibling)
|
|
{
|
|
if (!strcmp(ot->body, "PLAIN"))
|
|
canplain = true;
|
|
// else if (!strcmp(ot->body, "SCRAM-SHA-1"))
|
|
// cansha1 = true;
|
|
// else if (!strcmp(ot->body, "DIGEST-MD5"))
|
|
// canmd5 = true;
|
|
// else if (!strcmp(ot->body, "X-OAUTH2"))
|
|
// canxoath2 = true;
|
|
// else
|
|
// Con_Printf("Unknown auth method \'%s\'\n", ot->body);
|
|
}
|
|
/*
|
|
if (canscramsha1)
|
|
{
|
|
Con_Printf("Using scram-sha-1%s\n", jclient->issecure?" over tls/ssl":"");
|
|
strcpy(jcl->authnonce, "abcdefghijklmnopqrstuvwxyz"); //FIXME: should be random
|
|
Base64_Add("n,,n=", 5);
|
|
Base64_Add(jclient->username, strlen(jcl->username));
|
|
Base64_Add(",r=", 3);
|
|
Base64_Add(jcl->authnonce, strlen(jcl->authnonce)); //must be random ascii.
|
|
Base64_Finish();
|
|
JCL_AddClientMessagef(jcl, "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>%s</auth>", base64);
|
|
unparsable = false;
|
|
}
|
|
else
|
|
*/
|
|
if (canplain && (jclient->issecure || pCvar_GetFloat("xmpp_allowplainauth")))
|
|
{
|
|
//plain can still be read with man-in-the-middle attacks, of course, even with tls if the certificate is spoofed, so this should always be the lowest priority.
|
|
//we just hope that the tls certificate cannot be spoofed.
|
|
Con_Printf("Using plain auth%s\n", jclient->issecure?" over tls/ssl":"");
|
|
|
|
Base64_Add("", 1);
|
|
Base64_Add(jclient->username, strlen(jcl->username));
|
|
Base64_Add("", 1);
|
|
Base64_Add(jcl->password, strlen(jcl->password));
|
|
Base64_Finish();
|
|
JCL_AddClientMessagef(jcl, "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>", base64);
|
|
unparsable = false;
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("XMPP: No suitable auth methods. Unable to connect.\n");
|
|
XML_ConPrintTree(tree, 0);
|
|
XML_Destroy(tree);
|
|
return JCL_KILL;
|
|
}
|
|
}
|
|
else //we cannot auth, no suitable method.
|
|
{
|
|
Con_Printf("XMPP: Neither SASL or TLS are usable\n");
|
|
XML_Destroy(tree);
|
|
return JCL_KILL;
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
else if (!strcmp(tree->name, "challenge") && !strcmp(tree->xmlns, "urn:ietf:params:xml:ns:xmpp-sasl"))
|
|
{
|
|
//sasl SCRAM-SHA-1 challenge
|
|
//send back the same 'r' attribute
|
|
buf saslchal;
|
|
int l, i;
|
|
buf salt;
|
|
buf csn;
|
|
buf itr;
|
|
buf final;
|
|
buf sigkey;
|
|
char salted_password[20];
|
|
char proof[20];
|
|
char proof64[30];
|
|
char clientkey[20];
|
|
char storedkey[20];
|
|
|
|
void hmacsha1(char *out, char *key, int keysize, char *data, int datalen);
|
|
void Hi(char *out, char *password, buf salt, int i);
|
|
|
|
saslchal.len = Base64_Decode(saslchal.buf, sizeof(saslchal.buf), tree->body, strlen(tree->body));
|
|
//be warned, these CAN contain nulls.
|
|
csn = saslattr(&saslchal, 'r');
|
|
salt = saslattr(&saslchal, 's');
|
|
itr = saslattr(&saslchal, 'i');
|
|
|
|
|
|
//this is the first part of the message we're about to send, with no proof.
|
|
//c(channel) is mandatory but nulled and forms part of the hash
|
|
final.len = 0;
|
|
buf_cat(&final, "c=", 2);
|
|
Base64_Add("n,,", 3));
|
|
Base64_Finish();
|
|
final.cat(&final, base64, strlen(base64));
|
|
final.cat(&final, "r=", 2);
|
|
final.cat(&final, csn.buf, csn.len);
|
|
|
|
//our original message + ',' + challenge + ',' + the message we're about to send.
|
|
sigkey.len = 0;
|
|
buf_cat(&sigkey, "n,,n=", 5);
|
|
buf_cat(&sigkey, jcl->username, strlen(jcl->username));
|
|
buf_cat(&sigkey, "r=", 2);
|
|
buf_cat(&sigkey, jcl->authnonce, strlen(jcl->authnonce));
|
|
buf_cat(&sigkey, ",", 1);
|
|
buf_cat(&sigkey, saslchal.buf, saslchal.len);
|
|
buf_cat(&sigkey, ",", 1);
|
|
buf_cat(&sigkey, final.buf, final.len);
|
|
|
|
Hi(salted_password, password, salt, atoi(itr));
|
|
hmacsha1(clientkey, salted_password, sizeof(salted_password), "Client Key", strlen("Client Key"));
|
|
storedkey = sha1(clientkey, sizeof(clientkey));
|
|
hmacsha1(clientsignature, storedkey, sizeof(storedkey), sigkey.buf, sigkey.len);
|
|
|
|
for (i = 0; i < sizeof(proof); i++)
|
|
proof[i] = clientkey[i] ^ clientsignature[i];
|
|
|
|
Base64_Add(proof, sizeof(proof));
|
|
Base64_Finish();
|
|
strcpy(proof64, base64);
|
|
Base64_Add(final, buflen(final));
|
|
Base64_Add(",p=", 3);
|
|
Base64_Add(proof64, strlen(proof64));
|
|
Base64_Finish();
|
|
JCL_AddClientMessagef(jcl, "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%s</response>", base64);
|
|
}
|
|
*/
|
|
else if (!strcmp(tree->name, "proceed"))
|
|
{
|
|
//switch to TLS, if we can
|
|
|
|
//Restart everything, basically.
|
|
jcl->bufferedinammount = 0;
|
|
jcl->instreampos = 0;
|
|
jcl->tagdepth = 0;
|
|
|
|
if (!BUILTINISVALID(Net_SetTLSClient))
|
|
{
|
|
Con_Printf("XMPP: proceed without TLS\n");
|
|
XML_Destroy(tree);
|
|
return JCL_KILL;
|
|
}
|
|
|
|
if (pNet_SetTLSClient(jcl->socket, jcl->domain)<0)
|
|
{
|
|
Con_Printf("XMPP: failed to switch to TLS\n");
|
|
XML_Destroy(tree);
|
|
return JCL_KILL;
|
|
}
|
|
jclient->issecure = true;
|
|
|
|
JCL_AddClientMessageString(jcl,
|
|
"<?xml version='1.0' ?>"
|
|
"<stream:stream to='");
|
|
JCL_AddClientMessageString(jcl, jcl->domain);
|
|
JCL_AddClientMessageString(jcl, "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");
|
|
|
|
XML_Destroy(tree);
|
|
return JCL_DONE;
|
|
}
|
|
else if (!strcmp(tree->name, "failure"))
|
|
{
|
|
if (tree->child)
|
|
Con_Printf("XMPP: Failure: %s\n", tree->child->name);
|
|
else
|
|
Con_Printf("XMPP: Unknown failure\n");
|
|
XML_Destroy(tree);
|
|
return JCL_KILL;
|
|
}
|
|
else if (!strcmp(tree->name, "error"))
|
|
{
|
|
ot = XML_ChildOfTree(tree, "text", 0);
|
|
if (ot)
|
|
Con_Printf("XMPP: %s\n", ot->body);
|
|
else
|
|
Con_Printf("XMPP: Unknown error\n");
|
|
|
|
ot = XML_ChildOfTree(tree, "conflict", 0);
|
|
XML_Destroy(tree);
|
|
|
|
if (ot)
|
|
return JCL_NUKEFROMORBIT;
|
|
else
|
|
return JCL_KILL;
|
|
}
|
|
else if (!strcmp(tree->name, "success"))
|
|
{
|
|
//Restart everything, basically, AGAIN! (third time lucky?)
|
|
jcl->bufferedinammount = 0;
|
|
jcl->instreampos = 0;
|
|
jcl->tagdepth = 0;
|
|
|
|
JCL_AddClientMessageString(jcl,
|
|
"<?xml version='1.0' ?>"
|
|
"<stream:stream to='");
|
|
JCL_AddClientMessageString(jcl, jcl->domain);
|
|
JCL_AddClientMessageString(jcl, "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");
|
|
|
|
return JCL_DONE;
|
|
}
|
|
else if (!strcmp(tree->name, "iq"))
|
|
{
|
|
JCL_ParseIQ(jcl, tree);
|
|
unparsable = false;
|
|
}
|
|
else if (!strcmp(tree->name, "message"))
|
|
{
|
|
JCL_ParseMessage(jcl, tree);
|
|
unparsable = false;
|
|
}
|
|
else if (!strcmp(tree->name, "presence"))
|
|
{
|
|
JCL_ParsePresence(jcl, tree);
|
|
//we should keep a list of the people that we know of.
|
|
unparsable = false;
|
|
}
|
|
else
|
|
{
|
|
Con_Printf("JCL unrecognised stanza: %s\n", tree->name);
|
|
XML_ConPrintTree(tree, 0);
|
|
}
|
|
|
|
XML_Destroy(tree);
|
|
|
|
memmove(jcl->bufferedinmessage, jcl->bufferedinmessage+pos, jcl->bufferedinammount-pos);
|
|
jcl->bufferedinammount -= pos;
|
|
jcl->instreampos -= pos;
|
|
|
|
if (unparsable)
|
|
{
|
|
Con_Printf("XMPP: Input corrupt, urecognised, or unusable. Disconnecting.\n");
|
|
return JCL_KILL;
|
|
}
|
|
|
|
return JCL_CONTINUE;
|
|
}
|
|
|
|
void JCL_CloseConnection(jclient_t *jcl, qboolean reconnect)
|
|
{
|
|
//send our signoff to the server, if we're still alive.
|
|
Con_Printf("XMPP: Disconnected from %s@%s\n", jcl->username, jcl->domain);
|
|
|
|
if (jcl->status == JCL_ACTIVE)
|
|
JCL_AddClientMessageString(jcl, "<presence type='unavailable'/>");
|
|
JCL_AddClientMessageString(jcl, "</stream:stream>");
|
|
JCL_FlushOutgoing(jcl);
|
|
|
|
//forget all our friends.
|
|
JCL_ForgetBuddy(jcl, NULL, NULL);
|
|
|
|
//destroy any data that never got sent
|
|
free(jcl->outbuf);
|
|
jcl->outbuf = NULL;
|
|
jcl->outbuflen = 0;
|
|
jcl->outbufpos = 0;
|
|
jcl->outbufmax = 0;
|
|
|
|
pNet_Close(jcl->socket);
|
|
jcl->socket = -1;
|
|
jcl->status = JCL_DEAD;
|
|
|
|
jcl->timeout = jclient_curtime + 30*1000; //wait 30 secs before reconnecting, to avoid flood-prot-protection issues.
|
|
|
|
if (!reconnect)
|
|
{
|
|
free(jcl);
|
|
if (jclient == jcl)
|
|
jclient = NULL;
|
|
}
|
|
}
|
|
|
|
//can be polled for server address updates
|
|
void JCL_GeneratePresence(jclient_t *jcl, qboolean force)
|
|
{
|
|
int dummystat;
|
|
char serveraddr[1024*16];
|
|
char servermap[1024*16];
|
|
//get the last server address
|
|
|
|
serveraddr[0] = 0;
|
|
servermap[0] = 0;
|
|
|
|
if (!pCvar_GetFloat("xmpp_nostatus"))
|
|
{
|
|
if (pCvar_GetFloat("sv.state"))
|
|
{
|
|
pCvar_GetString("sv.mapname", servermap, sizeof(servermap));
|
|
}
|
|
else
|
|
{
|
|
if (!pCvar_GetString("cl_serveraddress", serveraddr, sizeof(serveraddr)))
|
|
serveraddr[0] = 0;
|
|
if (BUILTINISVALID(CL_GetStats))
|
|
{
|
|
//if we can't get any stats, its because we're not actually on the server.
|
|
if (!pCL_GetStats(0, &dummystat, 1))
|
|
serveraddr[0] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (force || strcmp(jcl->curquakeserver, *servermap?servermap:serveraddr))
|
|
{
|
|
char caps[256];
|
|
Q_strlcpy(jcl->curquakeserver, *servermap?servermap:serveraddr, sizeof(jcl->curquakeserver));
|
|
Con_DPrintf("Sending presence %s\n", jcl->curquakeserver);
|
|
//note: ext='voice-v1 camera-v1 video-v1' is some legacy nonsense, and is required for voice calls with googletalk clients or something stupid like that
|
|
Q_snprintf(caps, sizeof(caps), "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://fteqw.com/ftexmppplugin' ver='%s'/>", buildcapshash());
|
|
|
|
if (!*jcl->curquakeserver)
|
|
JCL_AddClientMessagef(jcl,
|
|
"<presence>"
|
|
"%s"
|
|
"</presence>", caps);
|
|
else if (*servermap) //if we're running a server, say so
|
|
JCL_AddClientMessagef(jcl,
|
|
"<presence>"
|
|
"<quake xmlns='fteqw.com:game' servermap='%s'/>"
|
|
"%s"
|
|
"</presence>"
|
|
, servermap, caps);
|
|
else //if we're connected to a server, say so
|
|
JCL_AddClientMessagef(jcl,
|
|
"<presence>"
|
|
"<quake xmlns='fteqw.com:game' serverip='%s' />"
|
|
"%s"
|
|
"</presence>"
|
|
,jcl->curquakeserver, caps);
|
|
}
|
|
}
|
|
|
|
//functions above this line allow connections to multiple servers.
|
|
//it is just the control functions that only allow one server.
|
|
|
|
qintptr_t JCL_Frame(qintptr_t *args)
|
|
{
|
|
int stat = JCL_CONTINUE;
|
|
jclient_t *jcl = jclient;
|
|
|
|
jclient_curtime = args[0];
|
|
if (jcl)
|
|
{
|
|
JCL_JingleTimeouts(jcl, false);
|
|
if (jcl->status == JCL_DEAD)
|
|
{
|
|
if (jclient_curtime > jcl->timeout)
|
|
{
|
|
JCL_Reconnect(jcl);
|
|
jcl->timeout = jclient_curtime + 60*1000;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (jcl->connected)
|
|
JCL_GeneratePresence(jcl, false);
|
|
while(stat == JCL_CONTINUE)
|
|
stat = JCL_ClientFrame(jcl);
|
|
if (stat == JCL_NUKEFROMORBIT)
|
|
JCL_CloseConnection(jcl, false);
|
|
else if (stat == JCL_KILL)
|
|
JCL_CloseConnection(jcl, true);
|
|
else
|
|
JCL_FlushOutgoing(jcl);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void JCL_WriteConfig(void)
|
|
{
|
|
if (jclient->connected)
|
|
{
|
|
qhandle_t config;
|
|
pFS_Open("**plugconfig", &config, 2);
|
|
if (config >= 0)
|
|
{
|
|
char buffer[8192];
|
|
Q_snprintf(buffer, sizeof(buffer), "%i \"%s\" \"%s@%s\" \"%s\"\n",
|
|
jclient->tlsconnect, jclient->server, jclient->username, jclient->domain, jclient->password);
|
|
pFS_Write(config, buffer, strlen(buffer));
|
|
pFS_Close(config);
|
|
}
|
|
}
|
|
}
|
|
void JCL_LoadConfig(void)
|
|
{
|
|
if (!jclient)
|
|
{
|
|
int len;
|
|
qhandle_t config;
|
|
char buf[8192];
|
|
char tls[256];
|
|
char server[256];
|
|
char account[256];
|
|
char password[256];
|
|
char *line = buf;
|
|
qboolean oldtls;
|
|
len = pFS_Open("**plugconfig", &config, 1);
|
|
if (config >= 0)
|
|
{
|
|
if (len >= sizeof(buf))
|
|
len = sizeof(buf)-1;
|
|
buf[len] = 0;
|
|
pFS_Read(config, buf, len);
|
|
pFS_Close(config);
|
|
|
|
line = JCL_ParseOut(line, tls, sizeof(tls));
|
|
line = JCL_ParseOut(line, server, sizeof(server));
|
|
line = JCL_ParseOut(line, account, sizeof(account));
|
|
line = JCL_ParseOut(line, password, sizeof(password));
|
|
|
|
oldtls = atoi(tls);
|
|
|
|
jclient = JCL_Connect(server, oldtls, account, password);
|
|
}
|
|
}
|
|
}
|
|
static void JCL_PrintBuddyStatus(char *console, buddy_t *b, bresource_t *r)
|
|
{
|
|
if (r->servertype == 2)
|
|
Con_SubPrintf(console, "^[[Playing Quake - %s]\\xmpp\\%s/%s\\xmppact\\join^]", r->server, b->accountdomain, r->resource);
|
|
else if (r->servertype)
|
|
Con_SubPrintf(console, "^[[Playing Quake - %s]\\observe\\%s^]", r->server, r->server);
|
|
else if (*r->fstatus)
|
|
Con_SubPrintf(console, "%s - %s", r->bstatus, r->fstatus);
|
|
else
|
|
Con_SubPrintf(console, "%s", r->bstatus);
|
|
|
|
if ((r->caps & CAP_INVITE) && !r->servertype)
|
|
Con_SubPrintf(console, " ^[[Invite]\\xmpp\\%s/%s\\xmppact\\invite^]", b->accountdomain, r->resource);
|
|
if (r->caps & CAP_VOICE)
|
|
Con_SubPrintf(console, " ^[[Call]\\xmpp\\%s/%s\\xmppact\\call^]", b->accountdomain, r->resource);
|
|
}
|
|
void JCL_PrintBuddyList(char *console, jclient_t *jcl, qboolean all)
|
|
{
|
|
buddy_t *b;
|
|
bresource_t *r;
|
|
struct c2c_s *c2c;
|
|
if (!jcl->buddies)
|
|
Con_SubPrintf(console, "You have no friends\n");
|
|
for (b = jcl->buddies; b; b = b->next)
|
|
{
|
|
//if we don't actually know them, don't list them.
|
|
if (!b->friended)
|
|
continue;
|
|
|
|
if (!b->resources) //offline
|
|
{
|
|
if (all)
|
|
Con_SubPrintf(console, "^[^7[%s]\\xmpp\\%s^]: offline\n", b->name, b->accountdomain);
|
|
}
|
|
else if (b->resources->next)
|
|
{ //multiple potential resources
|
|
Con_SubPrintf(console, "^[[%s]\\xmpp\\%s^]\n", b->name, b->accountdomain);
|
|
for (r = b->resources; r; r = r->next)
|
|
{
|
|
Con_SubPrintf(console, " ^[[%s]\\xmpp\\%s/%s^]: ", r->resource, b->accountdomain, r->resource);
|
|
JCL_PrintBuddyStatus(console, b, r);
|
|
Con_SubPrintf(console, "\n");
|
|
}
|
|
}
|
|
else //only one resource
|
|
{
|
|
r = b->resources;
|
|
Con_SubPrintf(console, "^[[%s]\\xmpp\\%s/%s^]: ", b->name, b->accountdomain, r->resource);
|
|
JCL_PrintBuddyStatus(console, b, r);
|
|
Con_SubPrintf(console, "\n");
|
|
}
|
|
}
|
|
|
|
if (jcl->c2c)
|
|
Con_SubPrintf(console, "Active sessions:\n");
|
|
for (c2c = jcl->c2c; c2c; c2c = c2c->next)
|
|
{
|
|
JCL_FindBuddy(jcl, c2c->with, &b, &r);
|
|
switch(c2c->mediatype)
|
|
{
|
|
case ICEP_VOICE:
|
|
Con_SubPrintf(console, " ^[[%s]\\xmpp\\%s/%s^]: voice ^[[Hang Up]\\xmppact\\jdeny\\xmpp\\%s\\xmppsid\\%s^]\n", b->name, b->accountdomain, r->resource, c2c->with, c2c->sid);
|
|
break;
|
|
case ICEP_QWSERVER:
|
|
Con_SubPrintf(console, " ^[[%s]\\xmpp\\%s/%s^]: server ^[[Kick]\\xmppact\\jdeny\\xmpp\\%s\\xmppsid\\%s^]\n", b->name, b->accountdomain, r->resource, c2c->with, c2c->sid);
|
|
break;
|
|
case ICEP_QWCLIENT:
|
|
Con_SubPrintf(console, " ^[[%s]\\xmpp\\%s/%s^]: client ^[[Disconnect]\\xmppact\\jdeny\\xmpp\\%s\\xmppsid\\%s^]\n", b->name, b->accountdomain, r->resource, c2c->with, c2c->sid);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void JCL_SendMessage(jclient_t *jcl, char *to, char *msg)
|
|
{
|
|
char markup[1024];
|
|
buddy_t *b;
|
|
bresource_t *br;
|
|
JCL_FindBuddy(jcl, to, &b, &br);
|
|
if (br)
|
|
JCL_AddClientMessagef(jcl, "<message to='%s/%s'><body>", b->accountdomain, br->resource);
|
|
else
|
|
JCL_AddClientMessagef(jcl, "<message to='%s'><body>", b->accountdomain);
|
|
JCL_AddClientMessage(jcl, markup, XML_Markup(msg, markup, sizeof(markup)) - markup);
|
|
JCL_AddClientMessageString(jcl, "</body></message>");
|
|
if (!strncmp(msg, "/me ", 4))
|
|
Con_SubPrintf(b->name, "*^5%s^7"COLOURYELLOW"%s\n", ((!strcmp(jcl->localalias, ">>"))?"me":jcl->localalias), msg+3);
|
|
else
|
|
Con_SubPrintf(b->name, "^5%s^7: "COLOURYELLOW"%s\n", jcl->localalias, msg);
|
|
}
|
|
void JCL_AttentionMessage(jclient_t *jcl, char *to, char *msg)
|
|
{
|
|
char fullto[256];
|
|
buddy_t *b;
|
|
bresource_t *br;
|
|
xmltree_t *m;
|
|
char *s;
|
|
|
|
JCL_FindBuddy(jcl, to, &b, &br);
|
|
if (!br)
|
|
br = b->defaultresource;
|
|
if (!br)
|
|
br = b->resources;
|
|
if (!br)
|
|
{
|
|
Con_SubPrintf(b->name, "User is not online\n");
|
|
return;
|
|
}
|
|
Q_snprintf(fullto, sizeof(fullto), "%s/%s", b->accountdomain, br->resource);
|
|
|
|
m = XML_CreateNode(NULL, "message", "", "");
|
|
XML_AddParameter(m, "to", fullto);
|
|
// XML_AddParameter(m, "type", "headline");
|
|
|
|
XML_CreateNode(m, "attention", "urn:xmpp:attention:0", "");
|
|
if (msg)
|
|
XML_CreateNode(m, "body", "", msg);
|
|
|
|
s = XML_GenerateString(m);
|
|
JCL_AddClientMessageString(jcl, s);
|
|
free(s);
|
|
XML_Destroy(m);
|
|
|
|
if (msg)
|
|
{
|
|
if (!strncmp(msg, "/me ", 4))
|
|
Con_SubPrintf(b->name, "*^5%s^7"COLOURYELLOW"%s\n", ((!strcmp(jcl->localalias, ">>"))?"me":jcl->localalias), msg+3);
|
|
else
|
|
Con_SubPrintf(b->name, "^5%s^7: "COLOURYELLOW"%s\n", jcl->localalias, msg);
|
|
}
|
|
}
|
|
|
|
void JCL_ToJID(jclient_t *jcl, char *in, char *out, int outsize)
|
|
{
|
|
//decompose links first
|
|
if (in[0] == '^' && in[1] == '[')
|
|
{
|
|
char *sl;
|
|
char *le;
|
|
sl = in+2;
|
|
sl = strchr(in, '\\');
|
|
if (sl)
|
|
{
|
|
le = strstr(sl, "^]");
|
|
if (le)
|
|
{
|
|
*le = 0;
|
|
JCL_Info_ValueForKey(in, "xmpp", out, outsize);
|
|
*le = '^';
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!strchr(in, '@') && jcl)
|
|
{
|
|
//no @? probably its an alias, but could also be a server/domain perhaps. not sure we care. you'll just have to rename your friend.
|
|
//check to see if we can find a friend going by that name
|
|
//fixme: allow resources to make it through here
|
|
buddy_t *b;
|
|
for (b = jcl->buddies; b; b = b->next)
|
|
{
|
|
if (!strcasecmp(b->name, in))
|
|
{
|
|
Q_strlcpy(out, b->accountdomain, outsize);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//a regular jabber account name
|
|
Q_strlcpy(out, in, outsize);
|
|
}
|
|
|
|
void JCL_Command(char *console)
|
|
{
|
|
char imsg[8192];
|
|
char arg[6][256];
|
|
char *msg;
|
|
int i;
|
|
char nname[256];
|
|
|
|
pCmd_Args(imsg, sizeof(imsg));
|
|
|
|
msg = imsg;
|
|
for (i = 0; i < 6; i++)
|
|
{
|
|
if (!msg)
|
|
continue;
|
|
msg = JCL_ParseOut(msg, arg[i], sizeof(arg[i]));
|
|
}
|
|
|
|
if (arg[0][0] == '/' && arg[0][1] != '/' && strcmp(arg[0]+1, "me"))
|
|
{
|
|
JCL_ToJID(jclient, *arg[1]?arg[1]:console, nname, sizeof(nname));
|
|
|
|
if (!strcmp(arg[0]+1, "open") || !strcmp(arg[0]+1, "connect") || !strcmp(arg[0]+1, "tlsopen") || !strcmp(arg[0]+1, "tlsconnect"))
|
|
{ //tlsconnect is 'old'.
|
|
if (!*arg[1])
|
|
{
|
|
Con_SubPrintf(console, "%s <account@domain/resource> <password> <server>\n", arg[0]+1);
|
|
return;
|
|
}
|
|
|
|
if (jclient)
|
|
{
|
|
Con_TrySubPrint(console, "You are already connected\nPlease /quit first\n");
|
|
return;
|
|
}
|
|
jclient = JCL_Connect(arg[3], !strncmp(arg[0]+1, "tls", 3), arg[1], arg[2]);
|
|
if (!jclient)
|
|
{
|
|
Con_TrySubPrint(console, "Connect failed\n");
|
|
return;
|
|
}
|
|
}
|
|
else if (!strcmp(arg[0]+1, "help"))
|
|
{
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /connect USERNAME@DOMAIN/RESOURCE PASSWORD XMPPSERVER^]\n");
|
|
if (BUILTINISVALID(Net_SetTLSClient))
|
|
{
|
|
Con_Printf("eg for gmail: ^[/" COMMANDPREFIX " /connect myusername@gmail.com mypassword talk.google.com^]\n");
|
|
Con_Printf("eg for facebook: ^[/" COMMANDPREFIX " /connect myusername@chat.facebook.com mypassword chat.facebook.com^]\n");
|
|
}
|
|
Con_Printf("Note that this info will be used the next time you start quake.\n");
|
|
|
|
//small note:
|
|
//for the account 'me@example.com' the server to connect to can be displayed with:
|
|
//nslookup -querytype=SRV _xmpp-client._tcp.example.com
|
|
//srv resolving seems to be non-standard on each system, I don't like having to special case things.
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /help^]\n"
|
|
"This text...\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /raw <XML STANZAS/>^]\n"
|
|
"For debug hackery.\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /friend accountname friendlyname^]\n"
|
|
"Befriends accountname, and shows them in your various lists using the friendly name. Can also be used to rename friends.\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /unfriend accountname^]\n"
|
|
"Ostracise your new best enemy. You will no longer see them and they won't be able to contact you.\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /blist^]\n"
|
|
"Show all your friends! Names are clickable and will begin conversations.\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /quit^]\n"
|
|
"Disconnect from the XMPP server, noone will be able to hear you scream.\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /join accountname^]\n"
|
|
"Joins your friends game (they will be prompted).\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /invite accountname^]\n"
|
|
"Invite someone to join your game (they will be prompted).\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /voice accountname^]\n"
|
|
"Begin a bi-directional peer-to-peer voice conversation with someone (they will be prompted).\n");
|
|
Con_TrySubPrint(console, "^[/" COMMANDPREFIX " /msg ACCOUNTNAME your message goes here^]\n"
|
|
"Sends a message to the named person. If given a resource postfix, your message will be sent only to that resource.\n");
|
|
Con_TrySubPrint(console, "If no arguments, will print out your friends list. If no /command is used, the arguments will be sent as a message to the person you last sent a message to.\n");
|
|
}
|
|
else if (!strcmp(arg[0]+1, "clear"))
|
|
{
|
|
//just clears the current console.
|
|
if (*console)
|
|
{
|
|
pCon_Destroy(console);
|
|
Con_SubPrintf(console, "");
|
|
pCon_SetActive(console);
|
|
}
|
|
else
|
|
pCmd_AddText("\nclear\n", true);
|
|
}
|
|
else if (!jclient)
|
|
{
|
|
Con_SubPrintf(console, "No account specified. Cannot %s\n", arg[0]);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "quit"))
|
|
{
|
|
//disconnect from the xmpp server.
|
|
JCL_CloseConnection(jclient, false);
|
|
}
|
|
else if (jclient->status != JCL_ACTIVE)
|
|
{
|
|
Con_SubPrintf(console, "You are not authed. Please wait.\n", arg[0]);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "blist"))
|
|
{
|
|
//print out a full list of everyone, even those offline.
|
|
JCL_PrintBuddyList(console, jclient, true);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "msg"))
|
|
{
|
|
//FIXME: validate the dest. deal with xml markup in dest.
|
|
Q_strlcpy(jclient->defaultdest, arg[1], sizeof(jclient->defaultdest));
|
|
msg = arg[2];
|
|
|
|
JCL_SendMessage(jclient, jclient->defaultdest, msg);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "friend"))
|
|
{
|
|
//FIXME: validate the name. deal with xml markup.
|
|
|
|
//can also rename. We should probably read back the groups for the update.
|
|
JCL_SendIQf(jclient, NULL, "set", NULL, "<query xmlns='jabber:iq:roster'><item jid='%s' name='%s'></item></query>", arg[1], arg[2]);
|
|
|
|
//start looking for em
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='subscribe'/>", arg[1]);
|
|
|
|
//let em see us
|
|
if (jclient->preapproval)
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='subscribed'/>", arg[1]);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "unfriend"))
|
|
{
|
|
//FIXME: validate the name. deal with xml markup.
|
|
|
|
//hide from em
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='unsubscribed'/>", arg[1]);
|
|
|
|
//stop looking for em
|
|
JCL_AddClientMessagef(jclient, "<presence to='%s' type='unsubscribe'/>", arg[1]);
|
|
|
|
//stop listing em
|
|
JCL_SendIQf(jclient, NULL, "set", NULL, "<query xmlns='jabber:iq:roster'><item jid='%s' subscription='remove' /></query>", arg[1]);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "join"))
|
|
{
|
|
JCL_Join(jclient, nname, NULL, true, ICEP_QWCLIENT);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "invite"))
|
|
{
|
|
JCL_Join(jclient, nname, NULL, true, ICEP_QWSERVER);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "voice") || !strcmp(arg[0]+1, "call"))
|
|
{
|
|
JCL_Join(jclient, nname, NULL, true, ICEP_VOICE);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "kick"))
|
|
{
|
|
JCL_Join(jclient, nname, NULL, false, ICEP_INVALID);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "slap"))
|
|
{
|
|
char *msgtab[] =
|
|
{
|
|
"/me slaps you around a bit with a large trout",
|
|
"/me slaps you around a bit with a large tunafish",
|
|
"/me slaps you around a bit with a slimy hagfish",
|
|
"/me slaps a large trout around a bit with your face",
|
|
"/me gets eaten by a rabid shark while trying to slap you with it",
|
|
"/me gets crushed under the weight of a large whale",
|
|
"/me searches for a fresh fish, but gets crabs instead",
|
|
"/me searches for a fish, but there are no more fish in the sea",
|
|
"/me tickles you around a bit with a large fish finger",
|
|
"/me goes to order cod and chips. brb",
|
|
"/me goes to watch some monty python"
|
|
};
|
|
JCL_AttentionMessage(jclient, nname, msgtab[rand()%(sizeof(msgtab)/sizeof(msgtab[0]))]);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "poke"))
|
|
{
|
|
JCL_AttentionMessage(jclient, nname, NULL);
|
|
}
|
|
else if (!strcmp(arg[0]+1, "raw"))
|
|
{
|
|
jclient->streamdebug = true;
|
|
JCL_AddClientMessageString(jclient, arg[1]);
|
|
}
|
|
else
|
|
Con_SubPrintf(console, "Unrecognised command: %s\n", arg[0]);
|
|
}
|
|
else
|
|
{
|
|
if (jclient)
|
|
{
|
|
msg = imsg;
|
|
|
|
if (!*msg)
|
|
{
|
|
if (!*console)
|
|
{
|
|
JCL_PrintBuddyList(console, jclient, false);
|
|
//Con_TrySubPrint(console, "For help, type \"^[/" COMMANDPREFIX " /help^]\"\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
JCL_ToJID(jclient, *console?console:jclient->defaultdest, nname, sizeof(nname));
|
|
JCL_SendMessage(jclient, nname, msg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Con_TrySubPrint(console, "Not connected. For help, type \"^[/" COMMANDPREFIX " /help^]\"\n");
|
|
}
|
|
}
|
|
}
|