code: purgatorio

ref: 0ec1cfd78b495e9be40e4aa24e35b2aa5d5e5704
dir: /appl/charon/cookiesrv.b/

View raw version
implement Cookiesrv;
include "sys.m";
include "bufio.m";
include "string.m";
include "daytime.m";
include "cookiesrv.m";

sys: Sys;
bufio: Bufio;
S: String;
daytime: Daytime;

Iobuf: import bufio;

Cookielist: adt {
	prev: cyclic ref Cookielist;
	next: cyclic ref Cookie;
};

Cookie: adt {
	name: string;
	value: string;
	dom: string;
	path: string;
	expire: int;		# seconds from epoch, -1 => not set, 0 => expire now
	secure: int;
	touched: int;
	link: cyclic ref Cookielist;	# linkage for list of cookies in the same domain
};

Domain: adt {
	name: string;
	doms: cyclic list of ref Domain;
	cookies: ref Cookielist;
};

MAXCOOKIES: con 300;		# total number of cookies allowed
LISTMAX: con 20;			# max number of cookies per Domain
PURGENUM: con 30;			# number of cookies to delete when freeing up space
MAXCKLEN: con 4*1024;		# max cookie length

ncookies := 0;
doms: list of ref Domain;
now: int;	# seconds since epoch
cookiepath: string;
touch := 0;

start(path: string, saveinterval: int): ref Client
{
	sys = load Sys Sys->PATH;
	bufio = load Bufio Bufio->PATH;
	if (bufio == nil) {
		sys->print("cookiesrv: cannot load %s: %r\n", Bufio->PATH);
		return nil;
	}
	S = load String String->PATH;
	if (S == nil) {
		sys->print("cookiesrv: cannot load %s: %r\n", String->PATH);
		return nil;
	}
	daytime = load Daytime Daytime->PATH;
	if (daytime == nil) {
		sys->print("cookiesrv: cannot load %s: %r\n", Daytime->PATH);
		return nil;
	}

	cookiepath = path;
	now = daytime->now();

	# load the cookie file
	# order is most recently touched first 
	iob := bufio->open(cookiepath, Sys->OREAD);
	if (iob != nil) {
		line: string;
		while ((line = iob.gets('\n')) != nil) {
			if (line[len line -1] == '\n')
				line = line[:len line -1];
			loadcookie(line);
		}
		iob.close();
		iob = nil;
		expire();
	}
	fdc := chan of ref Sys->FD;
	spawn server(fdc, saveinterval);
	fd := <- fdc;
	if (fd == nil)
		return nil;
	return ref Client(fd);
}

addcookie(ck: ref Cookie, domlist: ref Cookielist)
{
	(last, n) := lastlink(domlist);
	if (n == LISTMAX)
		rmcookie(last.prev.next);
	if (ncookies == MAXCOOKIES)
		rmlru();
	ck.link = ref Cookielist(domlist, domlist.next);
	if (domlist.next != nil)
		domlist.next.link.prev = ck.link;
	domlist.next = ck;
	ncookies++;
}

rmcookie(ck: ref Cookie)
{
	nextck := ck.link.next;
	ck.link.prev.next = nextck;
	if (nextck != nil) 
		nextck.link.prev = ck.link.prev;
	ncookies--;
}

lastlink(ckl: ref Cookielist): (ref Cookielist, int)
{
	n := 0;
	for (nckl := ckl.prev; nckl != nil; nckl = nckl.prev)
		n++;
	for (; ckl.next != nil; ckl = ckl.next.link)
		n++;
	return (ckl, n);
}

rmlru()
{
	cka := array [ncookies] of ref Cookie;
	ix := getallcookies(doms, cka, 0);
	if (ix < PURGENUM)
		return;
	mergesort(cka, nil, SORT_TOUCHED);
	for (n := 0; n < PURGENUM; n++)
		rmcookie(cka[n]);
}

getallcookies(dl: list of ref Domain, cka: array of ref Cookie, ix: int): int
{
	for (; dl != nil; dl = tl dl) {
		dom := hd dl;
		for (ck := dom.cookies.next; ck != nil; ck = ck.link.next)
			cka[ix++] = ck;
		ix = getallcookies(dom.doms, cka, ix);
	}
	return ix;
}

isipaddr(s: string): int
{
	# assume ipaddr if only numbers and '.'s
	# should maybe count the dots too (what about IPV6?)
	return S->drop(s, ".0123456789") == nil;
}

setcookie(ck: ref Cookie)
{
	parent, dom: ref Domain;
	domain := ck.dom;
	if (isipaddr(domain))
		(parent, dom, domain) = getdom(doms, nil, domain);
	else
		(parent, dom, domain) = getdom(doms, domain, nil);

	if (dom == nil)
		dom = newdom(parent, domain);

	for (oldck := dom.cookies.next; oldck != nil; oldck = oldck.link.next) {
		if (ck.name == oldck.name && ck.path == oldck.path) {
			rmcookie(oldck);
			break;
		}
	}
	if (ck.expire > 0 && ck.expire <= now)
		return;
	addcookie(ck, dom.cookies);
}

expire()
{
	cka := array [ncookies] of ref Cookie;
	ix := getallcookies(doms, cka, 0);
	for (i := 0; i < ix; i++) {
		ck := cka[i];
		if (ck.expire > 0 && ck.expire < now)
			rmcookie(ck);
	}
}

newdom(parent: ref Domain, domain: string): ref Domain
{
	while (domain != "") {
		(lhs, rhs) := splitdom(domain);
		d := ref Domain(rhs, nil, ref Cookielist(nil, nil));
		if (parent == nil)
			doms = d :: doms;
		else
			parent.doms = d :: parent.doms;
		parent = d;
		domain = lhs;
	}
	return parent;
}

getdom(dl: list of ref Domain, lhs, rhs: string): (ref Domain, ref Domain, string)
{
	if (rhs == "")
		(lhs, rhs) = splitdom(lhs);
	parent: ref Domain;
	while (dl != nil) {
		d := hd dl;
		if (d.name != rhs) {
			dl = tl dl;
			continue;
		}
		# name matches
		if (lhs == nil)
			return (parent, d, rhs);
		parent = d;
		(lhs, rhs) = splitdom(lhs);
		dl = d.doms;
	}
	return (parent, nil, lhs+rhs);
}

# returned list is in shortest to longest domain match order
getdoms(dl: list of ref Domain, lhs, rhs: string): list of ref Domain
{
	if (rhs == "")
		(lhs, rhs) = splitdom(lhs);
	for (; dl != nil; dl = tl dl) {
		d := hd dl;
		if (d.name == rhs) {
			if (lhs == nil)
				return d :: nil;
			(lhs, rhs) = splitdom(lhs);
			return d :: getdoms(d.doms, lhs, rhs);
		}
	}
	return nil;
}

server(fdc: chan of ref Sys->FD, saveinterval: int)
{
	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
	sys->bind("#s", "/chan", Sys->MBEFORE);
	fio := sys->file2chan("/chan", "ctl");
	if (fio == nil) {
		fdc <-= nil;
		return;
	}
	fd := sys->open("/chan/ctl", Sys->OWRITE);
	fdc <-= fd;
	if (fd == nil)
		return;
	fd = nil;
		
	tick := chan of int;
	spawn ticker(tick, 1*60*1000);	# clock tick once a minute
	tickerpid := <- tick;

	modified := 0;
	savetime := now + saveinterval;

	for (;;) alt {
	now = <- tick =>
		expire();
		if (saveinterval != 0 && now > savetime) {
			if (modified) {
				save();
				modified = 0;
			}
			savetime = now + saveinterval;
		}
	(nil, line, nil, rc) := <- fio.write =>
		now = daytime->now();
		if (rc == nil) {
			kill(tickerpid);
			expire();
			save();
			return;
		}
		loadcookie(string line);
		alt {
		rc <-= (len line, nil) =>
			;
		* =>
			;
		};
		modified = 1;
	}
}

ticker(tick: chan of int, ms: int)
{
	tick <-= sys->pctl(0, nil);
	for (;;) {
		sys->sleep(ms);
		tick <-= daytime->now();
	}
}

# sort orders
SORT_TOUCHED, SORT_PATHLEN: con iota;

mergesort(a, b: array of ref Cookie, order: int)
{
	if (b == nil)
		b = array [len a] of ref Cookie;
	r := len a;
	if (r > 1) {
		m := (r-1)/2 + 1;
		mergesort(a[0:m], b[0:m], order);
		mergesort(a[m:], b[m:], order);
		b[0:] = a;
		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
			if (greater(b[i], b[j], order))
				a[k] = b[j++];
			else
				a[k] = b[i++];
		}
		if (i < m)
			a[k:] = b[i:m];
		else if (j < r)
			a[k:] = b[j:r];
	}
}

greater(x, y: ref Cookie, order: int): int
{
	if (y == nil)
		return 0;
	case order {
	SORT_TOUCHED =>
		if (x.touched > y.touched)
			return 1;
	SORT_PATHLEN =>
		if (len x.path < len y.path)
			return 1;
	}
	return 0;
}

cookie2str(ck: ref Cookie): string
{
	if (len ck.name +1 > MAXCKLEN)
		return "";
	namval := sys->sprint("%s=%s", ck.name, ck.value);
	if (len namval > MAXCKLEN)
		namval = namval[:MAXCKLEN];
	return sys->sprint("%s\t%s\t%d\t%d\t%s", ck.dom, ck.path, ck.expire, ck.secure, namval);
}

loadcookie(ckstr: string)
{
	(n, toks) := sys->tokenize(ckstr, "\t");
	if (n < 5)
		return;
	dom, path, exp, sec, namval: string;
	(dom, toks) = (hd toks, tl toks);
	(path, toks) = (hd toks, tl toks);
	(exp, toks) = (hd toks, tl toks);
	(sec, toks) = (hd toks, tl toks);
	(namval, toks) = (hd toks, tl toks);

	# some sanity checks
	if (dom == "" || path == "" || path[0] != '/')
		return;

	(name, value) := S->splitl(namval, "=");
	if (value == nil)
		return;
	value = value[1:];
	ck := ref Cookie(name, value, dom, path, int exp, int sec, touch++, nil);
	setcookie(ck);
}

Client.set(c: self ref Client, host, path, cookie: string)
{
	ck := parsecookie(host, path, cookie);
	if (ck == nil)
		return;
	b := array of byte cookie2str(ck);
	sys->write(c.fd, b, len b);
}

Client.getcookies(nil: self ref Client, host, path: string, secure: int): string
{
	dl: list of ref Domain;
	if (isipaddr(host))
		dl = getdoms(doms, nil, host);
	else {
		# note some domains match hosts
		# e.g. site X.com has to set a cookie for '.X.com'
		# to get around the netscape '.' count check
		# this messes up our domain checking
		# putting a '.' on the front of host is a safe way of handling this
#		host = "." + host;
		dl = getdoms(doms, host, nil);
	}
	cookies: list of ref Cookie;
	for (; dl != nil; dl = tl dl) {
		ckl := (hd dl).cookies;
		for (ck := ckl.next; ck != nil; ck = ck.link.next) {
			if (ck.secure && !secure)
				continue;
			if (!S->prefix(ck.path, path))
				continue;
			ck.touched = touch++;
			cookies = ck :: cookies;
		}
	}
	if (cookies == nil)
		return "";

	# sort w.r.t path len and creation order
	cka := array [len cookies] of ref Cookie;
	for (i := 0; cookies != nil; cookies = tl cookies)
		cka[i++] = hd cookies;

	mergesort(cka, nil, SORT_PATHLEN);

	s := sys->sprint("%s=%s", cka[0].name, cka[0].value);
	for (i = 1; i < len cka; i++)
		s += sys->sprint("; %s=%s", cka[i].name, cka[i].value);
	return s;
}

save()
{
	fd := sys->create(cookiepath, Sys->OWRITE, 8r600);
	if (fd == nil)
		return;
	cka := array [ncookies] of ref Cookie;
	ix := getallcookies(doms, cka, 0);
	mergesort(cka, nil, SORT_TOUCHED);

	for (i := 0; i < ix; i++) {
		ck := cka[i];
		if (ck.expire > now)
			sys->fprint(fd, "%s\n", cookie2str(cka[i]));
	}
}

parsecookie(dom, path, cookie: string): ref Cookie
{
	defpath := "/";
	if (path != nil)
		(defpath, nil) = S->splitr(path, "/");

	(nil, toks) := sys->tokenize(cookie, ";");
	namval := hd toks;
	toks = tl toks;

	(name, value) := S->splitl(namval, "=");
	name = trim(name);
	if (value != nil && value[0] == '=')
		value = value[1:];
	value = trim(value);

	ck := ref Cookie(name, value, dom, defpath, -1, 0, 0, nil);
	for (; toks != nil; toks = tl toks) {
		(name, value) = S->splitl(hd toks, "=");
		if (value != nil && value[0] == '=')
			value = value[1:];
		name = trim(name);
		value = trim(value);
		case S->tolower(name) {
		"domain" =>
			ck.dom = value;
		"expires" =>
			ck.expire = date2sec(value);
		"path" =>
			ck.path = value;
		"secure" =>
			ck.secure = 1;
		}
	}
	if (ckcookie(ck, dom, path))
		return ck;
	return nil;
}

# Top Level Domains as defined in Netscape cookie spec
tld := array [] of {
	".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
};

ckcookie(ck: ref Cookie, host, path: string): int
{
#dumpcookie(ck, "CKCOOKIE");
	if (ck == nil)
		return 0;
	if (ck.path == "" || ck.dom == "")
		return 0;
	if (host == "" || path == "")
		return 1;

# netscape does no path check on accpeting a cookie
# any page can set a cookie on any path within its domain.
# the filtering is done when sending cookies back to the server
#	if (!S->prefix(ck.path, path))
#		return 0;

	if (host == ck.dom)
		return 1;
	if (ck.dom[0] != '.' || len host < len ck.dom)
		return 0;

	ipaddr := S->drop(host, ".0123456789") == nil;
	if (ipaddr)
		# ip addresses have to match exactly
		return 0;

	D := host[len host - len ck.dom:];
	if (D != ck.dom)
		return 0;

	# netscape specific policy
	ndots := 0;
	for (i := 0; i < len D; i++)
		if (D[i] == '.')
			ndots++;
	for (i = 0; i < len tld; i++) {
		if (len D >= len tld[i] && D[len D - len tld[i]:] == tld[i]) {
			if (ndots < 2)
				return 0;
			return 1;
		}
	}
	if (ndots < 3)
		return 0;
	return 1;
}

trim(s: string): string
{
	is := 0;
	ie := len s;
	while(is < ie) {
		c := s[is];
		if(!(c == ' ' || c == '\t'))
			break;
		is++;
	}
	if(is == ie)
		return "";
	while(ie > is) {
		c := s[ie-1];
		if(!(c == ' ' || c == '\t'))
			break;
		ie--;
	}
	if(is >= ie)
		return "";
	if(is == 0 && ie == len s)
		return s;
	return s[is:ie];
}

kill(pid: int)
{
	sys->fprint(sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE), "kill");
}

date2sec(date: string): int
{
	Tm: import daytime;
	tm := daytime->string2tm(date);
	if(tm == nil || tm.year < 70 || tm.zone != "GMT")
		t := -1;
	else
		t = daytime->tm2epoch(tm);
	return t;
}

dumpcookie(ck: ref Cookie, msg: string)
{
	if (msg != nil)
		sys->print("%s: ", msg);
	if (ck == nil)
		sys->print("NIL\n");
	else {
		dbgval := ck.value;
		if (len dbgval > 10)
			dbgval = dbgval[:10];
		sys->print("dom[%s], path[%s], name[%s], value[%s], secure=%d\n", ck.dom, ck.path, ck.name, dbgval, ck.secure);
	}
}

splitdom(s: string): (string, string)
{
	for (ie := len s -1; ie > 0; ie--)
		if (s[ie] == '.')
			break;
	return (s[:ie], s[ie:]);
}