Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -31,13 +31,15 @@ where id = $1::bigint ]]; }; actor_fetch_xid = { - params = {rawstring}, sql = [[ + params = {lib.mem.ptr(int8)}, sql = [[ select a.id, a.nym, a.handle, a.origin, - a.bio, a.rank, a.quota, a.key, $1::text, + a.bio, a.rank, a.quota, a.key, + coalesce(a.handle || '@' || s.domain, + '@' || a.handle) as xid, coalesce(s.domain, (select value from parsav_config where key='domain' limit 1)) as domain @@ -45,11 +47,13 @@ left join parsav_servers as s on a.origin = s.id where $1::text = (a.handle || '@' || domain) or $1::text = ('@' || a.handle || '@' || domain) or - (a.origin is null and $1::text = ('@' || a.handle)) + (a.origin is null and + $1::text = a.handle or + $1::text = ('@' || a.handle)) ]]; }; actor_enum_local = { params = {}, sql = [[ @@ -64,18 +68,58 @@ actor_enum = { params = {}, sql = [[ select a.id, a.nym, a.handle, a.origin, a.bio, a.rank, a.quota, a.key, - a.handle ||'@'|| - coalesce(s.domain, - (select value from parsav_config - where key='domain' limit 1)) as xid + coalesce(a.handle || '@' || s.domain, + '@' || a.handle) as xid from parsav_actors a left join parsav_servers s on s.id = a.origin ]]; }; + + actor_auth_how = { + params = {rawstring, lib.store.inet}, sql = [[ + with mts as (select a.kind from parsav_auth as a + left join parsav_actors as u on u.id = a.uid + where (a.uid is null or u.handle = $1::text or ( + a.uid = 0 and a.name = $1::text + )) and + (a.netmask is null or a.netmask >> $2::inet) and + blacklist = false) + + select + (select count(*) from mts where kind like 'pw-%') > 0, + (select count(*) from mts where kind like 'otp-%') > 0, + (select count(*) from mts where kind like 'challenge-%') > 0, + (select count(*) from mts where kind = 'trust') > 0 + ]]; -- cheat + }; + + actor_session_fetch = { + params = {uint64, lib.store.inet}, sql = [[ + select a.id, a.nym, a.handle, a.origin, + a.bio, a.rank, a.quota, a.key, + coalesce(a.handle || '@' || s.domain, + '@' || a.handle) as xid, + + au.restrict, + array['post' ] <@ au.restrict as can_post, + array['edit' ] <@ au.restrict as can_edit, + array['acct' ] <@ au.restrict as can_acct, + array['upload'] <@ au.restrict as can_upload, + array['censor'] <@ au.restrict as can_censor, + array['admin' ] <@ au.restrict as can_admin + + from parsav_auth au + left join parsav_actors a on au.uid = a.id + left join parsav_servers s on a.origin = s.id + + where au.aid = $1::bigint and au.blacklist = false and + (au.netmask is null or au.netmask >> $2::inet) + ]]; + }; } local struct pqr { sz: intptr res: &lib.pq.PGresult @@ -87,10 +131,11 @@ terra pqr:len(row: intptr, col: intptr) return lib.pq.PQgetlength(self.res, row, col) end terra pqr:cols() return lib.pq.PQnfields(self.res) end terra pqr:string(row: intptr, col: intptr) -- not to be exported!! + if self:null(row,col) then return nil end var v = lib.pq.PQgetvalue(self.res, row, col) -- var r: lib.mem.ptr(int8) -- r.ct = lib.str.sz(v) -- r.ptr = v return v @@ -100,10 +145,11 @@ ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col)); ct = lib.pq.PQgetlength(self.res, row, col); } end terra pqr:String(row: intptr, col: intptr) -- suitable to be exported + if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) } s.ct = lib.pq.PQgetlength(self.res, row, col) return s end terra pqr:bool(row: intptr, col: intptr) @@ -178,10 +224,19 @@ args[i] = symbol(ty) ft[i] = `1 if ty == rawstring then counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]])) casts[i] = `[&int8]([args[i]]) + elseif ty == lib.store.inet then -- assume not CIDR + counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4 + casts[i] = quote + var ipbuf: int8[20] + ;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf)) + in &ipbuf[0] end + elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then + counters[i] = `[args[i]].ct + casts[i] = `[&int8]([args[i]].ptr) elseif ty:isintegral() then counters[i] = ty.bytes casts[i] = `[&int8](&[args[i]]) fixers[#fixers + 1] = quote --lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]]) @@ -214,22 +269,23 @@ end end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) var a: lib.mem.ptr(lib.store.actor) + if r:cols() >= 8 then a = [ lib.str.encapsulate(lib.store.actor, { - nym = {`r:string(row, 1); `r:len(row,1) + 1}; + nym = {`r:string(row,1), `r:len(row,1)+1}; + bio = {`r:string(row,4), `r:len(row,4)+1}; handle = {`r:string(row, 2); `r:len(row,2) + 1}; - bio = {`r:string(row, 4); `r:len(row,4) + 1}; xid = {`r:string(row, 8); `r:len(row,8) + 1}; }) ] else a = [ lib.str.encapsulate(lib.store.actor, { - nym = {`r:string(row, 1); `r:len(row,1) + 1}; + nym = {`r:string(row,1), `r:len(row,1)+1}; + bio = {`r:string(row,4), `r:len(row,4)+1}; handle = {`r:string(row, 2); `r:len(row,2) + 1}; - bio = {`r:string(row, 4); `r:len(row,4) + 1}; }) ] a.ptr.xid = nil end a.ptr.id = r:int(uint64, row, 0); a.ptr.rights = lib.store.rights_default(); @@ -347,10 +403,21 @@ var a = row_to_actor(&r, 0) a.ptr.source = src return a end end]; + + actor_fetch_xid = [terra(src: &lib.store.source, xid: lib.mem.ptr(int8)) + var r = queries.actor_fetch_xid.exec(src, xid) + if r.sz == 0 then + return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil } + else defer r:free() + var a = row_to_actor(&r, 0) + a.ptr.source = src + return a + end + end]; actor_enum = [terra(src: &lib.store.source) var r = queries.actor_enum.exec(src) if r.sz == 0 then return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil } @@ -375,74 +442,77 @@ actor_auth_how = [terra( src: &lib.store.source, ip: lib.store.inet, username: rawstring ) - var authview = src:conf_get('auth-source') defer authview:free() - var a: lib.str.acc defer a:free() - a:compose('with mts as (select a.kind from ',authview,[' ' .. sqlsquash [[as a - left join parsav_actors as u on u.id = a.uid - where (a.uid is null or u.handle = $1::text or ( - a.uid = 0 and a.name = $1::text - )) and - (a.netmask is null or a.netmask >> $2::inet) and - blacklist = false) - - select - (select count(*) from mts where kind like 'pw-%') > 0, - (select count(*) from mts where kind like 'otp-%') > 0, - (select count(*) from mts where kind like 'challenge-%') > 0, - (select count(*) from mts where kind = 'trust') > 0 ]]]) -- cheat var cs: lib.store.credset cs:clear(); - var ipbuf: int8[20] - ;[pqt[lib.store.inet](false)](ip, [&uint8](&ipbuf)) - var ipbl: intptr if ip.pv == 4 then ipbl = 8 else ipbl = 20 end - var params = arrayof(rawstring, username, [&int8](&ipbuf)) - var params_sz = arrayof(int, lib.str.sz(username), ipbl) - var params_ft = arrayof(int, 1, 1) - var res = lib.pq.PQexecParams([&lib.pq.PGconn](src.handle), a.buf, 2, nil, - params, params_sz, params_ft, 1) - if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then - if res == nil then - lib.bail('grievous error occurred checking for auth methods') - end - lib.bail('could not get auth methods for user ',username,':\n',lib.pq.PQresultErrorMessage(res)) - end - var r = pqr { res = res, sz = lib.pq.PQntuples(res) } + var r = queries.actor_auth_how.exec(src, username, ip) if r.sz == 0 then return cs end -- just in case + defer r:free() (cs.pw << r:bool(0,0)) (cs.otp << r:bool(0,1)) (cs.challenge << r:bool(0,2)) (cs.trust << r:bool(0,3)) - lib.pq.PQclear(res) return cs end]; actor_auth_pw = [terra( src: &lib.store.source, ip: lib.store.inet, username: rawstring, cred: rawstring ) - var authview = src:conf_get('auth-source') defer authview:free() - var a: lib.str.acc defer a:free() - a:compose('select a.aid from ',authview,[' ' .. sqlsquash [[as a + var q = [[select a.aid from parsav_auth as a left join parsav_actors as u on u.id = a.uid where (a.uid is null or u.handle = $1::text or ( a.uid = 0 and a.name = $1::text )) and (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and (a.netmask is null or a.netmask >> $4::inet) - order by blacklist desc limit 1]]]) + order by blacklist desc limit 1]] - [ checksha(`src.handle, `a.buf, 256, ip, username, cred) ] -- most common - [ checksha(`src.handle, `a.buf, 512, ip, username, cred) ] -- most secure - [ checksha(`src.handle, `a.buf, 384, ip, username, cred) ] -- weird - [ checksha(`src.handle, `a.buf, 224, ip, username, cred) ] -- weirdest + [ checksha(`src.handle, q, 256, ip, username, cred) ] -- most common + [ checksha(`src.handle, q, 512, ip, username, cred) ] -- most secure + [ checksha(`src.handle, q, 384, ip, username, cred) ] -- weird + [ checksha(`src.handle, q, 224, ip, username, cred) ] -- weirdest -- TODO: check pbkdf2-hmac -- TODO: check OTP return 0 end]; + + actor_session_fetch = [terra( + src: &lib.store.source, + aid: uint64, + ip : lib.store.inet + ): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) } + var r = queries.actor_session_fetch.exec(src, aid, ip) + if r.sz == 0 then goto fail end + do defer r:free() + + if r:null(0,0) then goto fail end + + var a = row_to_actor(&r, 0) + a.ptr.source = src + + var au = [lib.stat(lib.store.auth)] { ok = true } + au.val.aid = aid + au.val.uid = a.ptr.id + if not r:null(0,10) then -- restricted? + au.val.privs:clear() + (au.val.privs.post << r:bool(0,11)) + (au.val.privs.edit << r:bool(0,12)) + (au.val.privs.acct << r:bool(0,13)) + (au.val.privs.upload << r:bool(0,14)) + (au.val.privs.censor << r:bool(0,15)) + (au.val.privs.admin << r:bool(0,16)) + else au.val.privs:fill() end + + return au, a + end + + ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, + [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } + end]; } return b Index: cmdparse.t ================================================================== --- cmdparse.t +++ cmdparse.t @@ -4,32 +4,43 @@ local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end local helpstr = 'usage: parsav [-' .. flags .. '] [...]\n' options.entries = { {field = 'arglist', type = lib.mem.ptr(rawstring)} } - local shortcases, longcases, init = {}, {}, {} + local shortcases, longcases, init, verifiers = {}, {}, {}, {} local self = symbol(&options) local arg = symbol(rawstring) - local idxo = symbol(uint) + local idx = symbol(uint) + local argv = symbol(&rawstring) + local argc = symbol(int) + local optstack = symbol(intptr) local skip = label() local sanitize = function(s) return s:gsub('_','-') end for o,desc in pairs(tbl) do local consume = desc[3] or 0 options.entries[#options.entries + 1] = { - field = o, type = (consume > 0) and uint or bool + field = o, type = (consume > 0) and &rawstring or bool } helpstr = helpstr .. string.format(' -%s --%s: %s\n', desc[1], sanitize(o), desc[2]) end for o,desc in pairs(tbl) do local flag = desc[1] local consume = desc[3] or 0 - init[#init + 1] = quote [self].[o] = [consume > 0 and 0 or false] end - local ch if consume > 0 then ch = quote - [self].[o] = idxo - idxo = idxo + consume - end else ch = quote + init[#init + 1] = quote [self].[o] = [(consume > 0 and `nil) or false] end + local ch if consume > 0 then + ch = quote + [self].[o] = argv+(idx+1+optstack) + optstack = optstack + consume + end + verifiers[#verifiers+1] = quote + var terminus = argv + argc + if [self].[o] ~= nil and [self].[o] >= terminus then + lib.bail(['missing argument for command line option ' .. sanitize(o)]) + end + end + else ch = quote [self].[o] = true end end shortcases[#shortcases + 1] = quote case [int8]([string.byte(flag)]) then [ch] end end @@ -36,26 +47,26 @@ longcases[#longcases + 1] = quote if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end end end terra options:free() self.arglist:free() end - options.methods.parse = terra([self], argc: int, argv: &rawstring) + options.methods.parse = terra([self], [argc], [argv]) [init] var parseopts = true - var [idxo] = 1 + var [optstack] = 0 self.arglist = lib.mem.heapa(rawstring, argc) var finalargc = 0 - for i=1,argc do - var [arg] = argv[i] - if arg[0] == ('-')[0] and parseopts then - if arg[1] == ('-')[0] then -- long option + for [idx]=1,argc do + var [arg] = argv[idx] + if optstack > 0 then optstack = optstack - 1 goto [skip] end + if arg[0] == @'-' and parseopts then + if arg[1] == @'-' then -- long option if arg[2] == 0 then -- last option parseopts = false else [longcases] end else -- short options - var j = 1 - while arg[j] ~= 0 do + var j = 1 while arg[j] ~= 0 do switch arg[j] do [shortcases] end j = j + 1 end end else @@ -62,12 +73,13 @@ self.arglist.ptr[finalargc] = arg finalargc = finalargc + 1 end ::[skip]:: end + [verifiers] if finalargc == 0 then self.arglist:free() else self.arglist:resize(finalargc) end end options.helptxt = helpstr end return options end Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -38,10 +38,13 @@ when = os.date(); }; feat = {}; backends = defaultlist('parsav_backends', 'pgsql'); braingeniousmode = false; + embeds = { + {'style.css', 'text/css'}; + }; } if u.ping '.fslckout' or u.ping '_FOSSIL_' then if u.ping '_FOSSIL_' then default_os = 'windows' end conf.build.branch = u.exec { 'fossil', 'branch', 'current' } conf.build.checkout = (u.exec { 'fossil', 'sql', Index: crypt.t ================================================================== --- crypt.t +++ crypt.t @@ -15,19 +15,26 @@ local ctx = lib.pk.mbedtls_pk_context local struct hashalg { id: uint8 bytes: intptr } local m = { pemfile = uint8[const.maxpemsz]; - alg = { - sha1 = `hashalg {id = lib.md.MBEDTLS_MD_SHA1; bytes = 160/8}; - sha256 = `hashalg {id = lib.md.MBEDTLS_MD_SHA256; bytes = 256/8}; - sha512 = `hashalg {id = lib.md.MBEDTLS_MD_SHA512; bytes = 512/8}; - sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = 384/8}; - sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = 224/8}; - -- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!! - }; + algsz = { + sha1 = 160/8; + sha256 = 256/8; + sha512 = 512/8; + sha384 = 384/8; + sha224 = 224/8; + } } +m.alg = { + sha1 = `hashalg {id = lib.md.MBEDTLS_MD_SHA1; bytes = m.algsz.sha1}; + sha256 = `hashalg {id = lib.md.MBEDTLS_MD_SHA256; bytes = m.algsz.sha256}; + sha512 = `hashalg {id = lib.md.MBEDTLS_MD_SHA512; bytes = m.algsz.sha512}; + sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = m.algsz.sha384}; + sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = m.algsz.sha224}; + -- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!! +}; local callbacks = {} if config.feat.randomizer == 'kern' then local rnd = terralib.externfunction('getrandom', {&opaque, intptr, uint} -> ptrdiff); terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int return rnd(dest, sz, 0) Index: file.t ================================================================== --- file.t +++ file.t @@ -1,10 +1,11 @@ -- vim: ft=terra -- TODO: add support for windows IO calls local handle_type = int local posix = terralib.includec 'fcntl.h' local unistd = terralib.includec 'unistd.h' +local mm = terralib.includec 'sys/mman.h' struct file { handle: handle_type read: bool write: bool @@ -63,7 +64,36 @@ else lib.bail('invalid seek mode') end return unistd.lseek(self.handle, ofs, whence) end; } + +terra file:len(): intptr + var cur = self:seek(0, [file.seek.ofs]) + var sz = self:seek(0, [file.seek.eof]) + self:seek(cur, [file.seek.abs]) + return sz +end + +local struct mapping { + addr: &opaque + sz: intptr +} +terra mapping:unmap() + lib.dbg('releasing file mapping') + return mm.munmap(self.addr, self.sz) +end +-- provide for syncing mechanism? + +terra file:mapin(ofs: intptr, sz: intptr) + var prot = 0 + if self.read then prot = mm.PROT_READ end + if self.write then prot = prot or mm.PROT_WRITE end + if sz == 0 then sz = self:len() end + lib.dbg('mapping file into memory') + return mapping { + addr = mm.mmap(nil, sz, prot, mm.MAP_PRIVATE, self.handle, ofs); + sz = sz; + } +end return file Index: http.t ================================================================== --- http.t +++ http.t @@ -1,9 +1,13 @@ -- vim: ft=terra local m = {} local util = dofile('common.lua') +m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' } + +m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name + struct m.header { key: rawstring value: rawstring } struct m.page { @@ -17,13 +21,17 @@ [201] = 'Created'; [301] = 'Moved Permanently'; [302] = 'Found'; [303] = 'See Other'; [307] = 'Temporary Redirect'; - [404] = 'Not Found'; + [400] = 'Bad Request'; [401] = 'Unauthorized'; + [402] = 'Payment Required'; [403] = 'Forbidden'; + [404] = 'Not Found'; + [405] = 'Method Not Allowed'; + [406] = 'Not Acceptable'; [418] = 'I\'m a teapot'; [405] = 'Method Not Allowed'; [500] = 'Internal Server Error'; } local resptext = symbol(rawstring) @@ -38,10 +46,45 @@ m.codestr = terra(code: uint16) var [resptext] var [resplen] switch code do [respbranches] end return resptext, resplen end + +terra m.hier(uri: lib.mem.ptr(int8)): lib.mem.ptr(lib.mem.ref(int8)) + if uri.ct == 0 then return [lib.mem.ptr(lib.mem.ref(int8))] { ptr = nil, ct = 0 } end + var sz = 1 + var start = 0 if uri.ptr[0] == @'/' then start = 1 end + for i = start, uri.ct do if uri.ptr[i] == @'/' then sz = sz + 1 end end + var lst = lib.mem.heapa([lib.mem.ref(int8)], sz) + if sz == 0 then + lst.ptr[0].ptr = uri.ptr + lst.ptr[0].ct = uri.ct + return lst + end + + var idx: intptr = 0 + var len: intptr = 0 + lst.ptr[0].ptr = uri.ptr + for i = 0, uri.ct do + if uri.ptr[i] == @'/' then + if len == 0 then + lst.ptr[idx].ptr = lst.ptr[idx].ptr + 1 + goto skip + end + lst.ptr[idx].ct = len + idx = idx + 1 + lst.ptr[idx].ptr = uri.ptr + i + 1 + len = 0 + else + len = len + 1 + end + ::skip::end + lst.ptr[idx].ct = len + + return lst +end + m.page.methods = { free = terra(self: &m.page) self.body:free() self.headers:free() end; Index: makefile ================================================================== --- makefile +++ makefile @@ -26,13 +26,13 @@ # with the build apparatus. note that parsav is designed # to be fronted by a real web server like nginx if SSL # is to be used, so we don't turn on SSL in mongoose lib/mongoose/libmongoose.a: lib/mongoose lib/mongoose/mongoose.c lib/mongoose/mongoose.h $(CC) -c $ ty.bytes then ty = b.tree.type end + return quote + var _a = [a] + var _b = [b] + var r: ty if _a > _b then r = _a else r = _b end + in r end +end) + +m.smallest = macro(function(a,b) + local ty = a.tree.type + if b.tree.type.bytes < ty.bytes then ty = b.tree.type end + return quote + var _a = [a] + var _b = [b] + var r: ty if _a < _b then r = _a else r = _b end + in r end +end) terra m.hexdigit(hb: uint8): int8 var a = hb and 0x0F if a < 10 then return 0x30 + a else return 0x61 + (a-10) end Index: mem.t ================================================================== --- mem.t +++ mem.t @@ -17,12 +17,12 @@ ptr = [&ty:astype()](m.heapa_raw(sizeof(ty) * sz)); ct = sz; } end) -m.ptr = terralib.memoize(function(ty) - local t = terralib.types.newstruct(string.format('ptr<%s>', ty)) +local function mkptr(ty, dyn) + local t = terralib.types.newstruct(string.format('%s<%s>', dyn and 'ptr' or 'ref', ty)) t.entries = { {'ptr', &ty}; {'ct', intptr}; } t.ptr_basetype = ty @@ -31,45 +31,75 @@ if ty.methods.free then recurse = true end end t.metamethods.__not = macro(function(self) return `self.ptr end) - t.methods = { - free = terra(self: &t): bool - [recurse and quote - self.ptr:free() - end or {}] - if self.ct > 0 then - m.heapf(self.ptr) - self.ct = 0 - return true + if dyn then + t.methods = { + free = terra(self: &t): bool + [recurse and quote + self.ptr:free() + end or {}] + if self.ct > 0 then + m.heapf(self.ptr) + self.ct = 0 + return true + end + return false + end; + init = terra(self: &t, newct: intptr): bool + var nv = [&ty](m.heapa_raw(sizeof(ty) * newct)) + if nv ~= nil then + self.ptr = nv + self.ct = newct + return true + else return false end + end; + resize = terra(self: &t, newct: intptr): bool + var nv: &ty + if self.ct > 0 + then nv = [&ty](m.heapr_raw(self.ptr, sizeof(ty) * newct)) + else nv = [&ty](m.heapa_raw(sizeof(ty) * newct)) + end + if nv ~= nil then + self.ptr = nv + self.ct = newct + return true + else return false end + end; + } + terra t:ensure(n: intptr) + if self.ptr == nil then + if not self:init(n) then return t {ptr=nil,ct=0} end end - return false - end; - init = terra(self: &t, newct: intptr): bool - var nv = [&ty](m.heapa_raw(sizeof(ty) * newct)) - if nv ~= nil then - self.ptr = nv - self.ct = newct - return true - else return false end - end; - resize = terra(self: &t, newct: intptr): bool - var nv: &ty - if self.ct > 0 - then nv = [&ty](m.heapr_raw(self.ptr, sizeof(ty) * newct)) - else nv = [&ty](m.heapa_raw(sizeof(ty) * newct)) + if self.ct >= n then return @self end + self:resize(n) + return @self + end + end + terra t:advance(n: intptr) + self.ptr = self.ptr + n + self.ct = self.ct - n + return self.ptr + end + if not ty:isstruct() then + terra t:cmp_raw(other: &ty) + for i=0, self.ct do + if self.ptr[i] ~= other[i] then return false end end - if nv ~= nil then - self.ptr = nv - self.ct = newct - return true - else return false end - end; - } + return true + end + terra t:cmp(other: t) + if other.ct ~= self.ct then return false end + return self:cmp_raw(other.ptr) + end + end return t -end) +end + +m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end) +m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end) m.vec = terralib.memoize(function(ty) local v = terralib.types.newstruct(string.format('vec<%s>', ty.name)) v.entries = { {field = 'storage', type = m.ptr(ty)}; Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -6,11 +6,18 @@ lib = { init = {}; load = function(lst) for _, l in pairs(lst) do - lib[l] = terralib.loadfile(l .. '.t')() + local path = {} + for m in l:gmatch('([^:]+)') do path[#path+1]=m end + local tgt = lib + for i=1,#path-1 do + if tgt[path[i]] == nil then tgt[path[i]] = {} end + tgt = tgt[path[i]] + end + tgt[path[#path]] = terralib.loadfile(l:gsub(':','/') .. '.t')() end end; loadlib = function(name,hdr) local p = config.pkg[name] -- for _,v in pairs(p.dylibs) do @@ -24,11 +31,11 @@ if v.tree.type == ty then return fn(v,...) end end return (tbl[false])(v,...) end) end; - emit_unitary = function(fd,...) + emit_unitary = function(nl,fd,...) local code = {} for i,v in ipairs{...} do if type(v) == 'string' or type(v) == 'number' then local str = tostring(v) code[#code+1] = `lib.io.send(2, str, [#str]) @@ -40,14 +47,14 @@ else code[#code+1] = quote var n = v in lib.io.send(2, n, lib.str.sz(n)) end end end - code[#code+1] = `lib.io.send(fd, '\n', 1) + if nl then code[#code+1] = `lib.io.send(fd, '\n', 1) end return code end; - emitv = function(fd,...) + emitv = function(nl,fd,...) local vec = {} local defs = {} for i,v in ipairs{...} do local str, ct if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then @@ -68,11 +75,11 @@ end ct = ct or #str end vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct} end - vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} + if nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} end return quote [defs] var strs = array( [vec] ) in lib.uio.writev(fd, strs, [#vec]) end end; @@ -80,10 +87,36 @@ return quote var c: bool = [cond] var r: i.tree.type if c == true then r = i else r = e end in r end + end); + coalesce = macro(function(...) + local args = {...} + local ty = args[1].tree.type + local val = symbol(ty) + local empty if ty.type == 'integer' + then empty = `0 + else empty = `nil + end + local exp = quote val = [empty] end + + for i=#args, 1, -1 do + local v = args[i] + exp = quote + if [v] ~= [empty] + then val = v + else [exp] + end + end + end + + local q = quote + var [val] + [exp] + in val end + return q end); proc = { exit = terralib.externfunction('exit', int -> {}); getenv = terralib.externfunction('getenv', rawstring -> rawstring); }; @@ -99,39 +132,56 @@ local new = {} for k,v in pairs(tbl) do new[k] = v end setmetatable(new, getmetatable(tbl)) return new end; + osclock = terralib.includec 'time.h'; } if config.posix then lib.uio = terralib.includec 'sys/uio.h'; lib.emit = lib.emitv -- use more efficient call where available else lib.emit = lib.emit_unitary end +local starttime = global(lib.osclock.time_t) +local lastnoisetime = global(lib.osclock.time_t) local noise = global(uint8,1) local noise_header = function(code,txt,mod) if mod then - return string.format('\27[%s;1m(parsav::%s %s)\27[m ', code,mod,txt) + return string.format('\27[%s;1m(%s %s)\27[m ', code,mod,txt) else - return string.format('\27[%s;1m(parsav %s)\27[m ', code,txt) + return string.format('\27[%s;1m(%s)\27[m ', code,txt) + end +end + +local terra timehdr() + var now = lib.osclock.time(nil) + var diff = now - lastnoisetime + if diff > 30 then -- print cur time + lastnoisetime = now + var curtime: int8[26] + lib.osclock.ctime_r(&now, &curtime[0]) + for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/ + [ lib.emit(false, 2, '\27[1m[', `&curtime[0], ']\27[;36m\n +00 ') ] + else -- print time since last msg + var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0) + [ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ] end end + local defrep = function(level,n,code) return macro(function(...) - local q = lib.emit(2, noise_header(code,n), ...) - return quote - if noise >= level then [q] end - end + local q = lib.emit(true, 2, noise_header(code,n), ...) + return quote if noise >= level then timehdr(); [q] end end end); end lib.dbg = defrep(3,'debug', '32') lib.report = defrep(2,'info', '35') lib.warn = defrep(1,'warn', '33') lib.bail = macro(function(...) - local q = lib.emit(2, noise_header('31','fatal'), ...) + local q = lib.emit(true, 2, noise_header('31','fatal'), ...) return quote - [q] + timehdr(); [q] lib.proc.exit(1) end end); lib.stat = terralib.memoize(function(ty) local n = struct { @@ -161,10 +211,11 @@ local o = {} for i, name in ipairs(tbl) do o[name] = i end local struct set { _store: uint8[bytes] } local struct bit { _v: intptr _set: &set} terra set:clear() for i=0,bytes do self._store[i] = 0 end end + terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end set.members = tbl set.name = string.format('set<%s>', table.concat(tbl, '|')) set.metamethods.__entrymissing = macro(function(val, obj) if o[val] == nil then error('value ' .. val .. ' not in set') end return `bit { _v=[o[val] - 1], _set = &obj } @@ -221,56 +272,59 @@ lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h') lib.net = lib.loadlib('mongoose','mongoose.h') lib.pq = lib.loadlib('libpq','libpq-fe.h') lib.load { - 'mem', 'str', 'file', 'math', 'crypt'; - 'http', 'tpl', 'store'; + 'mem', 'math', 'str', 'file', 'crypt'; + 'http', 'session', 'tpl', 'store'; } local be = {} for _, b in pairs(config.backends) do be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() end lib.store.backends = global(`array([be])) lib.cmdparse = terralib.loadfile('cmdparse.t')() -lib.srv = terralib.loadfile('srv.t')() do local collate = function(path,f, ...) return loadfile(path..'/'..f..'.lua')(path, ...) end data = { view = collate('view','load'); + static = {}; + stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content } end +for i,e in ipairs(config.embeds) do local v = e[1] + local fh = io.open('static/' .. v,'r') + if fh == nil then error('static file ' .. v .. ' missing') end + data.static[v] = fh:read('*a') fh:close() +end -- slightly silly -- because we're using plain lua to gather up -- the template sources, they have to be actually turned into -- templates in the terra code, so we "mutate" them here for k,v in pairs(data.view) do local t = lib.tpl.mk { body = v, id = 'view/'..k } data.view[k] = t end -local pemdump = macro(function(pub, kp) - local msg = (pub:asvalue() and ' * emitting public key\n') or ' * emitting private key\n' - return quote - var buf: lib.crypt.pemfile - lib.mem.zero(buf) - lib.crypt.pem(pub, &kp, buf) - lib.emit(msg, buf, '\n') - --lib.io.send(1, msg, [#msg]) - --lib.io.send(1, [rawstring](&buf), lib.str.sz([rawstring](&buf))) - --lib.io.send(1, '\n', 1) - end -end) +lib.load { + 'srv'; + 'render:profile'; + 'render:userpage'; + 'route'; +} do local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) terra version() lib.io.send(1, p, [#p]) end end + terra noise_init() + starttime = lib.osclock.time(nil) + lastnoisetime = 0 var n = lib.proc.getenv('parsav_noise') if n ~= nil then if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then noise = n[0] - 0x30 return @@ -282,34 +336,75 @@ local options = lib.cmdparse { version = {'V', 'display information about the binary build and exit'}; quiet = {'q', 'do not print to standard out'}; help = {'h', 'display this list'}; backend_file = {'b', 'init from specified backend file', 1}; + static_dir = {'S', 'directory with overrides for static content', 1}; + builtin_data = {'B', 'do not load static content overrides at runtime under any circumstances'}; } + +local static_setup = quote end +local mapin = quote end +local odir = symbol(rawstring) +local pathbuf = symbol(lib.str.acc) +for i,e in ipairs(config.embeds) do local v = e[1] + local d = data.static[v] + static_setup = quote [static_setup] + ([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] { ptr = [d], ct = [#d] }) + end + mapin = quote [mapin] + var osz = pathbuf.sz + pathbuf:push([v],[#v]) + var f = lib.file.open(pathbuf.buf, [lib.file.mode.read]) + if f.ok then defer f.val:close() + var map = f.val:mapin(0,0) -- currently maps are leaked, maybe do something more elegant in future + lib.report('loading static override content from ', pathbuf.buf) + ([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] { + ptr = [rawstring](map.addr); + ct = map.sz; + }) + end + pathbuf.sz = osz + end +end +local terra static_init(mode: &options) + [static_setup] + if mode.builtin_data then return end + + var [odir] = lib.proc.getenv('parsav_override_dir') + if mode.static_dir ~= nil then + odir=@mode.static_dir + end + if odir == nil then return end + + var [pathbuf] defer pathbuf:free() + pathbuf:compose(odir,'/') + [mapin] +end + terra entry(argc: int, argv: &rawstring): int if argc < 1 then lib.bail('bad invocation!') end noise_init() [lib.init] -- shut mongoose the fuck up lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil) - var srv: lib.srv + var srv: lib.srv.overlord do var mode: options mode:parse(argc,argv) defer mode:free() + static_init(&mode) if mode.version then version() return 0 end if mode.help then lib.io.send(1, [options.helptxt], [#options.helptxt]) return 0 end var cnf: rawstring - if mode.backend_file ~= 0 - then if mode.arglist.ct >= mode.backend_file - then cnf = mode.arglist.ptr[mode.backend_file - 1] - else lib.bail('bad invocation, backend file not specified') end + if mode.backend_file ~= nil + then cnf = @mode.backend_file else cnf = lib.proc.getenv('parsav_backend_file') end if cnf == nil then cnf = "backend.conf" end srv:start(cnf) ADDED render/profile.t Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -0,0 +1,18 @@ +-- vim: ft=terra +local terra +render_profile(actor: &lib.store.actor) + var profile = data.view.profile { + nym = lib.coalesce(actor.nym, actor.handle); + bio = lib.coalesce(actor.bio, "tall, dark, and mysterious"); + xid = actor.xid; + avatar = "/no-avatars-yet.png"; + + nposts = '0', nfollows = '0'; + nfollowers = '0', nmutuals = '0'; + tweetday = 'novembuary 67th'; + } + + return profile:tostr() +end + +return render_profile ADDED render/userpage.t Index: render/userpage.t ================================================================== --- render/userpage.t +++ render/userpage.t @@ -0,0 +1,25 @@ +-- vim: ft=terra +local terra +render_userpage(co: &lib.srv.convo, actor: &lib.store.actor) + var ti: lib.str.acc defer ti:free() + if co.aid ~= 0 and co.who.id == actor.id then + ti:compose('my profile') + else + ti:compose('profile :: ', actor.handle) + end + var pftxt = lib.render.profile(actor) defer pftxt:free() + + var doc = data.view.docskel { + instance = co.srv.cfg.instance.ptr; + title = ti.buf; + body = pftxt.ptr; + class = 'profile'; + } + + var hdrs = array( + lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } + ) + doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) +end + +return render_userpage ADDED route.t Index: route.t ================================================================== --- route.t +++ route.t @@ -0,0 +1,113 @@ +-- vim: ft=terra +local r = lib.srv.route +local method = lib.http.method +local http = {} + +terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) + var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } + for i=2,uri.ct do + if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end + end + if handle.ct == 0 then + handle.ct = uri.ct - 2 + uri:advance(uri.ct) + else + if handle.ct + 2 < uri.ct then + uri:advance(handle.ct + 2) + --uri.ptr = uri.ptr + (handle.ct + 2) + --uri.ct = uri.ct - (handle.ct + 2) + end + end + + lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct}) + + var path = lib.http.hier(uri) defer path:free() + for i=0,path.ct do + lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct}) + end + + var actor = co.srv:actor_fetch_xid(handle) + if actor.ptr == nil then + co:complain(404,'no such user','no such user known to this server') + return + end + defer actor:free() + + lib.render.userpage(co, actor.ptr) +end + +terra http.actor_profile_uid(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), meth: method.t) + if path.ct < 2 then + co:complain(404,'bad url','invalid user url') + return + end + + var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct) + if not ok then + co:complain(400, 'bad user ID', 'that user ID is not valid') + return + end + + var actor = co.srv:actor_fetch_uid(uid) + if actor.ptr == nil then + co:complain(404, 'no such user', 'no user by that ID is known to this instance') + return + end + defer actor:free() + + lib.render.userpage(co, actor.ptr) +end + +do local branches = quote end + local filename, flen = symbol(&int8), symbol(intptr) + local page = symbol(lib.http.page) + local send = label() + local storage = data.stmap + for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2] + local d = data.static[id] + branches = quote [branches]; + if lib.str.ncmp(filename, id, lib.math.biggest([#id], flen)) == 0 then + page.headers.ptr[0].value = mime; + page.body = [lib.mem.ptr(int8)] { + ptr = storage[([i-1])].ptr; + ct = storage[([i-1])].ct; + } + goto [send] + end + end + end + terra http.static_content(co: &lib.srv.convo, [filename], [flen]) + var hdrs = array(lib.http.header{'Content-Type',nil}) + var [page] = lib.http.page { + respcode = 200; + headers = [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = 1 + } + } + [branches] + do return false end + ::[send]:: page:send(co.con) return true + end +end + +http.static_content:printpretty() + +-- entry points +terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) + if uri.ptr[0] ~= @'/' then + co:complain(404, 'what the hell', 'how did you do that') + elseif uri.ptr[1] == @'@' then + http.actor_profile_xid(co, uri, meth) + elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then + if meth ~= method.get then goto wrongmeth end + if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end + else + var path = lib.http.hier(uri) defer path:free() + if path.ptr[0]:cmp(lib.str.lit('user')) then + http.actor_profile_uid(co, path, meth) + else goto notfound end + end + + ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this path') do return end + ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end +end Index: schema.sql ================================================================== --- schema.sql +++ schema.sql @@ -1,11 +1,11 @@ \prompt 'domain name: ' domain +\prompt 'instance name: ' inst \prompt 'bind to socket: ' bind \qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in.' \prompt 'admin actor: ' admin -\qecho 'you will need to create an authentication view mapping your user database to something parsav can understand; see auth.sql for an example. enter the name of the view to use.' -\prompt 'auth view: ' auth +\qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.' begin; drop table if exists parsav_config; create table if not exists parsav_config ( @@ -14,11 +14,11 @@ ); insert into parsav_config (key,value) values ('bind',:'bind'), ('domain',:'domain'), - ('auth-source',:'auth'), + ('instance-name',:'inst'), ('administrator',:'admin'), ('server-secret', encode( digest(int8send((2^63 * (random()*2 - 1))::bigint), 'sha512'), 'base64')); ADDED session.t Index: session.t ================================================================== --- session.t +++ session.t @@ -0,0 +1,53 @@ +-- vim: ft=terra +-- sessions are implemented so as to avoid any local data storage. they +-- are tracked by storing an encrypted cookie which contains an authid, +-- a login epoch time, and a truncated hmac code authenticating both, all +-- encoded using Shorthand. we need functions to generate and parse these + +local m = { + maxlen = lib.math.shorthand.maxlen*3 + 2; + maxage = 2 * 60 * 60; -- 2 hours +} + +terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr + var ptr = out + ptr = ptr + lib.math.shorthand.gen(authid, ptr) + @ptr = @'.' ptr = ptr + 1 + ptr = ptr + lib.math.shorthand.gen(time, ptr) + @ptr = @'.' ptr = ptr + 1 + var len = ptr - out + var hash: uint8[lib.crypt.algsz.sha256] + lib.crypt.hmac(lib.crypt.alg.sha256, + [lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct}, + [lib.mem.ptr( int8)] {ptr = out, ct = len}, + &hash[0]) + ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr) + return ptr - out +end + +terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64): uint64 -- returns either 0 or a valid authid + var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct) + if authid_sz == 0 then return 0 end + if authid_sz + 1 > c.ct then return 0 end + var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1)) + if time_sz == 0 then return 0 end + if (authid_sz + time_sz + 2) > c.ct then return 0 end + var hash_sz = c.ct - (authid_sz + time_sz + 2) + + var knownhash: uint8[lib.crypt.algsz.sha256] + lib.crypt.hmac(lib.crypt.alg.sha256, + [lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct}, + [lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz}, + &knownhash[0]) + + var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz) + var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz) + var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz) + if not (timeok and authok and hashok) then return 0 end + if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0 end + if now - time > m.maxage then return 0 end + + return authid +end + +return m Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -1,111 +1,26 @@ -- vim: ft=terra local util = dofile 'common.lua' + +local struct srv +local struct cfgcache { + secret: lib.mem.ptr(int8) + instance: lib.mem.ptr(int8) + overlord: &srv +} local struct srv { sources: lib.mem.ptr(lib.store.source) webmgr: lib.net.mg_mgr webcon: &lib.net.mg_connection + cfg: cfgcache } -local handle = { - http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque) - switch event do - case lib.net.MG_EV_HTTP_MSG then - lib.dbg('routing HTTP request') - var msg = [&lib.net.mg_http_message](p) - - end - end - end; -} -local char = macro(function(ch) return `[string.byte(ch:asvalue())] end) -local terra cfg(s: &srv, befile: rawstring) - lib.report('configuring backends from ', befile) - - var fr = lib.file.open(befile, [lib.file.mode.read]) - if fr.ok == false then - lib.bail('could not open configuration file ', befile) - end - - var f = fr.val - var c: lib.mem.vec(lib.store.source) c:init(8) - var text: lib.str.acc text:init(64) - do var buf: int8[64] - while true do - var ct = f:read(buf, [buf.type.N]) - if ct == 0 then break end - text:push(buf, ct) - end - end - f:close() - - var cur = text.buf - var segs: tuple(&int8, &int8)[3] = array( - {[&int8](0),[&int8](0)}, - {[&int8](0),[&int8](0)}, - {[&int8](0),[&int8](0)} - ) - var segdup = [terra(s: {rawstring, rawstring}) - var sz = s._1 - s._0 - var str = s._0 - return [lib.mem.ptr(int8)] { - ptr = lib.str.ndup(str, sz); - ct = sz; - } - end] - var fld = 0 - while (cur - text.buf) < text.sz do - if segs[fld]._0 == nil then - if not (@cur == char(' ') or @cur == char('\t') or @cur == char('\n')) then - segs[fld] = {cur, nil} - end - else - if fld < 2 and @cur == char(' ') or @cur == char('\t') then - segs[fld]._1 = cur - fld = fld + 1 - segs[fld] = {nil, nil} - elseif @cur == char('\n') or cur == text.buf + (text.sz-1) then - if fld < 2 then lib.bail('incomplete backend line in ', befile) else - segs[fld]._1 = cur - var src = c:new() - src.id = segdup(segs[0]) - src.string = segdup(segs[2]) - src.backend = nil - for i = 0,[lib.store.backends.type.N] do - if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then - src.backend = &lib.store.backends[i] - break - end - end - if src.backend == nil then - lib.bail('unknown backend in ', befile) - end - src.handle = nil - fld = 0 - segs[0] = {nil, nil} - end - end - end - cur = cur + 1 - end - text:free() - - s.sources = c:crush() +terra cfgcache:free() -- :/ + self.secret:free() + self.instance:free() end ---srv.methods.conf_set = terra(self: &srv, key: rawstring, val:rawstring) --- self.sources.ptr[0]:conf_set(key, val) ---end - -terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring) - var cs: lib.store.credset cs:clear() - for i=0,self.sources.ct do - var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn) - cs = cs + set - end - return cs -end srv.metamethods.__methodmissing = macro(function(meth, self, ...) local primary, ptr, stat, simple, oid = 0,1,2,3,4 local tk, rt = primary local expr = {...} for _,f in pairs(lib.store.backend.entries) do @@ -148,22 +63,271 @@ end end in r end end end) + +local struct convo { + srv: &srv + con: &lib.net.mg_connection + msg: &lib.net.mg_http_message + aid: uint64 -- 0 if logged out + who: &lib.store.actor -- who we're logged in as, if aid ~= 0 +} + +-- this is unfortunately necessary to work around a terra bug +-- it can't seem to handle forward-declarations of structs in C + +local getpeer +do local struct strucheader { + next: &lib.net.mg_connection + mgr: &lib.net.mg_mgr + peer: lib.net.mg_addr + } + terra getpeer(con: &lib.net.mg_connection) + return [&strucheader](con).peer + end +end + +terra convo:complain(code: uint16, title: rawstring, msg: rawstring) + var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }) + + var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free() + var body = data.view.docskel { + instance = self.srv.cfg.instance.ptr; + title = ti.buf; + body = msg; + class = 'error'; + } + + if body.body == nil then + body.body = "i'm sorry, dave. i can't let you do that" + end + + body:send(self.con, code, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = [hdrs.type.N] + }) +end + +local urimatch = macro(function(uri, ptn) + return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1) +end) + +local route = {} -- these are defined in route.t, as they need access to renderers +terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {} + +local handle = { + http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque) + var server = [&srv](ext) + var mgpeer = getpeer(con) + var peer = lib.store.inet { port = mgpeer.port; } + if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end + if peer.pv == 6 then + for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end + else -- v4 + @[&uint32](&peer.v4) = mgpeer.ip + end + -- the peer property is currently broken and there is precious + -- little i can do about this -- it always reports a peer v4 IP + -- of 0.0.0.0, altho the port seems to come through correctly. + -- for now i'm leaving it as is, but note that netmask restrictions + -- WILL NOT WORK until upstream gets its shit together. FIXME + + switch event do + case lib.net.MG_EV_HTTP_MSG then + lib.dbg('routing HTTP request') + var msg = [&lib.net.mg_http_message](p) + var co = convo { + con = con, srv = server, msg = msg; + aid = 0, who = nil; + } + + -- we need to check if there's any cookies sent with the request, + -- and if so, whether they contain any credentials. this will be + -- used to set the auth parameters in the http conversation + var cookies_p = lib.http.findheader(msg, 'Cookie') + if cookies_p ~= nil then + var cookies = cookies_p.ptr + var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0} + var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0} + var i = 0 while i < cookies_p.ct and + cookies[i] ~= 0 and + cookies[i] ~= @'\r' and + cookies[i] ~= @'\n' do -- cover all the bases + if val.ptr == nil then + if cookies[i] == @'=' then + key.ct = (cookies + i) - key.ptr + val.ptr = cookies + i + 1 + end + i = i + 1 + else + if cookies[i] == @';' then + val.ct = (cookies + i) - val.ptr + if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then + goto foundcookie + end + + i = i + 1 + i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies + key.ptr = cookies + i + val.ptr = nil + else i = i + 1 end + end + end + if val.ptr == nil then goto nocookie end + val.ct = (cookies + i) - val.ptr + if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then + goto nocookie + end + ::foundcookie:: do + var aid = lib.session.cookie_interpret(server.cfg.secret, + [lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct}, + lib.osclock.time(nil)) + if aid ~= 0 then co.aid = aid end + end ::nocookie::; + end + + if co.aid ~= 0 then + var sess, usr = co.srv:actor_session_fetch(co.aid, peer) + if sess.ok == false then co.aid = 0 else co.who = usr.ptr end + end + + var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free() + var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1) + + var uri = uridec + if urideclen == -1 then + for i = 0,msg.uri.len do + if msg.uri.ptr[i] == @'+' + then uri.ptr[i] = @' ' + else uri.ptr[i] = msg.uri.ptr[i] + end + end + uri.ct = msg.uri.len + else uri.ct = urideclen end + lib.dbg('routing URI ', {uri.ptr, uri.ct}) + + if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then + route.dispatch_http(&co, uri, [lib.http.method.get]) + else + co:complain(400,'unknown method','you have submitted an invalid http request') + end + + if co.aid ~= 0 then lib.mem.heapf(co.who) end + end + end + end; +} + +local terra cfg(s: &srv, befile: rawstring) + lib.report('configuring backends from ', befile) + + var fr = lib.file.open(befile, [lib.file.mode.read]) + if fr.ok == false then + lib.bail('could not open configuration file ', befile) + end + + var f = fr.val + var c: lib.mem.vec(lib.store.source) c:init(8) + var text: lib.str.acc text:init(64) + do var buf: int8[64] + while true do + var ct = f:read(buf, [buf.type.N]) + if ct == 0 then break end + text:push(buf, ct) + end + end + f:close() + + var cur = text.buf + var segs: tuple(&int8, &int8)[3] = array( + {[&int8](0),[&int8](0)}, + {[&int8](0),[&int8](0)}, + {[&int8](0),[&int8](0)} + ) + var segdup = [terra(s: {rawstring, rawstring}) + var sz = s._1 - s._0 + var str = s._0 + return [lib.mem.ptr(int8)] { + ptr = lib.str.ndup(str, sz); + ct = sz; + } + end] + var fld = 0 + while (cur - text.buf) < text.sz do + if segs[fld]._0 == nil then + if not (@cur == @' ' or @cur == @'\t' or @cur == @'\n') then + segs[fld] = {cur, nil} + end + else + if fld < 2 and @cur == @' ' or @cur == @'\t' then + segs[fld]._1 = cur + fld = fld + 1 + segs[fld] = {nil, nil} + elseif @cur == @'\n' or cur == text.buf + (text.sz-1) then + if fld < 2 then lib.bail('incomplete backend line in ', befile) else + segs[fld]._1 = cur + var src = c:new() + src.id = segdup(segs[0]) + src.string = segdup(segs[2]) + src.backend = nil + for i = 0,[lib.store.backends.type.N] do + if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then + src.backend = &lib.store.backends[i] + break + end + end + if src.backend == nil then + lib.bail('unknown backend in ', befile) + end + src.handle = nil + fld = 0 + segs[0] = {nil, nil} + end + end + end + cur = cur + 1 + end + text:free() + + if c.sz > 0 then + s.sources = c:crush() + else + s.sources.ptr = nil + s.sources.ct = 0 + end +end + +terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring) + var cs: lib.store.credset cs:clear() + for i=0,self.sources.ct do + var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn) + cs = cs + set + end + return cs +end + +terra cfgcache.methods.load :: {&cfgcache} -> {} +terra cfgcache:init(o: &srv) + self.overlord = o + self:load() +end srv.methods.start = terra(self: &srv, befile: rawstring) cfg(self, befile) var success = false + if self.sources.ct == 0 then lib.bail('no data sources specified') end for i=0,self.sources.ct do var src = self.sources.ptr + i lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')') src.handle = src.backend.open(src) if src.handle ~= nil then success = true end end if not success then lib.bail('could not connect to any data sources!') end + + self.cfg:init(self) var dbbind = self:conf_get('bind') var envbind = lib.proc.getenv('parsav_bind') var bind: rawstring if envbind ~= nil then @@ -172,12 +336,18 @@ bind = dbbind.ptr else bind = '[::]:10917' end lib.report('binding to ', bind) lib.net.mg_mgr_init(&self.webmgr) - self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, nil) + self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self) + var buf: int8[lib.session.maxlen] + var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0]) + buf[len] = 0 + + var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil)) + lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid) if dbbind.ptr ~= nil then dbbind:free() end end srv.methods.poll = terra(self: &srv) @@ -191,6 +361,15 @@ src:close() end self.sources:free() end -return srv +terra cfgcache:load() + self.instance = self.overlord:conf_get('instance-name') + self.secret = self.overlord:conf_get('server-secret') +end + +return { + overlord = srv; + convo = convo; + route = route; +} ADDED static/style.scss Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss Index: store.t ================================================================== --- store.t +++ store.t @@ -12,10 +12,13 @@ 'follow', 'mute', 'block' }; credset = lib.set { 'pw', 'otp', 'challenge', 'trust' }; + privset = lib.set { + 'post', 'edit', 'acct', 'upload', 'censor', 'admin' + } } local str = rawstring --lib.mem.ptr(int8) struct m.source @@ -30,10 +33,12 @@ visible: bool post: bool shout: bool propagate: bool upload: bool + acct: bool + edit: bool -- admin powers -- default off ban: bool config: bool censor: bool @@ -142,14 +147,13 @@ struct m.auth { aid: uint64 uid: uint64 aname: str netmask: m.inet - restrict: lib.mem.ptr(rawstring) + privs: m.privset blacklist: bool } - -- backends only handle content on the local server struct m.backend { id: rawstring open: &m.source -> &opaque close: &m.source -> {} @@ -158,11 +162,11 @@ conf_set: {&m.source, rawstring, rawstring} -> {} conf_reset: {&m.source, rawstring} -> {} actor_save: {&m.source, m.actor} -> bool actor_create: {&m.source, m.actor} -> bool - actor_fetch_xid: {&m.source, rawstring} -> lib.mem.ptr(m.actor) + actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) @@ -185,10 +189,15 @@ -- handles API authentication -- origin: inet -- handle: rawstring -- key: rawstring (X-API-Key) actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth) + actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)} + -- retrieves an auth record + actor combo suitable by AID suitable + -- for determining session validity & caps + -- aid: uint64 + -- origin: inet actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) actor_conf_int: cnf(intptr, lib.stat(intptr)) post_save: {&m.source, &m.post} -> bool Index: str.t ================================================================== --- str.t +++ str.t @@ -1,7 +1,8 @@ -- vim: ft=terra -- string.t: string classes +local util = dofile('common.lua') local m = { sz = terralib.externfunction('strlen', rawstring -> intptr); cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); @@ -11,10 +12,11 @@ ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring); fmt = terralib.externfunction('asprintf', terralib.types.funcpointer({&rawstring,rawstring},{int},true)); bfmt = terralib.externfunction('sprintf', terralib.types.funcpointer({rawstring,rawstring},{int},true)); + span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring); } (lib.mem.ptr(int8)).metamethods.__cast = function(from,to,e) if from == &int8 then return `[lib.mem.ptr(int8)]{ptr = e, ct = m.sz(e)} @@ -33,20 +35,20 @@ local terra biggest(a: intptr, b: intptr) if a > b then return a else return b end end terra m.acc:init(run: intptr) - lib.dbg('initializing string accumulator') + --lib.dbg('initializing string accumulator') self.buf = [rawstring](lib.mem.heapa_raw(run)) self.run = run self.space = run self.sz = 0 return self end; terra m.acc:free() - lib.dbg('freeing string accumulator') + --lib.dbg('freeing string accumulator') if self.buf ~= nil and self.space > 0 then lib.mem.heapf(self.buf) end end; @@ -67,14 +69,14 @@ self.sz = 0 return pt end; terra m.acc:push(str: rawstring, len: intptr) - var llen = len + --var llen = len if str == nil then return self end - if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output - lib.dbg('pushing "',{str,llen},'" onto accumulator') + --if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output + -- lib.dbg('pushing "',{str,llen},'" onto accumulator') if self.buf == nil then self:init(self.run) end if len == 0 then len = m.sz(str) end if len >= self.space - self.sz then self.space = self.space + biggest(self.run,len + 1) self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space)) @@ -82,10 +84,15 @@ lib.mem.cpy(self.buf + self.sz, str, len) self.sz = self.sz + len self.buf[self.sz] = 0 return self end; + +m.lit = macro(function(str) + return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} +end) + m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8)) self:push(str.ptr, str.ct) return self end; m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8)) self:push(str.ptr, str.ct) str:free() return self end; m.acc.methods.compose = macro(function(self, ...) @@ -140,30 +147,37 @@ local memreq_exp = `0 local copiers = {} for k,v in pairs(vals) do local ty = (`box.obj.[k]).tree.type local kp + local isnull, nullify if ty.ptr_basetype then - kp = quote [box].obj.[k] = [ty] { [ptr] = [&ty.ptr_basetype]([ptr]) } ; end + kp = quote [box].obj.[k] = [ty] { ptr = [&ty.ptr_basetype]([ptr]) } ; end + nullify = quote [box].obj.[k] = [ty] { ptr = nil, ct = 0 } end else kp = quote [box].obj.[k] = [ty]([ptr]) ; end + nullify = quote [box].obj.[k] = nil end end local cpy if type(v) ~= 'table' or #v ~= 2 then cpy = quote [kp] ; [ptr] = m.cpy(ptr, v) end + isnull = `v == nil end if type(v) == 'string' then memreq_const = memreq_const + #v + 1 + isnull = `false elseif type(v) == 'table' and v.tree and (v.tree.type.ptr_basetype == int8 or v.tree.type.ptr_basetype == uint8) then cpy = quote [kp]; [ptr] = [&int8](lib.mem.cpy([ptr], [v].ptr, [v].ct)) end if ty.ptr_basetype then cpy = quote [cpy]; [box].obj.[k].ct = [v].ct end end + isnull = `[v].ptr == nil elseif type(v) == 'table' and v.asvalue and type(v:asvalue()) == 'string' then local str = tostring(v:asvalue()) memreq_const = memreq_const + #str + 1 + isnull = `false elseif type(v) == 'table' and #v == 2 then local str,sz = v[1],v[2] if type(sz) == 'number' then memreq_const = memreq_const + sz elseif type(sz:asvalue()) == 'number' then @@ -172,23 +186,56 @@ cpy = quote [kp] ; [ptr] = [&int8](lib.mem.cpy([ptr], str, sz)) end if ty.ptr_basetype then cpy = quote [cpy]; [box].obj.[k].ct = sz end end + isnull = `[str] == nil else memreq_exp = `(m.sz(v) + 1) + [memreq_exp] -- make room for NUL + isnull = `v == nil if ty.ptr_basetype then cpy = quote [cpy]; [box].obj.[k].ct = m.sz(v) end end end - copiers[#copiers + 1] = cpy + + copiers[#copiers + 1] = quote + if [isnull] then [nullify] + else [cpy] end + end end return quote var sz: intptr = memreq_const + [memreq_exp] var [box] = [&m.box(ty)](lib.mem.heapa_raw(sz)) var [ptr] = [box].storage [copiers] in [lib.mem.ptr(ty)] { ct = 1, ptr = &([box].obj) } end end + +terra m.cspan(str: lib.mem.ptr(int8), reject: lib.mem.ref(int8), maxlen: intptr) + for i=0, lib.math.smallest(maxlen,str.ct) do + if str.ptr[i] == 0 then return 0 end + for j=0, reject.ct do + if str.ptr[i] == reject.ptr[j] then return i end + end + end + return maxlen +end + +terra m.ffw(str: &int8, maxlen: intptr) + while maxlen > 0 and @str ~= 0 and + (@str == @' ' or @str == @'\t' or @str == @'\n') do + str = str + 1 + maxlen = maxlen - 1 + end + return str +end + +terra m.ffw_unsafe(str: &int8) + while @str ~= 0 and + (@str == @' ' or @str == @'\t' or @str == @'\n') do + str = str + 1 + end + return str +end return m Index: view/docskel.tpl ================================================================== --- view/docskel.tpl +++ view/docskel.tpl @@ -1,11 +1,11 @@ @instance :: @title - + - +

@title

@body Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -4,10 +4,11 @@ -- create templates from when we return to terra local path = ... local sources = { 'docskel'; 'tweet'; + 'profile'; } local ingest = function(filename) local hnd = io.open(path..'/'..filename) local txt = hnd:read('*a') ADDED view/profile.tpl Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -0,0 +1,22 @@ +
+ + + + + + + +
posts @nposts
following @nfollows
followers @nfollowers
mutuals @nmutuals
account created @tweetday
+ +
Index: view/tweet.tpl ================================================================== --- view/tweet.tpl +++ view/tweet.tpl @@ -1,12 +1,12 @@
@when
@text