summaryrefslogtreecommitdiff
path: root/appl/charon/cookiesrv.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/charon/cookiesrv.b')
-rw-r--r--appl/charon/cookiesrv.b595
1 files changed, 595 insertions, 0 deletions
diff --git a/appl/charon/cookiesrv.b b/appl/charon/cookiesrv.b
new file mode 100644
index 00000000..f50c1ce9
--- /dev/null
+++ b/appl/charon/cookiesrv.b
@@ -0,0 +1,595 @@
+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 < ncookies; 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:]);
+}