ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/charon/cookiesrv.b/
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:]);
}