fteqw/plugins/jabber/jabberclient.c
Spoike a05f3f36a0 slightly better doom3 compat
git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@4439 fc73d0e0-1445-4013-8a0c-d673dee63da5
2013-07-29 17:36:44 +00:00

5007 lines
140 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
/*
Network limitations:
googletalk:
username: same as gmail (foobar@gmail.com).
FIXME: need to test foobar@googlemail.com
auth mechanism: oauth2(tls+nontls) or plain(tls-only). no digests supported, so mitm can easily grab your password if they use certificate authority hackery, so DO NOT log in from work.
oauth2: I've registered a clientid for use with googletalk's network, but the whole web-browser-is-required crap makes it near unusable. We'll try it if they omit a password.
otherwise a complete implementation.
facebook:
username: foobar@chat.facebook.com
auth mechanism: digest-md5, x-facebook-platform.
gateway implementation: no arbitary iq support (no invite/join/voice).
no roster control
completely untested. I've no interest in signing up to be tracked constantly (but somehow google is okay... go figure... I guess I'm just trying to avoid a double-whammy)
oauth2: no idea where to register a clientid, or what the correct addresses are. a google search implies they don't do refresh tokens properly. sticking with digest-md5 should work.
msn:
username: foobar@messenger.live.com (NOT foobar@live.com - this will timeout)
auth mechansim: x-messenger-oath ONLY
non-standard unusable crap.
uses incorrect certificates. any client that doesn't warn about that is buggy as fuck.
probably doesn't have iq support, no idea, can't log in to test that
requires annoying see-other-host redirection.
no roster control
stun servers are listed in srv records for live.com and messanger.live.com but not messenger.live.com. retards.
oauth2: too lazy to register a clientid. stupid crap. I hate having to register everywhere.
ejabberd:
auth mechanism: digest-md5, scram-sha1, plain.
complete implementation. no issues.
may be lacking srv entries, depends on installation.
may have self-signed certificate issues, depends on installation.
client compat:
googletalk:
implements old version of jingle. voice calls not compatible.
not tested by me.
pidgin:
(linux) has issues with jingle+ice, and can be made to crash. voip uses speex. pidgin's ice seems vulnerable to dropped packets.
(windows) doesn't support voice calls
otherwise works.
*/
#include "../plugin.h"
#include <time.h>
#include "xml.h"
//#define NOICE
#define VOIP_SPEEX
//#define FILETRANSFERS //IBB only, speeds suck. autoaccept is forced on. no protection from mods stuffcmding sendfile commands. needs more extensive testing
#define JINGLE
#ifdef VOIP_SPEEX
#define VOIP
#endif
#ifdef JINGLE
#include "../../engine/common/netinc.h"
#endif
#define DEFAULTDOMAIN ""
#define DEFAULTRESOURCE "Quake"
#define QUAKEMEDIATYPE "quake"
#define QUAKEMEDIAXMLNS "fteqw.com:netmedia"
#define DEFAULTICEMODE ICEM_ICE
#ifdef JINGLE
icefuncs_t *piceapi;
#endif
#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++;
}
}
#ifdef _WIN32
#include "windns.h"
static DNS_STATUS (WINAPI *pDnsQuery_UTF8) (PCSTR pszName, WORD wType, DWORD Options, PIP4_ARRAY aipServers, PDNS_RECORD *ppQueryResults, PVOID *pReserved);
static VOID (WINAPI *pDnsRecordListFree)(PDNS_RECORD pRecordList, DNS_FREE_TYPE FreeType);
static HMODULE dnsapi_lib;
qboolean NET_DNSLookup_SRV(char *host, char *out, int outlen)
{
HRESULT hr;
DNS_RECORD *result = NULL;
if (!dnsapi_lib)
{
dnsapi_lib = LoadLibrary("dnsapi.dll");
pDnsQuery_UTF8 = (void*)GetProcAddress(dnsapi_lib, "DnsQuery_UTF8");
pDnsRecordListFree = (void*)GetProcAddress(dnsapi_lib, "DnsRecordListFree");
}
//win98?
if (!pDnsQuery_UTF8 || !pDnsRecordListFree)
return false;
//do lookup
hr = pDnsQuery_UTF8(host, DNS_TYPE_SRV, DNS_QUERY_STANDARD, NULL, &result, NULL);
if (result)
{
Q_snprintf(out, outlen, "[%s]:%i", result->Data.SRV.pNameTarget, result->Data.SRV.wPort);
pDnsRecordListFree(result, DnsFreeRecordList);
return true;
}
return false;
}
#else
qboolean NET_DNSLookup_SRV(char *host, char *out, int outlen)
{
return false;
}
#endif
char base64[((4096+3)*4/3)+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+4>=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 RenameConsole(char *totrim);
void JCL_Command(int accid, 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(0, "");
return true;
}
if (!strncmp(cmd, COMMANDPREFIX, strlen(COMMANDPREFIX)))
{
if (!args[0])
JCL_Command(atoi(cmd+strlen(COMMANDPREFIX)), "");
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 JCL_Shutdown(qintptr_t *args);
qintptr_t Plug_Init(qintptr_t *args)
{
if ( Plug_Export("Tick", JCL_Frame) &&
Plug_Export("Shutdown", JCL_Shutdown) &&
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);
pCmd_AddCommand(COMMANDPREFIX"0");
pCmd_AddCommand(COMMANDPREFIX"1");
pCmd_AddCommand(COMMANDPREFIX"2");
pCmd_AddCommand(COMMANDPREFIX"3");
pCmd_AddCommand(COMMANDPREFIX"4");
pCmd_AddCommand(COMMANDPREFIX"5");
pCmd_AddCommand(COMMANDPREFIX"6");
pCmd_AddCommand(COMMANDPREFIX"7");
//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");
#ifdef JINGLE
CHECKBUILTIN(Plug_GetNativePointer);
if (BUILTINISVALID(Plug_GetNativePointer))
piceapi = pPlug_GetNativePointer(ICE_API_CURRENT);
#endif
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.
#define CAP_SIFT 16 //non-jingle file transfers
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;
qboolean chatroom; //chatrooms are bizzare things that need special handling.
char name[256];
struct buddy_s *next;
char accountdomain[1]; //no resource on there
} buddy_t;
typedef struct jclient_s
{
int accountnum; //a private id to track which client links are associated with
char redirserveraddr[64]; //if empty, do an srv lookup.
enum
{
JCL_INACTIVE, //not trying to connect.
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];
//config info
char serveraddr[64]; //if empty, do an srv lookup.
int serverport;
char domain[256];
char username[256];
char password[256];
char resource[256];
char certificatedomain[256];
int forcetls; //-1=off, 0=ifpossible, 1=fail if can't upgrade, 2=old-style tls
qboolean allowauth_plainnontls; //allow plain plain
qboolean allowauth_plaintls; //allow tls plain
qboolean allowauth_digestmd5; //allow digest-md5 auth
qboolean allowauth_scramsha1; //allow scram-sha-1 auth
qboolean allowauth_oauth2; //use oauth2 where possible
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.
char authnonce[256];
int authmode;
int tagdepth;
int openbracket;
int instreampos;
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
{
char saslmethod[64];
char obtainurl[256];
char refreshurl[256];
char clientid[256];
char clientsecret[256];
char *useraccount;
char *scope;
char *accesstoken; //one-shot access token
char *refreshtoken; //long-term token that we can use to get new access tokens
char *authtoken; //short-term authorisation token, usable to get an access token (and a refresh token if we're lucky)
} oauth2;
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;
#ifdef JINGLE
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;
#endif
#ifdef FILETRANSFERS
struct ft_s
{
struct ft_s *next;
char fname[MAX_QPATH];
int size;
char *with;
char md5hash[16];
char sid[64];
int blocksize;
unsigned short seq;
qhandle_t file;
qboolean begun;
qboolean transmitting;
enum
{
FT_IBB, //in-band bytestreams
FT_BYTESTREAM //aka: relay
} method;
} *ft;
#endif
buddy_t *buddies;
} jclient_t;
jclient_t *jclients[8];
int jclient_curtime;
int jclient_poketime;
typedef struct
{
char *method;
int (*sasl_initial)(jclient_t *jcl, char *buf, int bufsize);
int (*sasl_challenge)(jclient_t *jcl, char *inbuf, int insize, char *outbuf, int outsize);
} saslmethod_t;
//#define OAUTH_CLIENT_ID_MSN "0"
#ifdef OAUTH_CLIENT_ID_MSN
static int sasl_plain_initial(jclient_t *jcl, char *buf, int bufsize)
{
//"https://oauth.live.com/authorize?client_id=" OAUTH_CLIENT_ID_MSN "&scope=wl.messenger,wl.basic,wl.offline_access,wl.contacts_create,wl.share&response_type=token&redirect_uri=http://localhost/";
}
#endif
//\0username\0password
static int sasl_plain_initial(jclient_t *jcl, char *buf, int bufsize)
{
int len = 0;
if (jcl->issecure?jcl->allowauth_plaintls:jcl->allowauth_plainnontls)
{
//realm isn't specified
buf[len++] = 0;
memcpy(buf+len, jcl->username, strlen(jcl->username));
len += strlen(jcl->username);
buf[len++] = 0;
memcpy(buf+len, jcl->password, strlen(jcl->password));
len += strlen(jcl->password);
return len;
}
return -1;
}
static int saslattr(char *out, int outlen, char *srcbuf, int srclen, char *arg)
{
char *vn;
char *s = srcbuf;
char *e = s + srclen;
char *vs;
while(s < e)
{
while (s < e && *s == ',')
s++;
vn = s;
s++;
while (s < e && *s >= 'a' && *s <= 'z')
s++;
if (*s == '=')
{
vs = ++s;
if (*s == '\"')
{
vs = ++s;
while (s < e && *s != '\"')
s++;
outlen = s - vs;
s++;
}
else
{
while (s < e && *s != ',')
s++;
outlen = s - vs;
}
if (!strncmp(vn, arg, strlen(arg)) && vn[strlen(arg)] == '=')
{
memcpy(out, vs, outlen);
out[outlen] = 0;
return outlen;
}
}
}
out[0] = 0;
return 0;
}
static int sasl_digestmd5_initial(jclient_t *jcl, char *buf, int bufsize)
{
if (jcl->allowauth_digestmd5)
{
//FIXME: randomize the cnonce and check the auth key
//although really I'm not entirely sure what the point is.
//if we just authenticated with a mitm attacker relay, we're screwed either way.
strcpy(jcl->authnonce, "abcdefghijklmnopqrstuvwxyz");
//nothing. server does the initial data.
return 0;
}
return -1;
}
char *MD5_ToHex(char *input, int inputlen, char *ret, int retlen);
char *MD5_ToBinary(char *input, int inputlen, char *ret, int retlen);
static int sasl_digestmd5_challenge(jclient_t *jcl, char *in, int inlen, char *out, int outlen)
{
char *username = jcl->username;
char *password = jcl->password;
char *cnonce = jcl->authnonce;
char rspauth[512];
char realm[512];
char nonce[512];
char qop[512];
char charset[512];
char algorithm[512];
char X[512];
char Y[33];
char A1[512];
char A2[512];
char HA1[33];
char HA2[33];
char KD[512];
char Z[33];
char *nc = "00000001";
char digesturi[512];
char *authzid = "";
saslattr(rspauth, sizeof(rspauth), in, inlen, "rspauth");
if (*rspauth)
{
return 0; //we don't actually send any data back, just an xml 'response' tag to tell the server that we accept it.
}
saslattr(realm, sizeof(realm), in, inlen, "realm");
saslattr(nonce, sizeof(nonce), in, inlen, "nonce");
saslattr(qop, sizeof(qop), in, inlen, "qop");
saslattr(charset, sizeof(charset), in, inlen, "charset");
saslattr(algorithm, sizeof(algorithm), in, inlen, "algorithm");
if (!*realm)
Q_strlcpy(realm, jcl->domain, sizeof(realm));
Q_snprintf(digesturi, sizeof(digesturi), "xmpp/%s", realm);
Q_snprintf(X, sizeof(X), "%s:%s:%s", username, realm, password);
MD5_ToBinary(X, strlen(X), Y, sizeof(Y));
memcpy(A1, Y, 16);
if (*authzid)
Q_snprintf(A1+16, sizeof(A1)-16, ":%s:%s:%s", nonce, cnonce, authzid);
else
Q_snprintf(A1+16, sizeof(A1)-16, ":%s:%s", nonce, cnonce);
Q_snprintf(A2, sizeof(A2), "%s:%s", "AUTHENTICATE", digesturi);
MD5_ToHex(A1, strlen(A1+16)+16, HA1, sizeof(HA1));
MD5_ToHex(A2, strlen(A2), HA2, sizeof(HA2));
Q_snprintf(KD, sizeof(KD), "%s:%s:%s:%s:%s:%s", HA1, nonce, nc, cnonce, qop, HA2);
MD5_ToHex(KD, strlen(KD), Z, sizeof(Z));
if (*authzid)
Q_snprintf(out, outlen, "username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\",nc=\"%s\",qop=\"%s\",digest-uri=\"%s\",response=\"%s\",charset=\"%s\",authzid=\"%s\"",
username, realm, nonce, cnonce, nc, qop, digesturi, Z, charset, authzid);
else
Q_snprintf(out, outlen, "username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\",nc=\"%s\",qop=\"%s\",digest-uri=\"%s\",response=\"%s\",charset=\"%s\"",
username, realm, nonce, cnonce, nc, qop, digesturi, Z, charset);
return strlen(out);
}
static int sasl_scramsha1_initial(jclient_t *jcl, char *buf, int bufsize)
{
if (jcl->allowauth_scramsha1)
{
strcpy(jcl->authnonce, "abcdefghijklmnopqrstuvwxyz"); //FIXME: should be random, to validate that the server knows our password too
Q_snprintf(buf, bufsize, "n,,n=%s,r=%s", jcl->username, jcl->authnonce);
return strlen(buf);
}
return -1;
}
typedef struct
{
int len;
char buf[512];
} buf_t;
static void buf_cat(buf_t *buf, char *data, int len)
{
memcpy(buf->buf + buf->len, data, len);
buf->len += len;
buf->buf[buf->len] = 0;
}
static void buf_cats(buf_t *buf, char *data)
{
buf_cat(buf, data, strlen(data));
}
static void SHA1_Hi(char *out, char *password, int passwordlen, buf_t *salt, int times)
{
char prev[20];
int i, j;
//first iteration is special
buf_cat(salt, "\0\0\0\1", 4);
SHA1_HMAC(prev, sizeof(prev), salt->buf, salt->len, password, passwordlen);
memcpy(out, prev, sizeof(prev));
//later iterations just use the previous iteration
for (i = 1; i < times; i++)
{
SHA1_HMAC(prev, sizeof(prev), prev, sizeof(prev), password, passwordlen);
for (j = 0; j < sizeof(prev); j++)
out[j] ^= prev[j];
}
}
static int sasl_scramsha1_challenge(jclient_t *jcl, char *in, int inlen, char *out, int outlen)
{
//sasl SCRAM-SHA-1 challenge
//send back the same 'r' attribute
buf_t saslchal;
int l, i;
buf_t salt;
buf_t csn;
buf_t itr;
buf_t final;
buf_t sigkey;
char salted_password[20];
char proof[20];
char proof64[30];
char clientkey[20];
char storedkey[20];
char clientsignature[20];
char *username = jcl->username;
char *password = jcl->password;
#if 0
/*hack zone*/
in = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096";
inlen = strlen(in);
strcpy(jcl->authnonce, "wvDh8bTUrSc=");//"fyko+d2lbbFgONRv9qkxdawL");
username = "user";
password = "pencil";
//should result in "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="
#endif
saslchal.len = 0;
buf_cat(&saslchal, in, inlen);
//be warned, these CAN contain nulls.
csn.len = saslattr(csn.buf, sizeof(csn.buf), saslchal.buf, saslchal.len, "r");
salt.len = saslattr(salt.buf, sizeof(salt.buf), saslchal.buf, saslchal.len, "s");
itr.len = saslattr(itr.buf, sizeof(itr.buf), saslchal.buf, saslchal.len, "i");
salt.len = Base64_Decode(salt.buf, sizeof(salt.buf), salt.buf, salt.len);
//FIXME: should we validate that csn is prefixed with our cnonce?
//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_cats(&final, "c=");
Base64_Add("n,,", 3);
Base64_Finish();
buf_cat(&final, base64, strlen(base64));
buf_cats(&final, ",r=");
buf_cat(&final, csn.buf, csn.len);
//our original message + ',' + challenge + ',' + the message we're about to send.
sigkey.len = 0;
buf_cats(&sigkey, "n=");
buf_cats(&sigkey, username);
buf_cats(&sigkey, ",r=");
buf_cats(&sigkey, jcl->authnonce);
buf_cats(&sigkey, ",");
buf_cat(&sigkey, saslchal.buf, saslchal.len);
buf_cats(&sigkey, ",");
buf_cat(&sigkey, final.buf, final.len);
SHA1_Hi(salted_password, password, strlen(password), &salt, atoi(itr.buf));
SHA1_HMAC(clientkey, sizeof(clientkey), "Client Key", strlen("Client Key"), salted_password, sizeof(salted_password));
SHA1(storedkey, sizeof(storedkey), clientkey, sizeof(clientkey));
SHA1_HMAC(clientsignature, sizeof(clientsignature), sigkey.buf, sigkey.len, storedkey, sizeof(storedkey));
for (i = 0; i < sizeof(proof); i++)
proof[i] = clientkey[i] ^ clientsignature[i];
Base64_Add(proof, sizeof(proof));
Base64_Finish();
//"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="
Q_snprintf(out, outlen, "%s,p=%s", final.buf, base64);
return strlen(out);
}
void URL_Split(char *url, char *proto, int protosize, char *host, int hostsize, char *res, int ressize)
{
char *s;
*proto = 0;
*host = 0;
*res = 0;
s = strchr(url, ':');
if (!s)
return;
protosize = min(protosize-1, s-url);
memcpy(proto, url, protosize);
proto[protosize] = 0;
s++;
if (s[0] == '/' && s[1] == '/')
s+=2;
url = s;
s = strchr(url, '/');
if (!s)
s = url+strlen(url);
hostsize = min(hostsize-1, s-url);
memcpy(host, url, hostsize);
host[hostsize] = 0;
url = s;
s = url+strlen(url);
ressize = min(ressize-1, s-url);
memcpy(res, url, ressize);
res[ressize] = 0;
}
void Q_strlcat_urlencode(char *d, const char *s, int n)
{
char hex[16] = "0123456789ABCDEF";
int clen = strlen(d);
d += clen;
n -= clen;
n--;
if (s)
while (*s)
{
if ((*s >= '0' && *s <= '9') ||
(*s >= 'a' && *s <= 'z') ||
(*s >= 'A' && *s <= 'Z') ||
*s == '-' || *s == '_' || *s == '.')
{
if (!n)
break;
n--;
*d++ = *s++;
}
else
{
if (n < 3)
break;
n -= 3;
*d++ = '%';
*d++ = hex[*s>>4];
*d++ = hex[*s&15];
s++;
}
}
*d = 0;
}
static int sasl_oauth2_initial(jclient_t *jcl, char *buf, int bufsize)
{
char msg[4096];
char proto[256];
char host[256];
char resource[256];
int sock, l, rl=0;
char result[8192];
xmltree_t *x;
if (*jcl->password)
return -1;
if (0)//*jcl->password)
{
char body[4096];
char header[4096];
URL_Split(jcl->oauth2.refreshurl, proto, sizeof(proto), host, sizeof(host), resource, sizeof(resource));
*body = 0;
Q_strlcat(body, "client_id=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientid, sizeof(body));
Q_strlcat(body, "&client_secret=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientsecret, sizeof(body));
Q_strlcat(body, "&", sizeof(body));
Q_strlcat(body, "grant_type=password&username=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.useraccount, sizeof(body));
Q_strlcat(body, "&password=", sizeof(body));
Q_strlcat_urlencode(body, jcl->password, sizeof(body));
Q_strlcat(body, "&response_type=code", sizeof(body));
Q_strlcat(body, "&redirect_uri=", sizeof(body));
Q_strlcat_urlencode(body, "urn:ietf:wg:oauth:2.0:oob", sizeof(body));
Q_strlcat(body, "&scope=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.scope, sizeof(body));
Q_snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
//"Authorization: Basic %s\r\n"
"Content-length: %i\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Connection: close\r\n"
"\r\n",
resource,
host,
strlen(body));
sock = pNet_TCPConnect(host, 443);
pNet_SetTLSClient(sock, host);
pNet_Send(sock, header, strlen(header));
pNet_Send(sock, body, strlen(body));
while(1)
{
l = pNet_Recv(sock, result+rl, sizeof(result)-rl);
if (l < 0)
break;
else
rl += l;
}
pNet_Close(sock);
result[rl] = 0;
Con_Printf("Got %s\n", result);
}
//if we have nothing, load up a browser to ask for the first token
if (!*jcl->oauth2.refreshtoken && !*jcl->oauth2.authtoken)
{
char url[4096];
*url = 0;
Q_strlcat(url, jcl->oauth2.obtainurl, sizeof(url));
Q_strlcat(url, "?redirect_uri=", sizeof(url));
Q_strlcat_urlencode(url, "urn:ietf:wg:oauth:2.0:oob", sizeof(url));
Q_strlcat(url, "&%72esponse_type=code&client_id=", sizeof(url)); //%72 = r. fucking ezquake colour codes. works with firefox anyway. no idea if that's the server changing it to an r or not. :s
Q_strlcat_urlencode(url, jcl->oauth2.clientid, sizeof(url));
Q_strlcat(url, "&scope=", sizeof(url));
Q_strlcat_urlencode(url, jcl->oauth2.scope, sizeof(url));
Q_strlcat(url, "&access_type=offline", sizeof(url));
Q_strlcat(url, "&login_hint=", sizeof(url));
Q_strlcat_urlencode(url, jcl->oauth2.useraccount, sizeof(url));
Con_Printf("Please visit ^[^4%s\\url\\%s^] and then enter:\n^[/"COMMANDPREFIX"%i /oa2token <TOKEN>^]\n", url, url, jcl->accountnum);
//wait for user to act.
return -2;
}
//refresh token is not known, try and get one
if (!*jcl->oauth2.refreshtoken && *jcl->oauth2.authtoken)
{
xmltree_t *x;
char body[4096];
char header[4096];
//send a refresh request
*body = 0;
Q_strlcat(body, "code=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.authtoken, sizeof(body));
Q_strlcat(body, "&client_id=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientid, sizeof(body));
Q_strlcat(body, "&client_secret=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientsecret, sizeof(body));
Q_strlcat(body, "&redirect_uri=", sizeof(body));
Q_strlcat_urlencode(body, "urn:ietf:wg:oauth:2.0:oob", sizeof(body));
Q_strlcat(body, "&grant_type=", sizeof(body));
Q_strlcat_urlencode(body, "authorization_code", sizeof(body));
URL_Split(jcl->oauth2.refreshurl, proto, sizeof(proto), host, sizeof(host), resource, sizeof(resource));
Q_snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
//"Authorization: Basic %s\r\n"
"Content-length: %i\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"user-agent: fteqw-plugin-xmpp\r\n"
"Connection: close\r\n"
"\r\n",
resource, host, strlen(body));
Con_Printf("XMPP: Requesting access token\n");
sock = pNet_TCPConnect(host, 443);
pNet_SetTLSClient(sock, host);
pNet_Send(sock, header, strlen(header));
pNet_Send(sock, body, strlen(body));
while(1)
{
l = pNet_Recv(sock, result+rl, sizeof(result)-rl);
if (l < 0)
break;
else
rl += l;
}
result[rl] = 0;
pNet_Close(sock);
//should contain something like:
//{
//"access_token" : "ya29.AHES6ZR-_Sx0UpexZdgqQwR8LFqTx-GFi-Zrq4nKrcLLA98N7g",
//"token_type" : "Bearer",
//"expires_in" : 3600
//}
l = strstr(result, "\r\n\r\n")-result;
l+= 4;
if (l < 0 || l > rl)
l = rl;
x = XML_FromJSON(NULL, "oauth2", result, &l, rl);
XML_ConPrintTree(x, 1);
free(jcl->oauth2.accesstoken);
free(jcl->oauth2.refreshtoken);
jcl->oauth2.accesstoken = strdup(XML_GetChildBody(x, "access_token", ""));
// jcl->oauth2.token_type = strdup(XML_GetChildBody(x, "token_type", ""));
// jcl->oauth2.expires_in = strdup(XML_GetChildBody(x, "expires_in", ""));
jcl->oauth2.refreshtoken = strdup(XML_GetChildBody(x, "refresh_token", ""));
//in theory, the auth token is no longer valid/needed
free(jcl->oauth2.authtoken);
jcl->oauth2.authtoken = strdup("");
}
//refresh our refresh token, obtaining a usable sign-in token at the same time.
else if (!*jcl->oauth2.accesstoken)
{
char body[4096];
char header[4096];
char *newrefresh;
//send a refresh request
*body = 0;
Q_strlcat(body, "client_id=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientid, sizeof(body));
Q_strlcat(body, "&client_secret=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.clientsecret, sizeof(body));
Q_strlcat(body, "&grant_type=", sizeof(body));
Q_strlcat_urlencode(body, "refresh_token", sizeof(body));
Q_strlcat(body, "&refresh_token=", sizeof(body));
Q_strlcat_urlencode(body, jcl->oauth2.refreshtoken, sizeof(body));
URL_Split(jcl->oauth2.refreshurl, proto, sizeof(proto), host, sizeof(host), resource, sizeof(resource));
Q_snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
//"Authorization: Basic %s\r\n"
"Content-length: %i\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"user-agent: fteqw-plugin-xmpp\r\n"
"Connection: close\r\n"
"\r\n",
resource, host, strlen(body));
Con_Printf("XMPP: Refreshing access token\n");
sock = pNet_TCPConnect(host, 443);
pNet_SetTLSClient(sock, host);
pNet_Send(sock, header, strlen(header));
pNet_Send(sock, body, strlen(body));
while(1)
{
l = pNet_Recv(sock, result+rl, sizeof(result)-rl);
if (l < 0)
break;
else
rl += l;
}
pNet_Close(sock);
result[rl] = 0;
l = strstr(result, "\r\n\r\n")-result;
l+= 4;
if (l < 0 || l > rl)
l = rl;
x = XML_FromJSON(NULL, "oauth2", result, &l, rl);
XML_ConPrintTree(x, 1);
newrefresh = XML_GetChildBody(x, "refresh_token", NULL);
free(jcl->oauth2.accesstoken);
jcl->oauth2.accesstoken = strdup(XML_GetChildBody(x, "access_token", ""));
if (newrefresh || !*jcl->oauth2.accesstoken)
{
free(jcl->oauth2.refreshtoken);
jcl->oauth2.refreshtoken = strdup(XML_GetChildBody(x, "refresh_token", ""));
}
// jcl->oauth2.token_type = strdup(XML_GetChildBody(x, "token_type", ""));
// jcl->oauth2.expires_in = strdup(XML_GetChildBody(x, "expires_in", ""));
//refresh token may mutate. follow the mutation.
}
else if (*jcl->oauth2.accesstoken)
Con_Printf("XMPP: Using explicit access token\n");
if (*jcl->oauth2.accesstoken)
{
int len = 0;
if (*jcl->oauth2.useraccount)
{
//realm isn't specified
buf[len++] = 0;
memcpy(buf+len, jcl->oauth2.useraccount, strlen(jcl->oauth2.useraccount));
len += strlen(jcl->oauth2.useraccount);
buf[len++] = 0;
}
memcpy(buf+len, jcl->oauth2.accesstoken, strlen(jcl->oauth2.accesstoken));
len += strlen(jcl->oauth2.accesstoken);
Con_Printf("XMPP: Signing in\n");
free(jcl->oauth2.accesstoken);
jcl->oauth2.accesstoken = strdup("");
return len;
}
//if the reply has a refresh token in it, clear out any password info
return -1;
}
//in descending priority order
saslmethod_t saslmethods[] =
{
{NULL, sasl_oauth2_initial, NULL}, //potentially avoids having to ask+store their password. a browser is required to obtain auth token for us.
{"SCRAM-SHA-1", sasl_scramsha1_initial, sasl_scramsha1_challenge}, //lots of unreadable hashing
{"DIGEST-MD5", sasl_digestmd5_initial, sasl_digestmd5_challenge}, //kinda silly
{"PLAIN", sasl_plain_initial, NULL} //realm\0username\0password
};
/*
pidgin's msn request
https://oauth.live.com/authorize?client_id=000000004C07035A&scope=wl.messenger,wl.basic,wl.offline_access,wl.contacts_create,wl.share&response_type=token&redirect_uri=http://pidgin.im/
*/
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);
void JCL_CloseConnection(jclient_t *jcl, qboolean reconnect);
void JCL_JoinMUCChat(jclient_t *jcl, char *room, char *server, char *myhandle, char *password);
void JCL_GenLink(jclient_t *jcl, char *out, int outlen, char *action, char *context, char *contextres, char *sid, char *txtfmt, ...)
{
va_list argptr;
qboolean textonly = false;
*out = 0;
if (!strchr(txtfmt, '%'))
{
Q_strlcpy(out, "bad link text", outlen);
return;
}
if (textonly)
{
va_start(argptr, txtfmt);
Q_strlcat(out, "[", outlen);
Q_vsnprintf(out, outlen, txtfmt, argptr);
Q_strlcat(out, "]", outlen);
va_end(argptr);
return;
}
Q_strlcat(out, "^[[", outlen);
va_start(argptr, txtfmt);
Q_vsnprintf(out+3, outlen-3, txtfmt, argptr);
va_end(argptr);
Q_strlcat(out, "]", outlen);
if (jcl && jcl->accountnum)
{
char acc[32];
Q_snprintf(acc, sizeof(acc), "%i", jcl->accountnum);
Q_strlcat(out, "\\xmppacc\\", outlen);
Q_strlcat(out, acc, outlen);
}
if (action)
{
Q_strlcat(out, "\\xmppact\\", outlen);
Q_strlcat(out, action, outlen);
}
if (context)
{
Q_strlcat(out, "\\xmpp\\", outlen);
Q_strlcat(out, context, outlen);
if (contextres)
{
Q_strlcat(out, "/", outlen);
Q_strlcat(out, contextres, outlen);
}
}
if (sid)
{
Q_strlcat(out, "\\xmppsid\\", outlen);
Q_strlcat(out, sid, outlen);
}
Q_strlcat(out, "^]", outlen);
}
char *TrimResourceFromJid(char *jid)
{
char *slash;
slash = strchr(jid, '/');
if (slash)
{
*slash = '\0';
return slash+1;
}
return NULL;
}
#ifdef JINGLE
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];
char stunhost[256];
if (piceapi)
ice = piceapi->ICE_Create(NULL, sid, with, method, mediatype);
if (ice)
{
piceapi->ICE_Get(ice, "sid", generatedname, sizeof(generatedname));
sid = generatedname;
//note: the engine will ignore codecs it does not support.
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;
//query dns to see if there's a stunserver hosted by the same domain
//nslookup -querytype=SRV _stun._udp.example.com
//for msn, live.com has one, messanger.live.com has one, but messenger.live.com does NOT. seriously, the typo has more services. wtf microsoft?
//google doesn't provide a stun srv entry
//facebook doesn't provide a stun srv entry
Q_snprintf(stunhost, sizeof(stunhost), "_stun._udp.%s", jcl->domain);
if (NET_DNSLookup_SRV(stunhost, stunhost, sizeof(stunhost)))
piceapi->ICE_Set(ice, "stunip", stunhost);
else
{
//there is no real way to query stun servers from the xmpp server.
//while there is some extdisco extension (aka: the 'standard' way), it has some huge big fat do-not-implement message (and googletalk does not implement it).
//google provide their own jingleinfo extension. Which also has some huge fat 'do-not-implement' message. hardcoding the address should provide equivelent longevity. :(
//google also don't provide stun srv records.
//so we're basically screwed if we want to work with the googletalk xmpp service long term.
//more methods are best, I suppose, but I'm lazy.
//yes, hardcoding means that other services might 'borrow' googles' stun servers.
piceapi->ICE_Set(ice, "stunport", "19302");
piceapi->ICE_Set(ice, "stunip", "stun.l.google.com");
}
//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)
{
char convolink[512], hanguplink[512];
c2c = JCL_JingleCreateSession(jcl, target, true, sid, DEFAULTICEMODE, ((protocol == ICEP_INVALID)?ICEP_QWCLIENT:protocol));
JCL_JingleSend(jcl, c2c, "session-initiate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
JCL_GenLink(jcl, hanguplink, sizeof(hanguplink), "jdeny", target, NULL, c2c->sid, "%s", "Hang Up");
Con_SubPrintf(b->name, "%s %s %s.\n", protocol==ICEP_VOICE?"Calling":"Requesting session with", convolink, hanguplink);
}
else
Con_SubPrintf(b->name, "That session has expired.\n");
}
else if (c2c->creator)
{
char convolink[512];
//resend initiate if they've not acked it... I dunno...
JCL_JingleSend(jcl, c2c, "session-initiate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
Con_SubPrintf(b->name, "Restarting session with %s.\n", convolink);
}
else if (c2c->accepted)
Con_SubPrintf(b->name, "That session was already accepted.\n");
else
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "session-accept");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
Con_SubPrintf(b->name, "Accepting session from %s.\n", convolink);
}
}
else
{
if (c2c)
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "session-terminate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, target, NULL, NULL, "%s", target);
Con_SubPrintf(b->name, "Terminating session with %s.\n", convolink);
}
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)
{
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 = "wants to join your game";
autocvar = "xmpp_autoacceptjoins";
}
else if (!strcmp(host, "me"))
{
mt = ICEP_QWCLIENT;
offer = "wants to 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 = "is trying to call 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];
//note: the engine will ignore codecs it does not support, returning false.
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.
//we really ought to give a reason, but we're rude.
if (!okay)
{
char convolink[512];
JCL_JingleSend(jcl, c2c, "session-terminate");
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
Con_SubPrintf(b->name, "%s does not support any compatible audio codecs, and is unable to call you.\n", convolink);
return false;
}
}
JCL_JingleParsePeerPorts(jcl, c2c, inj, from);
if (c2c->mediatype != ICEP_INVALID)
{
char convolink[512];
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, from, NULL, NULL, "%s", b->name);
if (!pCvar_GetFloat(autocvar))
{
char authlink[512];
char denylink[512];
JCL_GenLink(jcl, authlink, sizeof(authlink), "jauth", from, NULL, sid, "%s", "Accept");
JCL_GenLink(jcl, denylink, sizeof(denylink), "jdeny", from, NULL, sid, "%s", "Reject");
//show a prompt for it, send the reply when the user decides.
Con_SubPrintf(b->name,
"%s %s. %s %s\n", convolink, offer, authlink, denylink);
pCon_SetActive(b->name);
return true;
}
else
{
Con_SubPrintf(b->name, "Auto-accepting session from %s\n", convolink);
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;
}
#endif
qintptr_t JCL_ConsoleLink(qintptr_t *args)
{
jclient_t *jcl;
char text[256];
char link[256];
char who[256];
char what[256];
char which[256];
int i;
// 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));
JCL_Info_ValueForKey(link, "xmppacc", which, sizeof(which));
if (!*who && !*what)
return false;
i = atoi(which);
i = bound(0, i, sizeof(jclients)/sizeof(jclients[0]));
jcl = jclients[i];
if (!strcmp(what, "pauth"))
{
//we should friend them too.
if (jcl && jcl->status == JCL_ACTIVE)
JCL_AddClientMessagef(jcl, "<presence to='%s' type='subscribed'/>", who);
return true;
}
else if (!strcmp(what, "pdeny"))
{
if (jcl && jcl->status == JCL_ACTIVE)
JCL_AddClientMessagef(jcl, "<presence to='%s' type='unsubscribed'/>", who);
return true;
}
#ifdef JINGLE
else if (!strcmp(what, "jauth"))
{
JCL_Info_ValueForKey(link, "xmppsid", what, sizeof(what));
if (jcl && jcl->status == JCL_ACTIVE)
JCL_Join(jcl, who, what, true, ICEP_INVALID);
return true;
}
else if (!strcmp(what, "jdeny"))
{
JCL_Info_ValueForKey(link, "xmppsid", what, sizeof(what));
if (jcl && jcl->status == JCL_ACTIVE)
JCL_Join(jcl, who, what, false, ICEP_INVALID);
return true;
}
else if (!strcmp(what, "join"))
{
if (jcl && jcl->status == JCL_ACTIVE)
JCL_Join(jcl, who, NULL, true, ICEP_QWCLIENT);
return true;
}
else if (!strcmp(what, "invite"))
{
if (jcl && jcl->status == JCL_ACTIVE)
JCL_Join(jcl, who, NULL, true, ICEP_QWSERVER);
return true;
}
else if (!strcmp(what, "call"))
{
if (jcl && jcl->status == JCL_ACTIVE)
JCL_Join(jcl, who, NULL, true, ICEP_VOICE);
return true;
}
#endif
else if (!strcmp(what, "mucjoin"))
{ //conference/chat join
JCL_Info_ValueForKey(link, "xmppsid", what, sizeof(what));
JCL_JoinMUCChat(jcl, who, NULL, NULL, what);
}
else if ((*who && !*what) || !strcmp(what, "msg"))
{
if (jcl)
{
char *f;
buddy_t *b;
bresource_t *br;
JCL_FindBuddy(jcl, *who?who:jcl->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];
jclient_t *jcl = jclients[0];
if (!jcl)
{
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 = jcl->buddies; b; b = b->next)
{
if (!strcmp(b->name, consolename))
{
if (b->defaultresource)
Q_snprintf(jcl->defaultdest, sizeof(jcl->defaultdest), "%s/%s", b->accountdomain, b->defaultresource->resource);
else
Q_snprintf(jcl->defaultdest, sizeof(jcl->defaultdest), "%s", b->accountdomain);
break;
}
}
JCL_Command(0, consolename);
return true;
}
void JCL_FlushOutgoing(jclient_t *jcl)
{
int sent;
if (!jcl || !jcl->outbuflen || jcl->socket == -1)
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 if (sent < 0)
Con_Printf("Error sending\n");
// 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[4096];
va_start (argptr, fmt);
Q_vsnprintf (body, sizeof(body), fmt, argptr);
va_end (argptr);
JCL_AddClientMessageString(jcl, body);
}
qboolean JCL_Reconnect(jclient_t *jcl)
{
char *serveraddr;
//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));
jcl->authmode = -1;
if (*jcl->redirserveraddr)
serveraddr = jcl->redirserveraddr;
else
serveraddr = jcl->serveraddr;
if (!*serveraddr)
{
//jcl->tlsconnect requires an explicit hostname, so should not be able to take this path.
char srv[256];
char srvserver[256];
Q_snprintf(srv, sizeof(srv), "_xmpp-client._tcp.%s", jcl->domain);
if (NET_DNSLookup_SRV(srv, srvserver, sizeof(srvserver)))
{
Con_Printf("XMPP: Trying to connect to %s (%s)\n", jcl->domain, srvserver);
jcl->socket = pNet_TCPConnect(srvserver, jcl->serverport); //port is should already be part of the srvserver name
}
else
{
Con_Printf("XMPP: Unable to determine service (%s). Check internet connection.\n", jcl->domain);
return false;
}
}
else
{
Con_Printf("XMPP: Trying to connect to %s\n", jcl->domain);
jcl->socket = pNet_TCPConnect(serveraddr, jcl->serverport); //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->forcetls==2)
if (pNet_SetTLSClient(jcl->socket, jcl->certificatedomain)>=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_ConnectXML(xmltree_t *acc)
{
//this is a list of xmpp networks that use oauth2 for which we have a clientid registered. this allows us to get the right oauth defaults.
struct
{
char *domain;
char *saslmethod;
char *obtainurl;
char *refreshurl;
char *revokeurl;
char *scope;
char *clientid;
char *clientsecret;
//char *signouturl;
//char *clientregistrationurl;
} *oa, dfltoauth[] = {
{
"gmail.com",
"X-OAUTH2",
"https://accounts.google.com/o/oauth2/auth", //authorize
"https://accounts.google.com/o/oauth2/token", //refresh
"https://accounts.google.com/o/oauth2/revoke", //revoke
"https://www.googleapis.com/auth/googletalk", //scope
"1060926168015.apps.googleusercontent.com", //clientid
"mptCRRTE5I626npsnoZ_RqoG" //client secrit. there really is no securing this. I'll just have to avoid any pay-for google apis. *shrug*
//"https://accounts.google.com/IssuedAuthSubTokens"
//"https://code.google.com/apis/console"
},
/*
{
"messenger.live.com",
"X-MESSENGER-OAUTH2",
"https://oauth.live.com/authorize",
"https://oauth.live.com/token",
"", //FIXME fill in revoke url
"wl.messenger,wl.basic,wl.offline_access,wl.contacts_create,wl.share", //no idea what's actually needed.
"", //client-id - none registered. go register it yourself.
"" //client-secret - client not registered, go do it yourself.
//""
//"https://manage.dev.live.com/"
},
*/
/*
{
"messenger.live.com",
"X-FACEBOOK-PLATFORM",
"", //FIXME fill in obtain url
"", //FIXME fill in refresh url
"", //FIXME fill in revoke url
"", //FIXME: fill in scope
"", //client-id - none registered. go register it yourself.
"" //client-secret - client not registered, go do it yourself.
//""
//""
},
*/
{
NULL,
"",
"",
"",
"",
"",
"",
""
}
};
jclient_t *jcl;
xmltree_t *oauth2;
char oauthname[256];
jcl = malloc(sizeof(jclient_t));
if (!jcl)
return NULL;
memset(jcl, 0, sizeof(jclient_t));
jcl->socket = -1;
jcl->accountnum = atoi(XML_GetParameter(acc, "id", "1"));
//make sure dependant properties are listed beneath their dependancies...
jcl->forcetls = atoi(XML_GetChildBody(acc, "forcetls", "1"));
jcl->streamdebug = !!atoi(XML_GetChildBody(acc, "streamdebug", "0"));
Q_strlcpy(jcl->serveraddr, XML_GetChildBody(acc, "serveraddr", ""), sizeof(jcl->serveraddr));
jcl->serverport = atoi(XML_GetChildBody(acc, "serverport", (jcl->forcetls==2)?"5223":"5222"));
Q_strlcpy(jcl->username, XML_GetChildBody(acc, "username", "user"), sizeof(jcl->username));
Q_strlcpy(jcl->domain, XML_GetChildBody(acc, "domain", "localhost"), sizeof(jcl->domain));
Q_strlcpy(jcl->resource, XML_GetChildBody(acc, "resource", ""), sizeof(jcl->resource));
//half these networks seem to have weird domains. especially microsoft.
if (strchr(jcl->username, '@'))
Q_strlcpy(oauthname, jcl->username, sizeof(oauthname));
else
Q_snprintf(oauthname, sizeof(oauthname), "%s@%s", jcl->username, jcl->domain);
for (oa = dfltoauth; oa->domain; oa++)
{
if (!strcmp(oa->domain, jcl->domain))
break;
}
oauth2 = XML_ChildOfTree(acc, "oauth2", 0);
Q_strlcpy(jcl->oauth2.saslmethod, XML_GetParameter(oauth2, "method", oa->saslmethod), sizeof(jcl->oauth2.saslmethod));
Q_strlcpy(jcl->oauth2.obtainurl, XML_GetChildBody(oauth2, "obtain-url", oa->obtainurl), sizeof(jcl->oauth2.obtainurl));
Q_strlcpy(jcl->oauth2.refreshurl, XML_GetChildBody(oauth2, "refresh-url", oa->refreshurl), sizeof(jcl->oauth2.refreshurl));
Q_strlcpy(jcl->oauth2.clientid, XML_GetChildBody(oauth2, "client-id", oa->clientid), sizeof(jcl->oauth2.clientid));
Q_strlcpy(jcl->oauth2.clientsecret, XML_GetChildBody(oauth2, "client-secret", oa->clientsecret), sizeof(jcl->oauth2.clientsecret));
jcl->oauth2.scope = strdup(XML_GetChildBody(oauth2, "scope", oa->scope));
jcl->oauth2.useraccount = strdup(XML_GetChildBody(oauth2, "authname", oauthname));
jcl->oauth2.authtoken = strdup(XML_GetChildBody(oauth2, "auth-token", ""));
jcl->oauth2.refreshtoken = strdup(XML_GetChildBody(oauth2, "refresh-token", ""));
jcl->oauth2.accesstoken = strdup(XML_GetChildBody(oauth2, "access-token", ""));
Q_strlcpy(jcl->password, XML_GetChildBody(acc, "password", ""), sizeof(jcl->password));
if (!*jcl->resource)
{ //the default resource matches the game that they're trying to play.
char gamename[64], *res, *o;
if (pCvar_GetString("fs_gamename", gamename, sizeof(gamename)))
{
//strip out any weird chars (namely whitespace)
for (o = gamename, res = gamename; *res; )
{
if (*res == ' ' || *res == '\t')
res++;
else
*o++ = *res++;
}
if (!*gamename)
Q_strlcpy(jcl->resource, "FTE", sizeof(jcl->resource));
else
Q_strlcpy(jcl->resource, gamename, sizeof(jcl->resource));
}
}
Q_strlcpy(jcl->certificatedomain, XML_GetChildBody(acc, "certificatedomain", jcl->domain), sizeof(jcl->certificatedomain));
jcl->status = atoi(XML_GetChildBody(acc, "inactive", "0"))?JCL_INACTIVE:JCL_DEAD;
jcl->allowauth_plainnontls = atoi(XML_GetChildBody(acc, "allowauth_plain_nontls", "0"));
jcl->allowauth_plaintls = atoi(XML_GetChildBody(acc, "allowauth_plain_tls", "1")); //required 1 for googletalk, otherwise I'd set it to 0.
jcl->allowauth_digestmd5 = atoi(XML_GetChildBody(acc, "allowauth_digest_md5", "1"));
jcl->allowauth_scramsha1 = atoi(XML_GetChildBody(acc, "allowauth_scram_sha_1", "1"));
jcl->allowauth_oauth2 = atoi(XML_GetChildBody(acc, "allowauth_oauth2", jcl->oauth2.saslmethod?"1":"0"));
return jcl;
}
jclient_t *JCL_Connect(int accnum, char *server, int forcetls, char *account, char *password)
{
char srvserver[256];
char gamename[64];
jclient_t *jcl;
char *domain;
char *res;
xmltree_t *x;
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 (forcetls>0)
{
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;
}
}
x = XML_CreateNode(NULL, "account", "", "");
XML_AddParameteri(x, "id", accnum);
XML_CreateNode(x, "serveraddr", "", server);
XML_CreateNode(x, "username", "", account);
XML_CreateNode(x, "domain", "", domain);
XML_CreateNode(x, "resource", "", res);
XML_CreateNode(x, "password", "", password);
jcl = JCL_ConnectXML(x);
XML_Destroy(x);
return jcl;
}
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;
}
}
//FIXME: add flags to avoid creation
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;
if (!jid)
{
if (buddy)
*buddy = NULL;
if (bres)
*bres = NULL;
return false;
}
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, false);
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)
{
char myjid[512];
Q_strlcpy(jcl->jid, c->body, sizeof(jcl->jid));
JCL_GenLink(jcl, myjid, sizeof(myjid), NULL, jcl->jid, NULL, NULL, "%s", jcl->jid);
Con_Printf("Bound to jid %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",
#ifdef JINGLE
"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
#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.
//file transfer
#ifdef FILETRANSFER
"http://jabber.org/protocol/si",
"http://jabber.org/protocol/si/profile/file-transfer",
"http://jabber.org/protocol/ibb",
//"http://jabber.org/protocol/bytestreams",
#endif
#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;
#ifdef FILETRANSFERS
ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "open", 0);
if (ot)
{
struct ft_s *ft;
char *sid = XML_GetParameter(ot, "sid", "");
int blocksize = atoi(XML_GetParameter(ot, "block-size", "4096")); //technically this is required.
char *stanza = XML_GetParameter(ot, "stanza", "iq");
for (ft = jcl->ft; ft; ft = ft->next)
{
if (!strcmp(ft->sid, sid))
{
if (!ft->begun && ft->transmitting == false)
{
if (blocksize > 65536 || strcmp(stanza, "iq"))
{ //blocksize: MUST NOT be greater than 65535
JCL_AddClientMessagef(jcl,
"<iq id='%s' to='%s' type='error'>"
"<error type='modify'>"
"<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"</error>"
"</iq>"
, id, from);
}
else if (blocksize > 4096)
{ //ask for smaller chunks
JCL_AddClientMessagef(jcl,
"<iq id='%s' to='%s' type='error'>"
"<error type='modify'>"
"<resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"</error>"
"</iq>"
, id, from);
}
else
{ //it looks okay
pFS_Open(ft->fname, &ft->file, 2);
ft->method = FT_IBB;
ft->blocksize = blocksize;
ft->begun = true;
//if its okay...
JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", id, from);
}
return;
}
}
}
}
ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "close", 0);
if (ot)
{
struct ft_s **link, *ft;
char *sid = XML_GetParameter(ot, "sid", "");
for (link = &jcl->ft; *link; link = &(*link)->next)
{
ft = *link;
if (!strcmp(ft->sid, sid))
{
if (ft->begun && ft->method == FT_IBB)
{
int size;
pFS_Close(ft->file);
if (ft->transmitting)
{
Con_Printf("%s aborted transfer of \"%s\"\n", from, ft->fname);
}
else
{
size = pFS_Open(ft->fname, &ft->file, 1);
pFS_Close(ft->file);
if (size == ft->size)
Con_Printf("Received file \"%s\" successfully\n", ft->fname);
else
Con_Printf("%s aborted transfer of \"%s\"\n", from, ft->fname);
}
*link = ft->next;
free(ft);
//if its okay...
JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", id, from);
return;
}
}
}
}
ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "data", 0);
if (ot)
{
char block[65536];
char *sid = XML_GetParameter(ot, "sid", "");
unsigned short seq = atoi(XML_GetParameter(ot, "seq", "0"));
int blocksize;
struct ft_s *ft;
for (ft = jcl->ft; ft; ft = ft->next)
{
if (!strcmp(ft->sid, sid) && !ft->transmitting)
{
blocksize = Base64_Decode(block, sizeof(block), ot->body, strlen(ot->body));
if (blocksize && blocksize <= ft->blocksize)
{
pFS_Write(ft->file, block, blocksize);
JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", id, from);
return;
}
else
Con_Printf("XMPP: Invalid blocksize in file transfer from %s\n", from);
break;
}
}
}
ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/si", "si", 0);
if (ot)
{
char *profile = XML_GetParameter(ot, "profile", "");
unparsable = false;
if (!strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer"))
{
char *s;
xmltree_t *repiq, *repsi, *c;
char *mimetype = XML_GetParameter(ot, "mime-type", "text/plain");
char *sid = XML_GetParameter(ot, "id", "");
xmltree_t *file = XML_ChildOfTreeNS(ot, "http://jabber.org/protocol/si/profile/file-transfer", "file", 0);
char *fname = XML_GetParameter(file, "name", "file.txt");
char *date = XML_GetParameter(file, "date", "");
char *md5hash = XML_GetParameter(file, "hash", "");
int fsize = strtoul(XML_GetParameter(file, "size", "0"), NULL, 0);
char *desc = XML_GetChildBody(file, "desc", "");
//file transfer offer
struct ft_s *ft = malloc(sizeof(*ft));
memset(ft, 0, sizeof(*ft));
ft->next = jcl->ft;
jcl->ft = ft;
ft->transmitting = false;
Q_strlcpy(ft->sid, sid, sizeof(ft->sid));
Q_strlcpy(ft->fname, fname, sizeof(ft->sid));
Base64_Decode(ft->md5hash, sizeof(ft->md5hash), md5hash, strlen(md5hash));
ft->size = fsize;
ft->file = -1;
// ft->with =
ft->method = FT_IBB;
ft->begun = false;
Con_Printf("Receiving file \"%s\" from \"%s\" (%i bytes)\n", fname, from, fsize);
//generate a response.
//FIXME: we ought to delay response until after we prompt.
repiq = XML_CreateNode(NULL, "iq", "", "");
XML_AddParameter(repiq, "type", "result");
XML_AddParameter(repiq, "to", from);
XML_AddParameter(repiq, "id", id);
repsi = XML_CreateNode(repiq, "si", "http://jabber.org/protocol/si", "");
XML_CreateNode(repsi, "file", "http://jabber.org/protocol/si/profile/file-transfer", ""); //I don't really get the point of this.
c = XML_CreateNode(repsi, "feature", "http://jabber.org/protocol/feature-neg", "");
c = XML_CreateNode(c, "x", "jabber:x:data", "");
XML_AddParameter(c, "type", "submit");
c = XML_CreateNode(c, "field", "", "");
XML_AddParameter(c, "var", "stream-method");
if (ft->method == FT_IBB)
c = XML_CreateNode(c, "value", "", "http://jabber.org/protocol/ibb");
else if (ft->method == FT_BYTESTREAM)
c = XML_CreateNode(c, "value", "", "http://jabber.org/protocol/bytestreams");
s = XML_GenerateString(repiq, false);
JCL_AddClientMessageString(jcl, s);
free(s);
XML_Destroy(repiq);
}
else
{
//profile not understood
JCL_AddClientMessagef(jcl,
"<iq type='error' to='%s' id='%s'>"
"<error code='400' type='cancel'>"
"<bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"<bad-profile xmlns='http://jabber.org/protocol/si'/>"
"</error>"
"</iq>", from, id);
}
}
#endif
c = XML_ChildOfTree(tree, "query", 0);
if (c && !strcmp(c->xmlns, "jabber:iq:roster"))
{
unparsable = false;
JCL_RosterUpdate(jcl, c);
}
#ifdef JINGLE
c = XML_ChildOfTree(tree, "jingle", 0);
if (c && !strcmp(c->xmlns, "urn:xmpp:jingle:1"))
{
unparsable = !JCL_ParseJingle(jcl, c, from, id);
}
#endif
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);
char *type = XML_GetParameter(tree, "type", "normal");
char *ctx;
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);
ctx = b->name;
if (!strcmp(type, "groupchat"))
f = br->resource;
else if (b->chatroom) //need to use the full resource for private chat within a room
{
ctx = f;
f = br->resource;
}
else
{
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(ctx, "%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(ctx, "%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(ctx, "\r", f);
}
ot = XML_ChildOfTree(tree, "active", 0);
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
{
unparsable = false;
Con_SubPrintf(ctx, "\r", f);
}
ot = XML_ChildOfTree(tree, "gone", 0);
if (ot && !strcmp(ot->xmlns, "http://jabber.org/protocol/chatstates"))
{
unparsable = false;
Con_SubPrintf(ctx, "%s has gone away\r", f);
}
}
ot = XML_ChildOfTree(tree, "attention", 0);
if (ot)
{
if (jclient_poketime < jclient_curtime) //throttle these.
{
jclient_poketime = jclient_curtime + 10*1000;
Con_SubPrintf(ctx, "%s is an attention whore.\n", f);
pCon_SetActive(ctx);
if (BUILTINISVALID(LocalSound))
pLocalSound("misc/talk.wav");
}
}
ot = XML_ChildOfTree(tree, "subject", 0);
if (ot && !strcmp(type, "groupchat"))
{
unparsable = false;
Con_SubPrintf(ctx, "^2%s^7 has set the topic to: %s\n", f, ot->body);
}
ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/muc#user", "x", 0);
if (ot && f && !strchr(f, '/'))
{
//this is an appaling extension protocol. we really have no way to know if someone's just making this shit up just to see our presence.
//this message came from the groupchat server.
xmltree_t *inv = XML_ChildOfTree(ot, "invite", 0);
if (inv)
{
char *who = XML_GetParameter(inv, "from", "");
char *reason = XML_GetChildBody(inv, "reason", NULL);
char *password = XML_GetChildBody(ot, "password", 0);
char link[512];
buddy_t *b;
JCL_FindBuddy(jcl, f, &b, NULL);
if (b->chatroom)
return; //we already know about it. don't spam.
JCL_GenLink(jcl, link, sizeof(link), "mucjoin", f, NULL, password, "%s", f);
if (reason)
Con_SubPrintf(ctx, "* ^2%s^7 has invited you to join %s: %s.\n", who, link, reason);
else
Con_SubPrintf(ctx, "* ^2%s^7 has invited you to join %s.\n", who, link);
return; //ignore any body/jabber:x:conference
}
}
ot = XML_ChildOfTreeNS(tree, "jabber:x:conference", "x", 0);
if (ot)
{
char link[512];
char *chatjit = XML_GetParameter(ot, "jid", "");
char *reason = XML_GetParameter(ot, "reason", NULL);
char *password = XML_GetParameter(ot, "password", NULL);
unparsable = false;
JCL_GenLink(jcl, link, sizeof(link), "mucjoin", chatjit, NULL, password, "%s", chatjit);
if (reason)
Con_SubPrintf(ctx, "* ^2%s^7 has invited you to join %s: %s.\n", f, link, reason);
else
Con_SubPrintf(ctx, "* ^2%s^7 has invited you to join %s.\n", f, link);
return; //ignore any body
}
ot = XML_ChildOfTree(tree, "body", 0);
if (ot)
{
unparsable = false;
if (f)
{
if (!strncmp(ot->body, "/me ", 4))
Con_SubPrintf(ctx, "* ^2%s^7%s\n", f, ot->body+3);
else
Con_SubPrintf(ctx, "^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);
xmltree_t *muc = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/muc#user", "x", 0);
char *type = XML_GetParameter(tree, "type", "");
char *serverip = NULL;
char *servermap = NULL;
char startconvo[512];
char oldbstatus[128];
char oldfstatus[128];
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"))
{
char pauth[512], pdeny[512];
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", from);
JCL_GenLink(jcl, pauth, sizeof(pauth), "pauth", from, NULL, NULL, "%s", "Authorize");
JCL_GenLink(jcl, pdeny, sizeof(pdeny), "pdeny", from, NULL, NULL, "%s", "Deny");
Con_Printf("%s wants to be your friend! %s %s\n", startconvo, pauth, pdeny);
}
else if (type && !strcmp(type, "subscribed"))
{
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", from);
Con_Printf("%s is now your friend!\n", startconvo);
}
else if (type && !strcmp(type, "unsubscribe"))
{
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", from);
Con_Printf("%s has unfriended you\n", startconvo);
}
else if (type && !strcmp(type, "unsubscribed"))
{
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", from);
Con_Printf("%s is no longer unfriended you\n", startconvo);
}
else
{
JCL_FindBuddy(jcl, from, &buddy, &bres);
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", buddy->name);
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(oldbstatus, bres->bstatus, sizeof(oldbstatus));
Q_strlcpy(oldfstatus, bres->fstatus, sizeof(oldfstatus));
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 (muc)
{
JCL_GenLink(jcl, startconvo, sizeof(startconvo), NULL, from, NULL, NULL, "%s", bres->resource);
if (type && !strcmp(type, "unavailable"))
Con_SubPrintf(buddy->name, "%s has left the conversation\n", bres->resource);
else if (strcmp(oldbstatus, bres->bstatus))
Con_SubPrintf(buddy->name, "%s is now %s\n", startconvo, bres->bstatus);
}
else
{
if (bres->servertype == 2)
{
char joinlink[512];
JCL_GenLink(jcl, joinlink, sizeof(joinlink), "join", from, NULL, NULL, "Playing Quake - %s", bres->server);
Con_Printf("%s is now %s\n", startconvo, joinlink);
}
else if (bres->servertype == 1)
Con_Printf("%s is now ^[[Playing Quake - %s]\\observe\\%s^]\n", startconvo, bres->server, bres->server);
else if (strcmp(oldbstatus, bres->bstatus) || strcmp(oldfstatus, bres->fstatus))
{
if (*bres->fstatus)
Con_Printf("%s is now %s: %s\n", startconvo, bres->bstatus, bres->fstatus);
else
Con_Printf("%s is now %s\n", startconvo, bres->bstatus);
}
}
if (type && !strcmp(type, "unavailable"))
{
//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 && tree)
{
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 ((!jcl->issecure) && BUILTINISVALID(Net_SetTLSClient) && XML_ChildOfTree(tree, "starttls", 0) != NULL && jcl->forcetls >= 0)
{
Con_Printf("XMPP: 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)))
{
xmltree_t *m;
int sm;
char out[512];
char *method = NULL;
int outlen = -1;
if (jcl->forcetls > 0 && !jcl->issecure)
{
Con_Printf("XMPP: Unable to switch to TLS.\n");
XML_ConPrintTree(tree, 0);
XML_Destroy(tree);
return JCL_KILL;
}
for (sm = 0; sm < sizeof(saslmethods)/sizeof(saslmethods[0]); sm++)
{
method = saslmethods[sm].method?saslmethods[sm].method:jcl->oauth2.saslmethod;
if (!*method)
continue;
for (m = ot->child; m; m = m->sibling)
{
if (!strcmp(m->body, method))
{
outlen = saslmethods[sm].sasl_initial(jcl, out, sizeof(out));
if (outlen != -1)
break;
}
}
if (outlen != -1)
break;
}
if (outlen == -2)
{
XML_Destroy(tree);
return JCL_KILL;
}
if (outlen >= 0)
{
jcl->authmode = sm;
Base64_Add(out, outlen);
Base64_Finish();
Con_Printf("XMPP: Authing with %s%s.\n", method, jcl->issecure?" over tls":" without encription");
JCL_AddClientMessagef(jcl, "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='%s'"
" auth:service='oauth2'"
" xmlns:auth='http://www.google.com/talk/protocol/auth'"
">%s</auth>", method, 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") && jcl->authmode >= 0)
{
char in[512];
int inlen;
char out[512];
int outlen;
inlen = Base64_Decode(in, sizeof(in), tree->body, strlen(tree->body));
outlen = saslmethods[jcl->authmode].sasl_challenge(jcl, in, inlen, out, sizeof(out));
if (outlen < 0)
{
Con_Printf("XMPP: unable to auth with server\n");
XML_Destroy(tree);
return JCL_KILL;
}
Base64_Add(out, outlen);
Base64_Finish();
JCL_AddClientMessagef(jcl, "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%s</response>", base64);
unparsable = false;
}
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;
}
//when using srv records, the certificate must match the user's domain, rather than merely the hostname of the server.
//if you want to match the hostname of the server, use (oldstyle) tlsconnect directly instead.
if (pNet_SetTLSClient(jcl->socket, jcl->certificatedomain)<0)
{
Con_Printf("XMPP: failed to switch to TLS\n");
XML_Destroy(tree);
return JCL_KILL;
}
if (!*jcl->certificatedomain)
Con_Printf("XMPP: WARNING: Connecting via TLS without validating certificate\n");
jcl->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, "see-other-host", 0);
if (ot)
{
//msn needs this, apparently
Q_strlcpy(jcl->redirserveraddr, ot->body, sizeof(jcl->redirserveraddr));
JCL_CloseConnection(jcl, true);
if (!JCL_Reconnect(jcl))
return JCL_KILL;
return JCL_CONTINUE;
}
else
{
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);
}
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");
XML_ConPrintTree(tree, 0);
XML_Destroy(tree);
return JCL_KILL;
}
XML_Destroy(tree);
return JCL_CONTINUE;
}
void JCL_CloseConnection(jclient_t *jcl, qboolean reconnect)
{
//send our signoff to the server, if we're still alive.
if (jcl->status != JCL_DEAD && jcl->status != JCL_INACTIVE)
Con_Printf("XMPP: Disconnected from %s@%s\n", jcl->username, jcl->domain);
if (jcl->status == JCL_ACTIVE)
JCL_AddClientMessageString(jcl, "<presence type='unavailable'/>");
if (jcl->status != JCL_DEAD && jcl->status != JCL_INACTIVE)
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;
if (jcl->socket != -1)
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)
{
int i;
free(jcl);
for (i = 0; i < sizeof(jclients)/sizeof(jclients[0]); i++)
{
if (jclients[i] == jcl)
jclients[i] = 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 i;
jclient_curtime = args[0];
for (i = 0; i < sizeof(jclients)/sizeof(jclients[0]); i++)
{
jclient_t *jcl = jclients[i];
if (jcl && jcl->status != JCL_INACTIVE)
{
int stat = JCL_CONTINUE;
#ifdef JINGLE
JCL_JingleTimeouts(jcl, false);
#endif
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, true);
jcl->status = JCL_INACTIVE;
}
else if (stat == JCL_KILL)
JCL_CloseConnection(jcl, true);
else
JCL_FlushOutgoing(jcl);
}
}
}
return 0;
}
void JCL_WriteConfig(void)
{
xmltree_t *m, *n, *oauth2;
int i;
qhandle_t config;
m = XML_CreateNode(NULL, "xmppaccounts", "", "");
for (i = 0; i < sizeof(jclients) / sizeof(jclients[0]); i++)
{
jclient_t *jcl = jclients[i];
if (jcl)
{
char foo[64];
n = XML_CreateNode(m, "account", "", "");
XML_AddParameteri(n, "id", i);
XML_CreateNode(n, "streamdebug", "", jcl->streamdebug?"1":"0");
Q_snprintf(foo, sizeof(foo), "%i", jcl->forcetls);
XML_CreateNode(n, "forcetls", "", foo);
XML_CreateNode(n, "allowauth_plain_nontls", "", jcl->allowauth_plainnontls?"1":"0");
XML_CreateNode(n, "allowauth_plain_tls", "", jcl->allowauth_plaintls?"1":"0");
XML_CreateNode(n, "allowauth_digest_md5", "", jcl->allowauth_digestmd5?"1":"0");
XML_CreateNode(n, "allowauth_scram_sha_1", "", jcl->allowauth_scramsha1?"1":"0");
if (*jcl->oauth2.saslmethod)
{
XML_CreateNode(n, "allowauth_oauth2", "", jcl->allowauth_oauth2?"1":"0");
oauth2 = XML_CreateNode(n, "oauth2", "", "");
XML_AddParameter(oauth2, "method", jcl->oauth2.saslmethod);
XML_CreateNode(oauth2, "obtain-url", "", jcl->oauth2.obtainurl);
XML_CreateNode(oauth2, "refresh-url", "", jcl->oauth2.refreshurl);
XML_CreateNode(oauth2, "client-id", "", jcl->oauth2.clientid);
XML_CreateNode(oauth2, "client-secret", "", jcl->oauth2.clientsecret);
XML_CreateNode(oauth2, "scope", "", jcl->oauth2.scope);
XML_CreateNode(oauth2, "auth-token", "", jcl->oauth2.authtoken);
XML_CreateNode(oauth2, "refresh-token", "", jcl->oauth2.refreshtoken);
XML_CreateNode(oauth2, "access-token", "", jcl->oauth2.accesstoken);
}
XML_CreateNode(n, "username", "", jcl->username);
XML_CreateNode(n, "domain", "", jcl->domain);
XML_CreateNode(n, "resource", "", jcl->resource);
if (!*jcl->oauth2.saslmethod || !jcl->allowauth_oauth2 || *jcl->password) //avoid writing password lest we encourage someone to supply it when its not useful
XML_CreateNode(n, "password", "", jcl->password); //FIXME: should we base64 this just to obscure it?
XML_CreateNode(n, "serveraddr", "", jcl->serveraddr);
Q_snprintf(foo, sizeof(foo), "%i", jcl->serverport);
XML_CreateNode(n, "serverport", "", foo);
XML_CreateNode(n, "certificatedomain", "", jcl->certificatedomain);
XML_CreateNode(n, "inactive", "", jcl->status == JCL_INACTIVE?"1":"0");
}
}
pFS_Open("**plugconfig", &config, 2);
if (config >= 0)
{
char *s = XML_GenerateString(m, true);
pFS_Write(config, s, strlen(s));
free(s);
pFS_Close(config);
}
XML_Destroy(m);
}
void JCL_LoadConfig(void)
{
if (!jclients[0])
{
int len;
qhandle_t config;
char buf[8192];
qboolean oldtls;
len = pFS_Open("**plugconfig", &config, 1);
if (len >= 0)
{
if (len >= sizeof(buf))
len = sizeof(buf)-1;
buf[len] = 0;
pFS_Read(config, buf, len);
pFS_Close(config);
if (len && *buf != '<')
{//legacy code, to be removed
char *line = buf;
char tls[256];
char server[256];
char account[256];
char password[256];
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);
Con_Printf("Legacy config: %s (%i)\n", buf, len);
jclients[0] = JCL_Connect(0, server, oldtls, account, password);
}
else
{
xmltree_t *accs;
int start = 0;
accs = XML_Parse(buf, &start, len, false, "");
if (accs)
{
int i;
xmltree_t *acc;
for (i = 0; (acc = XML_ChildOfTree(accs, "account", i)); i++)
{
int id = atoi(XML_GetParameter(acc, "id", "0"));
if (id < 0 || id >= sizeof(jclients) / sizeof(jclients[0]) || jclients[id])
continue;
jclients[id] = JCL_ConnectXML(acc);
}
XML_Destroy(accs);
}
}
}
}
}
//on shutdown, write config and close connections.
qintptr_t JCL_Shutdown(qintptr_t *args)
{
jclient_t *jcl;
int i;
JCL_WriteConfig();
for (i = 0; i < sizeof(jclients)/sizeof(jclients[0]); i++)
{
jcl = jclients[i];
if (jcl)
JCL_CloseConnection(jcl, false);
}
// if (_CrtDumpMemoryLeaks())
// OutputDebugStringA("Leaks detected\n");
return true;
}
static void JCL_PrintBuddyStatus(char *console, jclient_t *jcl, buddy_t *b, bresource_t *r)
{
if (r->servertype == 2)
{
char joinlink[512];
JCL_GenLink(jcl, joinlink, sizeof(joinlink), "join", b->accountdomain, r->resource, NULL, "Playing Quake - %s", r->server);
Con_SubPrintf(console, "%s", joinlink);
}
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)
{
char invitelink[512];
JCL_GenLink(jcl, invitelink, sizeof(invitelink), "invite", b->accountdomain, r->resource, NULL, "%s", "Invite");
Con_SubPrintf(console, " %s", invitelink);
}
if (r->caps & CAP_VOICE)
{
char calllink[512];
JCL_GenLink(jcl, calllink, sizeof(calllink), "call", b->accountdomain, r->resource, NULL, "%s", "Call");
Con_SubPrintf(console, " %s", calllink);
}
}
void JCL_PrintBuddyList(char *console, jclient_t *jcl, qboolean all)
{
buddy_t *b;
bresource_t *r;
struct c2c_s *c2c;
struct ft_s *ft;
char convolink[512], actlink[512];
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 && !b->chatroom)
continue;
if (!b->resources) //offline
{
if (all)
{
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, NULL, NULL, "^s^7%s^r", b->name);
Con_SubPrintf(console, "%s: offline\n", convolink);
}
}
else if (b->resources->next)
{ //multiple potential resources
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, NULL, NULL, "%s", b->name);
Con_SubPrintf(console, "%s:\n", convolink);
for (r = b->resources; r; r = r->next)
{
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", r->resource);
Con_SubPrintf(console, " %s: ", convolink);
JCL_PrintBuddyStatus(console, jcl, b, r);
Con_SubPrintf(console, "\n");
}
}
else //only one resource
{
r = b->resources;
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", b->name);
Con_SubPrintf(console, "%s: ", convolink);
JCL_PrintBuddyStatus(console, jcl, b, r);
Con_SubPrintf(console, "\n");
}
}
#ifdef JINGLE
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:
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", b->name);
JCL_GenLink(jcl, actlink, sizeof(actlink), "jdeny", c2c->with, NULL, c2c->sid, "%s", "Hang Up");
Con_SubPrintf(console, " %s: voice %s\n", convolink, actlink);
break;
case ICEP_QWSERVER:
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", b->name);
JCL_GenLink(jcl, actlink, sizeof(actlink), "jdeny", c2c->with, NULL, c2c->sid, "%s", "Kick");
Con_SubPrintf(console, " %s: server %s\n", convolink, actlink);
break;
case ICEP_QWCLIENT:
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", b->name);
JCL_GenLink(jcl, actlink, sizeof(actlink), "jdeny", c2c->with, NULL, c2c->sid, "%s", "Disconnect");
Con_SubPrintf(console, " %s: client %s\n", convolink, actlink);
break;
}
}
#endif
#ifdef FILETRANSFERS
if (jcl->ft)
Con_SubPrintf(console, "Active file transfers:\n");
for (ft = jcl->ft; ft; ft = ft->next)
{
JCL_FindBuddy(jcl, ft->with, &b, &r);
JCL_GenLink(jcl, convolink, sizeof(convolink), NULL, b->accountdomain, r->resource, NULL, "%s", b->name);
JCL_GenLink(jcl, actlink, sizeof(actlink), "ftdeny", ft->with, NULL, ft->sid, "%s", "Hang Up");
Con_SubPrintf(console, " %s: %s\n", convolink, ft->fname);
}
#endif
}
void JCL_SendMessage(jclient_t *jcl, char *to, char *msg)
{
char markup[1024];
char *con;
buddy_t *b;
bresource_t *br;
JCL_FindBuddy(jcl, to, &b, &br);
if (b->chatroom)
{
if (br)
{
JCL_AddClientMessagef(jcl, "<message to='%s/%s' type='chat'><body>", b->accountdomain, br->resource);
con = to;
}
else
{
JCL_AddClientMessagef(jcl, "<message to='%s' type='groupchat'><body>", b->accountdomain);
con = b->name;
}
}
else
{
con = b->name;
if (!br)
br = b->defaultresource;
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 (b->chatroom && !br)
return;
if (!strncmp(msg, "/me ", 4))
Con_SubPrintf(con, "* ^5%s^7"COLOURYELLOW"%s\n", ((!strcmp(jcl->localalias, ">>"))?"me":jcl->localalias), msg+3);
else
Con_SubPrintf(con, "^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, false);
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))
{
if (b->defaultresource)
Q_snprintf(out, outsize, "%s/%s", b->accountdomain, b->defaultresource->resource);
else
Q_strlcpy(out, b->accountdomain, outsize);
return;
}
}
}
//a regular jabber account name
Q_strlcpy(out, in, outsize);
}
//server may be null, in which case its expected to be folded into room.
void JCL_JoinMUCChat(jclient_t *jcl, char *room, char *server, char *myhandle, char *password)
{
char caps[512];
char roomserverhandle[512];
buddy_t *b;
bresource_t *r;
if (!myhandle)
myhandle = jcl->username;
if (server)
Q_snprintf(roomserverhandle, sizeof(roomserverhandle), "%s@%s/%s", room, server, myhandle);
else
Q_snprintf(roomserverhandle, sizeof(roomserverhandle), "%s/%s", room, myhandle);
JCL_FindBuddy(jcl, roomserverhandle, &b, &r);
b->chatroom = true;
Q_snprintf(caps, sizeof(caps), "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://fteqw.com/ftexmppplugin' ver='%s'/>", buildcapshash());
JCL_AddClientMessagef(jcl, "<presence to='%s'><x xmlns='http://jabber.org/protocol/muc'><password>%s</password></x>%s</presence>", roomserverhandle, password, caps);
}
#ifdef FILETRANSFERS
qboolean JCL_FT_IBBChunked(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
char *from = XML_GetParameter(x, "from", "");
struct ft_s *ft = iq->usrptr, **link, *v;
for (link = &jcl->ft; (v=*link); link = &(*link)->next)
{
if (v == ft)
{
//its still valid
if (x)
{
char rawbuf[4096];
int sz;
sz = pFS_Read(ft->file, rawbuf, ft->blocksize);
Base64_Add(rawbuf, sz);
Base64_Finish();
if (sz && strlen(base64))
{
x = XML_CreateNode(NULL, "data", "http://jabber.org/protocol/ibb", base64);
XML_AddParameteri(x, "seq", ft->seq++);
XML_AddParameter(x, "sid", ft->sid);
JCL_SendIQNode(jcl, JCL_FT_IBBChunked, "set", from, x, true)->usrptr = ft;
return true;
}
//else eof
}
//errored or ended
if (x)
Con_Printf("Transfer of %s to %s completed\n", ft->fname, ft->with);
else
Con_Printf("%s aborted %s\n", ft->with, ft->fname);
x = XML_CreateNode(NULL, "close", "http://jabber.org/protocol/ibb", "");
XML_AddParameter(x, "sid", ft->sid);
JCL_SendIQNode(jcl, NULL, "set", from, x, true)->usrptr = ft;
//errored
pFS_Close(ft->file);
*link = ft->next;
free(ft);
return true;
}
}
return false;
}
qboolean JCL_FT_IBBBegun(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
struct ft_s *ft = iq->usrptr, **link, *v;
for (link = &jcl->ft; (v=*link); link = &(*link)->next)
{
if (v == ft)
{
//its still valid
if (!x)
{
Con_Printf("%s aborted %s\n", ft->with, ft->fname);
//errored
pFS_Close(ft->file);
*link = ft->next;
free(ft);
}
else
{
ft->begun = true;
ft->method = FT_IBB;
JCL_FT_IBBChunked(jcl, x, iq);
}
return true;
}
}
return false;
}
qboolean JCL_FT_OfferAcked(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
struct ft_s *ft = iq->usrptr, **link, *v;
for (link = &jcl->ft; (v=*link); link = &(*link)->next)
{
if (v == ft)
{
//its still valid
if (!x)
{
Con_Printf("%s doesn't want %s\n", ft->with, ft->fname);
//errored
pFS_Close(ft->file);
*link = ft->next;
free(ft);
}
else
{
xmltree_t *xo;
Con_Printf("%s accepted %s\n", ft->with, ft->fname);
xo = XML_CreateNode(NULL, "open", "http://jabber.org/protocol/ibb", "");
XML_AddParameter(xo, "sid", ft->sid);
XML_AddParameteri(xo, "block-size", ft->blocksize);
//XML_AddParameter(xo, "stanza", "iq");
JCL_SendIQNode(jcl, JCL_FT_IBBBegun, "set", XML_GetParameter(x, "from", ""), xo, true)->usrptr = ft;
}
return true;
}
}
return false;
}
#endif
void JCL_Command(int accid, char *console)
{
char imsg[8192];
char arg[6][1024];
char *msg;
int i;
char nname[256];
jclient_t *jcl;
if (accid < 0 || accid >= sizeof(jclients)/sizeof(jclients[0]))
return;
jcl = jclients[accid];
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"))
{
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 (jcl)
{
Con_TrySubPrint(console, "You are already connected\nPlease /quit first\n");
return;
}
jcl = jclients[accid] = JCL_Connect(accid, arg[3], !strncmp(arg[0]+1, "tls", 3), arg[1], arg[2]);
if (!jclients[accid])
{
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^] (using oauth2)\n");
Con_Printf("eg for gmail: ^[/" COMMANDPREFIX " /connect myusername@gmail.com mypassword talk.google.com^] (warning: password will be saved locally in plain text)\n");
// Con_Printf("eg for facebook: ^[/" COMMANDPREFIX " /connect myusername@chat.facebook.com mypassword chat.facebook.com^]\n");
// Con_Printf("eg for msn: ^[/" COMMANDPREFIX " /connect myusername@messanger.live.com mypassword^]\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 (!jcl)
{
Con_SubPrintf(console, "No account specified. Cannot %s\n", arg[0]);
}
else if (!strcmp(arg[0]+1, "oa2token"))
{
free(jcl->oauth2.authtoken);
jcl->oauth2.authtoken = strdup(arg[1]);
if (jcl->status == JCL_INACTIVE)
jcl->status = JCL_DEAD;
}
else if (!strcmp(arg[0]+1, "quit"))
{
//disconnect from the xmpp server.
JCL_CloseConnection(jcl, false);
}
else if (jcl->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, jcl, true);
}
else if (!strcmp(arg[0]+1, "msg"))
{
//FIXME: validate the dest. deal with xml markup in dest.
Q_strlcpy(jcl->defaultdest, arg[1], sizeof(jcl->defaultdest));
msg = arg[2];
JCL_SendMessage(jcl, jcl->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(jcl, 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(jcl, "<presence to='%s' type='subscribe'/>", arg[1]);
//let em see us
if (jcl->preapproval)
JCL_AddClientMessagef(jcl, "<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(jcl, "<presence to='%s' type='unsubscribed'/>", arg[1]);
//stop looking for em
JCL_AddClientMessagef(jcl, "<presence to='%s' type='unsubscribe'/>", arg[1]);
//stop listing em
JCL_SendIQf(jcl, NULL, "set", NULL, "<query xmlns='jabber:iq:roster'><item jid='%s' subscription='remove' /></query>", arg[1]);
}
#ifdef JINGLE
else if (!strcmp(arg[0]+1, "join"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_Join(jcl, nname, NULL, true, ICEP_QWCLIENT);
}
else if (!strcmp(arg[0]+1, "invite"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_Join(jcl, nname, NULL, true, ICEP_QWSERVER);
}
else if (!strcmp(arg[0]+1, "voice") || !strcmp(arg[0]+1, "call"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_Join(jcl, nname, NULL, true, ICEP_VOICE);
}
else if (!strcmp(arg[0]+1, "kick"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_Join(jcl, nname, NULL, false, ICEP_INVALID);
}
else if (!strcmp(arg[0]+1, "kick"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_Join(jcl, nname, NULL, false, ICEP_INVALID);
}
#endif
#ifdef FILETRANSFERS
else if (!strcmp(arg[0]+1, "sendfile"))
{
xmltree_t *xfile, *xsi, *c;
char *fname = arg[1];
//file transfer offer
struct ft_s *ft;
if (!*fname || strchr(fname, '*'))
{
Con_SubPrintf(console, "XMPP: /sendfile FILENAME [to]\n");
return;
}
else
{
JCL_ToJID(jcl, *arg[2]?arg[2]:console, nname, sizeof(nname));
Con_SubPrintf(console, "Offering %s to %s.\n", fname, nname);
ft = malloc(sizeof(*ft));
memset(ft, 0, sizeof(*ft));
ft->next = jcl->ft;
jcl->ft = ft;
ft->transmitting = true;
ft->blocksize = 4096;
Q_strlcpy(ft->fname, fname, sizeof(ft->fname));
Q_snprintf(ft->sid, sizeof(ft->sid), "%x%s", rand(), ft->fname);
Q_strlcpy(ft->md5hash, "", sizeof(ft->md5hash));
ft->size = pFS_Open(ft->fname, &ft->file, 1);
ft->with = strdup(nname);
ft->method = FT_IBB;
ft->begun = false;
//generate an offer.
xsi = XML_CreateNode(NULL, "si", "http://jabber.org/protocol/si", "");
XML_AddParameter(xsi, "profile", "http://jabber.org/protocol/si/profile/file-transfer");
XML_AddParameter(xsi, "id", ft->sid);
//XML_AddParameter(xsi, "mime-type", "text/plain");
xfile = XML_CreateNode(xsi, "file", "http://jabber.org/protocol/si/profile/file-transfer", ""); //I don't really get the point of this.
XML_AddParameter(xfile, "name", ft->fname);
XML_AddParameteri(xfile, "size", ft->size);
c = XML_CreateNode(xsi, "feature", "http://jabber.org/protocol/feature-neg", "");
c = XML_CreateNode(c, "x", "jabber:x:data", "");
XML_AddParameter(c, "type", "form");
c = XML_CreateNode(c, "field", "", "");
XML_AddParameter(c, "var", "stream-method");
XML_AddParameter(c, "type", "listsingle");
XML_CreateNode(XML_CreateNode(c, "option", "", ""), "value", "", "http://jabber.org/protocol/ibb");
// XML_CreateNode(XML_CreateNode(c, "option", "", ""), "value", "", "http://jabber.org/protocol/bytestreams");
JCL_SendIQNode(jcl, JCL_FT_OfferAcked, "set", nname, xsi, true)->usrptr = ft;
}
}
#endif
else if (!strcmp(arg[0]+1, "joinchatroom") || !strcmp(arg[0]+1, "muc") || !strcmp(arg[0]+1, "joinmuc"))
{
JCL_JoinMUCChat(jcl, arg[1], arg[2], arg[3], arg[4]);
}
else if (!strcmp(arg[0]+1, "leavechatroom") || !strcmp(arg[0]+1, "leavemuc"))
{
char roomserverhandle[512];
buddy_t *b;
bresource_t *r;
Q_snprintf(roomserverhandle, sizeof(roomserverhandle), "%s@%s/%s", arg[1], arg[2], arg[3]);
JCL_FindBuddy(jcl, roomserverhandle, &b, &r);
b->chatroom = true;
JCL_AddClientMessagef(jcl, "<presence to='%s' type='unavailable'/>", roomserverhandle);
JCL_ForgetBuddy(jcl, b, NULL);
}
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_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_AttentionMessage(jcl, nname, msgtab[rand()%(sizeof(msgtab)/sizeof(msgtab[0]))]);
}
else if (!strcmp(arg[0]+1, "poke"))
{
JCL_ToJID(jcl, *arg[1]?arg[1]:console, nname, sizeof(nname));
JCL_AttentionMessage(jcl, nname, NULL);
}
else if (!strcmp(arg[0]+1, "raw"))
{
//parse it first, so noone ever generates invalid xml and gets kicked... too obviously.
int pos = 0;
int maxpos = strlen(arg[1]);
xmltree_t *t;
while (pos != maxpos)
{
t = XML_Parse(arg[1], &pos, maxpos, false, "");
if (t)
XML_Destroy(t);
else
break;
}
if (pos == maxpos)
{
jcl->streamdebug = true;
JCL_AddClientMessageString(jcl, arg[1]);
}
else
Con_Printf("XML not well formed\n");
}
else
Con_SubPrintf(console, "Unrecognised command: %s\n", arg[0]);
}
else
{
if (jcl)
{
msg = imsg;
if (!*msg)
{
if (!*console)
{
JCL_PrintBuddyList(console, jcl, false);
//Con_TrySubPrint(console, "For help, type \"^[/" COMMANDPREFIX " /help^]\"\n");
}
}
else
{
JCL_ToJID(jcl, *console?console:jcl->defaultdest, nname, sizeof(nname));
JCL_SendMessage(jcl, nname, msg);
}
}
else
{
Con_TrySubPrint(console, "Not connected. For help, type \"^[/" COMMANDPREFIX " /help^]\"\n");
}
}
}