shithub: xmpp

ref: 7b3151f0a13823fccc39b2609155ffd1e5fd806f
dir: /xmpp.c/

View raw version
#include <u.h>
#include <libc.h>
#include <auth.h>
#include <bio.h>
#include <thread.h>
#include "xml.h"
#include "xmpp.h"

/* commands are at xmpp.c:/^handle */

char *enttypes[] =
{
	[Emuc]    "groupchat",
	[Emucent] "chat",
	[Erost]   "chat",
};

static int pfd;
static Biobuf pb;
static char lastid[32];

int debug, nopresence, nohistory, plainallow;
Biobuf kbin;
char *server, *mydomain, *myjid, *mynick, *myresource, *myplainjid;
static QLock prlock;

static int
ismyjid(char *jid)
{
	int n;

	n = strlen(myplainjid);
	if(strncmp(jid, myplainjid, n) == 0 && (jid[n] == 0 || jid[n] == '/'))
		return 1;
	n = strlen(myjid);
	if(strncmp(jid, myjid, n) == 0 && (jid[n] == 0 || jid[n] == '/'))
		return 1;

	return 0;
}

static void
inerror(Xelem *x)
{
	Xattr *from, *type;
	Xelem *err;

	from = xmlgetattr(x->a, "from");
	err = xmlget(x->ch, "error");
	type = err == nil ? nil : xmlgetattr(err->a, "type");
	print("[%s] (%s, error) %s %s",
		strtime(), (from == nil) ? mydomain : from->v, x->n, type == nil ? "" : type->v);
	err = err == nil ? nil : err->ch;
	if(err != nil)
		print(": %s", (err->v == nil) ? err->n : err->v);
	print("\n");
}

static void
inmsg(Xelem *x)
{
	Xattr *type, *from, *stamp, *to, *ns;
	Xelem *body, *delay, *subj, *e;
	char *s, *nick, *bodyv, tmp[64];
	Target *t, *room;
	int i;

	if((from = xmlgetattr(x->a, "from")) == nil)
		return;

	/* message carbons https://xmpp.org/extensions/xep-0280.html */
	if((e = xmlget(x->ch, "received")) != nil &&
	   ismyjid(from->v) &&
	   (ns = xmlgetattr(e->a, "xmlns")) != nil &&
	   strcmp(ns->v, "urn:xmpp:carbons:2") == 0 &&
	   (e = xmlget(e->ch, "forwarded")) != nil &&
	   (e = xmlget(e->ch, "message")) != nil){
		x = e;
		if((from = xmlgetattr(x->a, "from")) == nil)
			return;
	}

	type = xmlgetattr(x->a, "type");
	body = xmlget(x->ch, "body");
	subj = xmlget(x->ch, "subject");
	/* ignore "composing..." messages */
	if(body == nil && subj == nil)
		return;

	to = xmlgetattr(x->a, "to");
	if(to != nil && !ismyjid(to->v))
		return;

	if((delay = xmlget(x->ch, "delay")) == nil)
		delay = xmlget(x->ch, "x");
	if((stamp = delay ? xmlgetattr(delay->a, "stamp") : nil) == nil)
		stamp = xmlgetattr(x->a, "ts");
	bodyv = (body == nil) ? nil : ((body->v == nil) ? "" : body->v);

	/*
	 * there is no difference between mucpriv and raw jid
	 * try to find the target
	 */
	t = room = nil;
	for(i = 0; i < numtargets; i++, t = nil){
		t = targets[i];
		if(t->type == Emuc && strncmp(t->jid, from->v, strlen(t->jid)) == 0)
			room = t;
		else if((t->type == Emucent && strcmp(t->jid, from->v) == 0) ||
				(t->type == Erost && strncmp(t->jid, from->v, strlen(t->jid)) == 0)){
			break;
		}
	}

	if(subj != nil && room != nil){
		free(room->muc.subj);
		room->muc.subj = strdup((subj->v == nil) ? "" : subj->v);
		return;
	}
	if(bodyv == nil)
		return;

	print("[%s] ", (stamp != nil) ? strstamp(stamp->v) : strtime());
	if(t == nil && room == nil)
		nick = from->v;
	else if(t != nil && t->type == Erost){
		snprint(tmp, sizeof(tmp), "%t", t);
		nick = tmp;
	}else{
		/* extract nick and muc */
		if(nick = strchr(from->v, '/'))
			nick++;
		if(s = strchr(from->v, '@'))
			*s = 0;
		print("(%s", from->v);
		if(type != nil && strcmp(type->v, enttypes[Emucent]) == 0)
			print(", private) ");
		else
			print(") ");
	}

	if(nick == nil)
		print("%s\n", bodyv);
	else if(strncmp(bodyv, "/me ", 4) == 0)
		print("→ %s %s\n", nick, bodyv+4);
	else
		print("%s → %s\n", nick, bodyv);
}

static int
iniq(Xelem *x, int fd)
{
	Xelem *e;
	Xattr *a, *from, *id, *var;
	int isget, isset;

	a = xmlgetattr(x->a, "type");
	if(a == nil || x->ch == nil)
		return 0;
	id = xmlgetattr(x->a, "id");

	if(strcmp(a->v, "result") == 0){
		if(x->ch != nil && (e = xmlget(x->ch->ch, "storage")) != nil){
			/* autojoin bookmarked MUCs http://xmpp.org/extensions/xep-0048.html */
			a = xmlgetattr(e->a, "xmlns");
			if(strcmp(a->v, "storage:bookmarks") != 0)
				return 0;
			return mucbookmarks(e, fd);
		}
		if(x->ch != nil && x->ch->ch != nil && strcmp(x->ch->n, "bind") == 0){
			if(strcmp(x->ch->ch->n, "jid") == 0){
				free(myjid);
				myjid = strdup(x->ch->ch->v);
			}
		}
		if(id != nil && strcmp(id->v, "afflist") == 0){
			int width, len, num;
			Xattr *aff, *jid;
			for(e = x->ch->ch, width = 0; e != nil; e = e->next){
				if((jid = xmlgetattr(e->a, "jid")) != nil && (len = strlen(jid->v)) > width)
					width = len;
			}
			for(e = x->ch->ch, num = 0; e != nil; e = e->next, num++){
				if((jid = xmlgetattr(e->a, "jid")) != nil && (aff = xmlgetattr(e->a, "affiliation")) != nil)
					print("  %*s  %-8s\n", -width, jid->v, aff->v);
			}
			print("%d jid(s)\n", num);
			return 0;
		}else if(id != nil && strcmp(id->v, "gimme0") == 0){
			/* bookmarks http://xmpp.org/extensions/xep-0048.html */
			if(fprint(pfd,
				"<iq type='get' from='%Ӽ' id='gimme1'>"
				"<query xmlns='jabber:iq:private'>"
				"<storage xmlns='storage:bookmarks'/>"
				"</query></iq>",
				myjid) < 0)
				return -1;

			/* ask for roster */
			if(fprint(pfd,
				"<iq type='get' from='%Ӽ' id='gimme2'>"
				"<query xmlns='jabber:iq:roster'/></iq>",
				myjid) < 0)
				return -1;

			/* enable message carbons https://xmpp.org/extensions/xep-0280.html */
			if(x->ch != nil)
			for(e = x->ch->ch; e != nil; e = e->next){
				if(strcmp(e->n, "feature") != 0)
					continue;
				if((var = xmlgetattr(e->a, "var")) == nil)
					continue;
				if(strcmp(var->v, "urn:xmpp:carbons:2") != 0)
					continue;
				
				if(fprint(pfd,
					"<iq type='set' from='%Ӽ' id='lotsofcarbs' xmlns='jabber:client'>"
					"<enable xmlns='urn:xmpp:carbons:2'/>"
					"</iq>",
					myjid) < 0)
					return -1;
				break;
			}
		}
	}

	/* incoming queries */
	isget = strcmp(a->v, "get") == 0;
	isset = strcmp(a->v, "set") == 0;
	if(!isget && !isset && strcmp(a->v, "result") != 0)
		return 0;
	from = xmlgetattr(x->a, "from");
	if(isget && (from == nil || id == nil))
		return 0;

	if(e = xmlget(x->ch, "query")){
		a = xmlgetattr(e->a, "xmlns");
		if(a != nil && isget &&strcmp(a->v, "jabber:iq:version") == 0){
			/* software version http://xmpp.org/extensions/xep-0092.html */
			return fprint(fd,
				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
				"<query xmlns='jabber:iq:version'>"
				"<name>xmpp</name>"
				"<version>9front edition</version>"
				"<os>Plan 9</os>"
				"</query></iq>",
				from->v, id->v);
		}
		if(a != nil && isget && strcmp(a->v, "http://jabber.org/protocol/disco#info") == 0){
			/* service discovery http://xmpp.org/extensions/xep-0030.html */
			return fprint(fd,
				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
				"<query xmlns='http://jabber.org/protocol/disco#info'>"
				"<feature var='http://jabber.org/protocol/disco#info'/>"
				"<feature var='jabber:iq:version'/>"
				"<feature var='urn:xmpp:time'/>"
				"<feature var='urn:xmpp:ping'/>"
				"<feature var='http://jabber.org/protocol/muc'/>"
				"</query></iq>",
				from->v, id->v);
		}
		if(a != nil && !isget && strcmp(a->v, "jabber:iq:roster") == 0)
			return rostupdate(x, fd);
	}else if(isget && (e = xmlget(x->ch, "time")) != nil){
		a = xmlgetattr(e->a, "xmlns");
		if(a != nil && strcmp(a->v, "urn:xmpp:time") == 0){
			/* entity time http://xmpp.org/extensions/xep-0202.html */
			char *utc, *tzo;

			utc = strenttime(&tzo);
			return fprint(fd,
				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
				"<time xmlns='urn:xmpp:time'>"
				"<utc>%Ӽ</utc><tzo>%Ӽ</tzo>"
				"</time></iq>",
				from->v, id->v, utc, tzo);
		}
	}else if(isget && (e = xmlget(x->ch, "ping")) != nil){
		a = xmlgetattr(e->a, "xmlns");
		if(a != nil && strcmp(a->v, "urn:xmpp:ping") == 0){
			/* ping http://xmpp.org/extensions/xep-0199.html */
			return fprint(fd,
				"<iq type='result' to='%Ӽ' id='%Ӽ'/>",
				from->v, id->v);
		}
	}

	if(isget)
		return fprint(fd,
			"<iq type='error' to='%Ӽ' id='%Ӽ'>"
			"<error type='cancel'>"
			"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
			"</error></iq>",
			from->v, id->v);
	return 0;
}

static void
inpresence(Xelem *x, int fd)
{
	int i, found;
	Target *t;
	Xattr *from, *type;

	from = xmlgetattr(x->a, "from");
	type = xmlgetattr(x->a, "type");
	if(type != nil && rostsubscr(from->v, type->v, fd))
		return;
	found = 0;
	for(i = 0; i < numtargets; i++){
		t = targets[i];
		if(t->type == Erost && strncmp(t->jid, from->v, strlen(t->jid)) == 0){
			rostpresence(x, t);
			found = 1;
			break;
		}
		if(t->type == Emucent && strcmp(t->jid, from->v) == 0){
			mucpresence(x, t->mucent.room, from);
			found = 1;
		}
		if(t->type == Emuc && strncmp(t->jid, from->v, strlen(t->jid)) == 0){
			mucpresence(x, t, from);
			found = 1;
		}
	}

	if(!found && debug > 0)
		fprint(2, "presence from unknown target: %q\n", from->v);
}

static void
reader(void *pd)
{
	Xelem *x;
	Xattr *type, *id;
	int err;

	USED(pd);
	threadsetname("reader");

	fprint(pfd,
		"<iq type='get' from='%Ӽ' to='%Ӽ' id='gimme0'>"
		"<query xmlns='http://jabber.org/protocol/disco#info'/>"
		"</iq>",
		myjid, server);

	for(err = 0; err == 0;){
		if((x = xmlread(&pb, 0, &err)) == nil)
			continue;

		qlock(&prlock);
		if((id = xmlgetattr(x->a, "id")) != nil && strcmp(id->v, lastid) == 0){
			xmlprint(x, 2);
			lastid[0] = 0;
		}else if(debug > 1){
			if(strcmp(x->n, "presence") != 0 || debug > 2)
				xmlprint(x, 2);
		}

		type = xmlgetattr(x->a, "type");
		if(type != nil && strcmp(type->v, "error") == 0)
			inerror(x);
		else if(strcmp(x->n, "message") == 0)
			inmsg(x);
		else if(strcmp(x->n, "presence") == 0)
			inpresence(x, pfd);
		else if(strcmp(x->n, "iq") == 0)
			iniq(x, pfd);
		xmlfree(x);
		qunlock(&prlock);
	}
	fprint(2, "%r\n");
	threadexitsall(nil);
}

static int
cmdmsg(int fd, int, char **)
{
	char *s;
	int res;

	if(curr < 0)
		return 0;

	s = readlines();
	res = fprint(fd,
		"<message to='%Ӽ' type='%Ӽ'><body>%Ӽ</body></message>",
		targets[curr]->jid,
		enttypes[targets[curr]->type],
		s);
	free(s);
	return res;
}

static int
handle(int fd, char *s)
{
	typedef int (*cmdf)(int, int, char **);
	char *ps, *pe, *argv[3];
	static cmdf cmds[256] = {
		['a'] cmdaff,      /*  muc.c:/^cmdaff      */
		['b'] cmdbookmark, /*  muc.c:/^cmdbookmark */
		['j'] cmdjoin,     /*  muc.c:/^cmdjoin     */
		['m'] cmdmsg,      /* xmpp.c:/^cmdmsg      */
		['n'] cmdnick,     /*  muc.c:/^cmdnick     */
		['p'] cmdpart,     /*  muc.c:/^cmdpart     */
		['r'] cmdroster,   /* rost.c:/^cmdroster   */
		['R'] cmdroster,   /* rost.c:/^cmdroster   */
		['s'] cmdsubj,     /*  muc.c:/^cmdsubj     */
		['S'] cmdsubj,     /*  muc.c:/^cmdsubj     */
		['t'] cmdtarget,   /* targ.c:/^cmdtarget   */
		['w'] cmdwho,      /*  muc.c:/^cmdwho      */
		['W'] cmdwho,      /*  muc.c:/^cmdwho      */
	};
	int argc;

	cleaninput(utflen(s)+1);
	if(*s == '/' && *(++s) != '/'){
		if(*s == 'q'){
			for(s++; (*s == ' ' || *s == '\t'); s++);
			lastid[0] = 0;
			if((ps = utfutf(s, "id='")) != nil && (pe = utfrune(ps+4, '\'')) != nil){
				ps += 4;
				if(sizeof(lastid) > pe-ps)
					strncpy(lastid, ps, pe-ps);
			}
			return fprint(fd, "%s", s);
		}else if(*s == 'm' && s[1] == 'e'){
			s--;
		}else if(cmds[*s] != nil){
			argc = tokenize(s, argv, nelem(argv));
			return cmds[*s](fd, argc, argv);
		}else{
			s--;
			print("unknown cmd %q\n", s);
			return 0;
		}
	}

	if(curr < 0)
		return 0;

	return fprint(fd,
		"<message to='%Ӽ' type='%Ӽ'><body>%Ӽ</body></message>",
		targets[curr]->jid, enttypes[targets[curr]->type], s);
}

static void
writer(void *pd)
{
	char *s;
	int err;

	USED(pd);
	threadsetname("writer");

	Binit(&kbin, 0, OREAD);
	for(err = 0; (s = Brdstr(&kbin, '\n', 1)) != nil && err >= 0;){
		qlock(&prlock);
		if(s[0] != 0)
			err = handle(pfd, s);
		free(s);
		qunlock(&prlock);
	}
}

static void
usage(void)
{
	fprint(2, "usage: xmpp [-n nick] [-r resource] [-p] [-y] jid\n");
	threadexits("usage");
}

static int
die(void *, char *)
{
	setlabel(nil, nil);
	return 0;
}

static void
pblethal(char *m)
{
	threadexitsall(m);
}

void
threadmain(int argc, char **argv)
{
	UserPasswd *up;
	char *user;

	debug = 0;
	plainallow = 0;
	nopresence = 1;
	nohistory = 1;
	myjid = nil;
	mynick = getuser();
	myresource = nil;
	curr = -1;

	ARGBEGIN{
	case 'd':
		debug++;
		break;
	case 'n':
		mynick = EARGF(usage());
		break;
	case 'p':
		nopresence = 0;
		break;
	case 'r':
		myresource = EARGF(usage());
		break;
	case 'y':
		plainallow = 1;
		break;
	case 'h':
		nohistory = 0;
		break;
	}ARGEND

	if(argc != 1)
		usage();
	myjid = strdup(argv[0]);
	/* myjid will get set later to a value given by the server,
	 * but sometimes that value isn't used, so we also
	 * check against the old value */
	myplainjid = strdup(myjid);

	quotefmtinstall();
	fmtinstall('H', encodefmt);
	fmtinstall('[', encodefmt);
	fmtinstall('t', targetfmt);
	fmtinstall(L'Ӽ', xmlstrfmt);
	user = strdup(myjid);
	server = strrchr(user, '@');
	if(server == nil)
		sysfatal("invalid jid: %q", user);
	*server++ = 0;
	server = strdup(server);
	mydomain = strrchr(user, '@');
	if(mydomain == nil)
		mydomain = server;
	else
		mydomain = strdup(mydomain);
	srand(time(nil));

	up = auth_getuserpasswd(auth_getkey, "proto=pass service=xmpp server=%q user=%q", server, user);
	if(up == nil)
		sysfatal("no password: %r");

	if((pfd = connect(&pb, up->user, up->passwd)) < 0)
		sysfatal("connect: %r");
	memset(up->passwd, 0, strlen(up->passwd));
	free(up);
	free(user);

	setlabel("", nil);

	Blethal(&pb, pblethal);
	threadnotify(die, 1);
	proccreate((void*)reader, nil, 8*1024);
	writer(nil);
	fprint(2, "%r\n");
	threadexitsall(nil);
}