diff options
Diffstat (limited to 'appl/charon/cookiesrv.b')
| -rw-r--r-- | appl/charon/cookiesrv.b | 595 |
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:]); +} |
