Index: api/lp/actor.t ================================================================== --- api/lp/actor.t +++ api/lp/actor.t @@ -11,11 +11,11 @@ "summary": %$desc, "alsoKnownAs": ["https://%+domain/@%+handle"], "publicKey": { "id": "%lpid#ident-rsa", "owner": "%lpid", - "publicKeyPem": %rsa + "publicKeyPem": %$rsa }, "icon": { "type": "Image", "url": "https://%+domain%+avi" }, @@ -28,28 +28,40 @@ "following": "https://%+domain/api/lp/rel/%uid/following" }]]; } local pstr = lib.str.t -terra cs(s: rawstring) return pstr {s, lib.str.sz(s)} end +terra cs(s: rawstring) return pstr {s, lib.trn(s == nil,0,lib.str.sz(s))} end local terra api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor) var lpid = co:stra(64) lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id) var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME + var upk = lib.crypt.loadpriv(actor.key) + var pem: lib.crypt.pemfile + + if not upk.ok then + lib.warn("could not load user's keypair; this is a sign of a bug, a corrupt database, or a problem with mbedtls") + else defer upk.val:free() + if not lib.crypt.pem(true, &upk.val, &pem[0]) then + pem[0] = 0; + lib.warn('could not export actor certificate as PEM file; there is a bug, the database is corrupt, or there is a problem in mbedtls') + end + end var body = tpl { domain = co.srv.cfg.domain; uid = uid:finalize(); lpid = lpid:finalize(); handle = cs(actor.handle); nym = cs(actor.nym); desc = cs(actor.bio); avi = cs(actor.avatar); - rsa = ''; + rsa = cs(&pem[0]); locked = 'false'; } co:json(body:poolstr(&co.srv.pool)) + end return api_lp_actor Index: api/lp/tweet.t ================================================================== --- api/lp/tweet.t +++ api/lp/tweet.t @@ -6,43 +6,93 @@ "type": "Note", "id": "https://@+domain/post/@^pid", "content": @$html, "source": @$raw, "attributedTo": "https://@+domain/user/@^uid", - "published": "@pubtime" + "actor": "https://@+domain/user/@^uid", + "published": "@pubtime", + "sensitive": false, + "directMessage": false, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "summary": @$subj @extra }]] local wrap = lib.tpl.mk [[{ "\@context": "https://@+domain/s/litepub.jsonld", "type": "@kind", "actor": "https://@+domain/user/@^uid", "published": "@pubtime", "id": "https://@+domain/api/lp/act/@^aid", + "to": ["https://www.w3.org/ns/activitystreams#Public"], "object": @obj }]] local terra -lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool) - var opdate = lib.conv.datetime(&co.srv.pool, p.posted) +lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool): pstr + var opdate = lib.munge.datetime(&co.srv.pool, p.posted) + + var extra: lib.str.acc extra:pool(&co.srv.pool,256) + + if p.parent ~= 0 then + extra:lpush ',"inReplyTo":"' + var par = co.srv:post_fetch(p.parent) + if not par then + lib.warn('database integrity violation: broken parent reference') + else defer par:free() + if par().localpost then -- gen uri for parent + extra:lpush'https://':qpush(co.srv.cfg.domain):lpush'/post/':shpush(p.parent) + else extra:push(par().uri,0) end + end + extra:lpush'"' + end + + extra:lpush ',"conversation":"' + if p.convoheaduri ~= nil then + extra:qpush(p.convoheaduri) + else + var cid: uint64 = 0 + if p.parent ~= 0 then + var top = co.srv:thread_top_find(p.parent) + var tp = co.srv:post_fetch(top) + if not tp then + lib.warn('database integrity violation: missing thread parent') + cid = p.id + else + if tp().convoheaduri ~= nil then + extra:push(tp().convoheaduri,0) + elseif tp().localpost == false then + extra:push(tp().uri,0) + else cid = top end + end + else + cid = p.id + end + if cid ~= 0 then + extra:lpush'https://':qpush(co.srv.cfg.domain) + :lpush'/post/':shpush(cid):lpush'/tree' + end + end + extra:lpush'"' var tweet = (obj { domain = co.srv.cfg.domain, uid = p.author, pid = p.id; html = lib.smackdown.html(&co.srv.pool, p.body, false); - raw = p.body, pubtime = opdate, extra = ''; + raw = p.body, pubtime = opdate, extra = extra:finalize(); + subj = lib.trn(p.subject ~= nil, pstr(p.subject), pstr''); }):poolstr(&co.srv.pool) if act_wrap then return (wrap { domain = co.srv.cfg.domain, obj = tweet; kind = lib.trn(p.rtdby == 0, 'Create', 'Announce'); uid = lib.trn(p.rtdby == 0, p.author, p.rtdby); aid = lib.trn(p.rtdby == 0, p.id, p.rtact); pubtime = lib.trn(p.rtdby == 0, opdate, - lib.conv.datetime(&co.srv.pool,p.rtdat)); + lib.munge.datetime(&co.srv.pool,p.rtdat)); }):poolstr(&co.srv.pool) else return tweet end end return lp_tweet Index: api/webfinger.t ================================================================== --- api/webfinger.t +++ api/webfinger.t @@ -12,10 +12,11 @@ }]] local terra webfinger(co: &lib.srv.convo) var res = co:pgetv('resource') + lib.dbg('got webfinger request for resource ', {res.ptr,res.ct}) if (not res) or not res:startswith 'acct:' then goto err end var acct = res + 5 var svp = lib.str.find(acct, '@') if svp:ref() then Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -459,21 +459,21 @@ }; post_create = { params = { uint64, rawstring, rawstring, rawstring, - uint64, uint64, rawstring + uint64, uint64, rawstring, rawstring }, sql = [[ insert into parsav_posts ( author, subject, acl, body, parent, posted, discovered, - circles, mentions, convoheaduri + circles, mentions, convoheaduri, uri ) values ( $1::bigint, case when $2::text = '' then null else $2::text end, $3::text, $4::text, $5::bigint, $6::bigint, $6::bigint, - array[]::bigint[], array[]::bigint[], $7::text + array[]::bigint[], array[]::bigint[], $7::text, $8::text ) returning id ]]; -- TODO array handling }; post_destroy_prepare = { @@ -504,10 +504,23 @@ from pg_temp.parsavpg_known_content as p where (p.post).parent = $1::bigint and (p.post).rtdby = 0 order by (p.post).posted, (p.post).discovered asc ]]; }; + + thread_top_find = { + params = {uint64}, sql = [[ + with recursive tree(gen,id,par) as ( + select 0, id, parent from parsav_posts where id = $1::bigint + union + select tree.gen + 1, p.id, p.parent from tree + inner join parsav_posts as p on p.id = tree.par + ) + + select id from tree order by gen desc limit 1 + ]]; + }; thread_latest_arrival_calc = { params = {uint64}, sql = [[ with recursive posts(id) as ( select id from parsav_posts where parent = $1::bigint @@ -1024,10 +1037,11 @@ return pqr {0, nil} else return pqr {ct, res} end end + q.exec.name = 'pgsql.' .. k .. '.exec' end local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact) var id = res:int(uint64,i,0) var idbuf: int8[lib.math.shorthand.maxlen] @@ -1046,23 +1060,29 @@ end local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post) var subj: rawstring, sblen: intptr var cvhu: rawstring, cvhlen: intptr + var uri: rawstring, urilen: intptr if r:null(row,3) then subj = nil sblen = 0 else subj = r:string(row,3) sblen = r:len(row,3)+1 end if r:null(row,10) then cvhu = nil cvhlen = 0 else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1 end + if r:null(row,12) + then uri = nil urilen = 0 + else uri = r:string(row,12) urilen = r:len(row,12)+1 + end var p = [ lib.str.encapsulate(lib.store.post, { subject = { `subj, `sblen }; acl = {`r:string(row,4), `r:len(row,4)+1}; body = {`r:string(row,5), `r:len(row,5)+1}; convoheaduri = { `cvhu, `cvhlen }; --FIXME + uri = { `uri, `urilen }; }) ] p.ptr.id = r:int(uint64,row,1) p.ptr.author = r:int(uint64,row,2) if r:null(row,6) then p.ptr.posted = 0 @@ -1079,17 +1099,17 @@ p.ptr.parent = r:int(uint64,row,9) if r:null(row,11) then p.ptr.chgcount = 0 else p.ptr.chgcount = r:int(uint32,row,11) end - p.ptr.accent = r:int(int16,row,12) - p.ptr.rtdby = r:int(uint64,row,13) - p.ptr.rtdat = r:int(uint64,row,14) - p.ptr.rtact = r:int(uint64,row,15) - p.ptr.likes = r:int(uint32,row,16) - p.ptr.rts = r:int(uint32,row,17) - p.ptr.isreply = r:bool(row,18) + p.ptr.accent = r:int(int16,row,13) + p.ptr.rtdby = r:int(uint64,row,14) + p.ptr.rtdat = r:int(uint64,row,15) + p.ptr.rtact = r:int(uint64,row,16) + p.ptr.likes = r:int(uint32,row,17) + p.ptr.rts = r:int(uint32,row,18) + p.ptr.isreply = r:bool(row,19) p.ptr.localpost = r:bool(row,0) return p end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) @@ -1096,10 +1116,11 @@ var a: lib.mem.ptr(lib.store.actor) var av: rawstring, avlen: intptr var nym: rawstring, nymlen: intptr var bio: rawstring, biolen: intptr var epi: rawstring, epilen: intptr + var key: &uint8, keylen: intptr var origin: uint64 = 0 var handle = r:_string(row, 2) if not r:null(row,3) then origin = r:int(uint64,row,3) end var avia = lib.str.acc {buf=nil} @@ -1125,30 +1146,32 @@ end if r:null(row,9) then epilen = 0 epi = nil else epi = r:string(row,9) epilen = r:len(row,9)+1 end + if r:null(row,8) then + keylen = 0 key = nil + else + var k = r:bin(row,8) + keylen = k.ct key = k.ptr + end a = [ lib.str.encapsulate(lib.store.actor, { nym = {`nym, `nymlen}; bio = {`bio, `biolen}; epithet = {`epi, `epilen}; avatar = {`av,`avlen}; handle = {`handle.ptr, `handle.ct + 1}; xid = {`r:string(row, 11); `r:len(row,11) + 1}; + key = {`key,`keylen}; }) ] a.ptr.id = r:int(uint64, row, 0); a.ptr.rights = lib.store.rights_default(); a.ptr.rights.rank = r:int(uint16, row, 6); a.ptr.rights.quota = r:int(uint32, row, 7); a.ptr.rights.invites = r:int(uint32, row, 12); a.ptr.knownsince = r:int(int64,row, 10); a.ptr.avatarid = r:int(uint64,row, 13); - if r:null(row,8) then - a.ptr.key.ct = 0 a.ptr.key.ptr = nil - else - a.ptr.key = r:bin(row,8) - end a.ptr.origin = origin if avia.buf ~= nil then avia:free() end return a end @@ -1466,11 +1489,11 @@ } end] if rsakeys.sz > 0 then defer rsakeys:free() for i=0, rsakeys.sz do var props = toprops(&rsakeys, i) lib.dbg('loading next RSA pubkey') - var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct) + var pub = lib.crypt.loadpub(props.pubkey) if pub.ok then defer pub.val:free() lib.dbg('checking pubkey against response') var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct) if vfy then lib.dbg('signature verified') @@ -1541,11 +1564,11 @@ src: &lib.store.source, post: &lib.store.post ): uint64 var r = queries.post_create.exec(src, post.author,post.subject,post.acl,post.body, - post.parent,post.posted,post.convoheaduri + post.parent,post.posted,post.convoheaduri,post.uri ) if r.sz == 0 then return 0 end defer r:free() var id = r:int(uint64,0,0) post.source = src @@ -2093,10 +2116,20 @@ lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N])) end return n end]; + + thread_top_find = [terra( + src: &lib.store.source, + post: uint64 + ): uint64 + var r = queries.thread_top_find.exec(src,post) + if r.sz == 0 then return 0 end + defer r:free() + return r:int(uint64,0,0) + end]; thread_latest_arrival_calc = [terra( src: &lib.store.source, post: uint64 ): lib.store.timepoint Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -108,10 +108,11 @@ discovered bigint, edited bigint, parent bigint, convoheaduri text, chgcount integer, + uri text, -- ephemeral accent smallint, rtdby bigint, -- note that these must be 0 if the record rtdat bigint, -- in question does not represent an RT! rtid bigint, -- (this one too) @@ -126,11 +127,11 @@ select a.origin is null, ($1).id, ($1).author, ($1).subject,($1).acl, ($1).body, ($1).posted, ($1).discovered, ($1).edited, ($1).parent, ($1).convoheaduri,($1).chgcount, - coalesce(c.value, -1)::smallint, + ($1).uri, coalesce(c.value, -1)::smallint, $2 as rtdby, $3 as rtdat, $4 as rtid, re.likes, re.rts, ($1).parent in (select id from parsav_posts) from parsav_actors as a left join parsav_actor_conf_ints as c Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -61,10 +61,11 @@ comment on table parsav_rights is 'a backward-compatible list of every non-default privilege or deprivilege granted to a local user'; create table parsav_posts ( id , + uri text, -- null if local author bigint references parsav_actors(id) on delete cascade, subject text, acl text not null default 'all', -- just store the script raw 🤷 body text, posted bigint not null, DELETED conv.t Index: conv.t ================================================================== --- conv.t +++ conv.t @@ -1,26 +0,0 @@ --- vim: ft=terra -local m={} -local pstr = lib.str.t - -terra m.datetime(pool: &lib.mem.pool, when: lib.osclock.time_t) - -- formats a unix epoch time as a dumbfuck XSD datetime spec - var td: lib.osclock.tm - if lib.osclock.gmtime_r(&when, &td) == nil then - return pstr.null() - end - - var tpl = [lib.tpl.mk ('@#year:-@MM@#month:-@dd@#day:T'.. - '@hh@#hour::@mm@#min::@ss@#sec:Z')] { - year = td.tm_year + 1900, month = td.tm_mon + 1, day = td.tm_mday; - hour = td.tm_hour, min = td.tm_min, sec = td.tm_sec; - MM = lib.trn(td.tm_mon+1 < 10, '0', ''); - dd = lib.trn(td.tm_mday < 10, '0', ''); - ss = lib.trn(td.tm_sec < 10, '0', ''); - mm = lib.trn(td.tm_min < 10, '0', ''); - hh = lib.trn(td.tm_min < 10, '0', ''); - } - - return tpl:poolstr(pool) -end - -return m ADDED convo.t Index: convo.t ================================================================== --- convo.t +++ convo.t @@ -0,0 +1,406 @@ +-- vim: ft=terra +local srv = ... +local pstring = lib.str.t + +local struct convo { + srv: &srv + con: &lib.net.mg_connection + msg: &lib.net.mg_http_message + aid: uint64 -- 0 if logged out + aid_issue: lib.store.timepoint + who: &lib.store.actor -- who we're logged in as, if aid ~= 0 + peer: lib.store.inet + reqtype: lib.http.mime.t -- negotiated content type + method: lib.http.method.t + live_last: lib.store.timepoint + uploads: lib.mem.vec(lib.http.upload) + body: pstring +-- cache + ui_hue: uint16 + navbar: pstring + actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries +-- private + varbuf: pstring + vbofs: &int8 +} + +struct convo.page { + title: pstring + body: pstring + class: pstring + cache: bool +} + +local usrdefs = { + str = { + ['acl-follow' ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'}; + ['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'}; + }; +} + +terra convo:matchmime(mime: lib.http.mime.t): bool + return self.reqtype == [lib.http.mime.none] + or self.reqtype == mime +end + +terra convo:usercfg_str(uid: uint64, setting: pstring): pstring + var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting) + if not set then + [(function() + local q = quote return pstring.null() end + for key, dfl in pairs(usrdefs.str) do + local rv + if dfl.cfgfld then + rv = quote + var cf = self.srv.cfg.[dfl.cfgfld] + in terralib.select(not cf, pstring([dfl.fallback]), cf) end + elseif dfl.lit then rv = dfl.lit end + q = quote + if setting:cmp([key]) then return [rv] else [q] end + end + end + return q + end)()] + else return set end +end + +terra convo:uid2actor_live(uid: uint64) + var actor = self.srv:actor_fetch_uid(uid) + if actor:ref() then + if self.aid ~= 0 and self.who.id ~= uid then + actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid) + else -- defensive branch + actor(0).relationship = lib.store.relationship { + agent = 0, patient = uid; + rel = [lib.store.relation.null], + recip = [lib.store.relation.null], + } + end + end + return actor +end + +terra convo:uid2actor(uid: uint64) + var actor: &lib.store.actor = nil + for j = 0, self.actorcache.top do + if uid == self.actorcache(j).ptr.id then + actor = self.actorcache(j).ptr + break + end + end + if actor == nil then + actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr + end + return actor +end + +terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header)) + var doc = data.view.docskel { + instance = self.srv.cfg.instance; + title = pg.title; + body = pg.body; + class = pg.class; + navlinks = self.navbar; + attr = ''; + } + var attrbuf: int8[32] + if self.aid ~= 0 and self.ui_hue ~= 323 then + var hdecbuf: int8[21] + var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20]) + lib.str.cpy(&attrbuf[0], ' style="--hue:') + lib.str.cpy(&attrbuf[14], hdec) + var len = &hdecbuf[20] - hdec + lib.str.cpy(&attrbuf[14] + len, '"') + doc.attr = &attrbuf[0] + end + + if self.method == [lib.http.method.head] + then doc:head(self.con,code,hdrs) + else doc:send(self.con,code,hdrs) + end +end + +terra convo:statpage(code: uint16, pg: convo.page) + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' } + ) + self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0]; + ct = [hdrs.type.N] - lib.trn(pg.cache,1,0); + }) +end + +terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint) + var nbuf: int8[21] + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' }, + lib.http.header { + key = 'X-Live-Newest-Artifact'; + value = lib.math.decstr(lastup, &nbuf[20]); + }, + lib.http.header { key = 'Content-Length', value = '0' } + ) + if self.live_last ~= 0 and self.live_last == lastup then + lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200)) + for i = 0, [hdrs.type.N] do + lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value) + end + lib.net.mg_printf(self.con, '\r\n') + else + self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = 3 + }) + end +end + +terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end + +terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8)) + var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n" + if not lockdown then lockhdr = "" end + lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr) + lib.net.mg_send(self.con, data.ptr, data.ct) + lib.net.mg_send(self.con, '\r\n', 2) +end + +terra convo:json(data: pstring) + self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob()) +end + +terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8)) + var ty = lib.mime.lookup(mime) + if ty == nil then + lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not in database!') + mime = 'application/x-octet-stream' + else + if not ty.safe then + lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not safe!') + if ty.binary then + mime = 'application/x-octet-stream' + else + mime = 'text/plain' + end + end + end + self:bytestream_trusted(true, mime, data) +end + +terra convo:reroute_cookie(dest: rawstring, cookie: rawstring) + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Location', value = dest }, + lib.http.header { key = 'Set-Cookie', value = cookie } + ) + + var body = data.view.docskel { + instance = self.srv.cfg.instance.ptr; + title = 'rerouting'; + body = 'you are being redirected'; + class = 'error'; + navlinks = ''; + attr = ''; + } + + body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0) + }) +end + +terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end + +terra convo:installkey(dest: rawstring, aid: uint64) + var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] + do var p = &sesskey[0] + p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) + p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p) + lib.dbg('sending cookie ',{&sesskey[0],15}) + p = lib.str.ncpy(p, '; Path=/', 9) + end + self:reroute_cookie(dest, &sesskey[0]) +end + +terra convo:stra(sz: intptr) -- convenience function + var s: lib.str.acc + s:pool(&self.srv.pool,sz) + return s +end + +convo.methods.qstr = macro(function(self, ...) -- convenience string builder + local exp = {...} + return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize() +end) + +terra convo:complain(code: uint16, title: rawstring, msg: rawstring) + if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end + + if self:matchmime(lib.http.mime.html) then + var body = [convo.page] { + title = self:qstr('error :: ', title); + body = self:qstr('

',title,'

',msg,'

'); + class = 'error'; + cache = false; + } + + self:statpage(code, body) + else + var pg = lib.http.page { respcode = code, body = pstring.null() } + var ctt = lib.http.mime.none + if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json + pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}'] + {ekind = title, edesc = msg}):poolstr(&self.srv.pool) + elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text + pg.body = self:qstr('error: ',title,'\n',msg) + elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown + pg.body = self:qstr('# error :: ',title,'\n\n',msg) + elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi + pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg) + end + var cthdr = lib.http.header { 'Content-Type', 'text/plain' } + if ctt == lib.http.mime.none then + pg.headers.ct = 0 + else + pg.headers = lib.typeof(pg.headers) { &cthdr, 1 } + switch ctt do + escape + for key,ty in ipairs(lib.mime.types) do + if key ~= 'none' and lib.http.mime[key] ~= nil then + emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [ty.id[1]] end end + end + end + end + end + end + pg:send(self.con) + end +end + +terra convo:fail(code: uint16) + switch code do + escape + local stderrors = { + {400, 'bad request', "the action you have attempted on this resource is not meaningful"}; + {401, 'unauthorized', "this resource is not available at your clearance level"}; + {403, 'forbidden', "we can neither confirm nor deny the existence of this resource"}; + {404, 'resource not found', "that resource is not extant on or known to this server"}; + {405, 'method not allowed', "the method you have attempted on this resource is not meaningful"}; + {406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"}; + {500, 'internal server error', "parsav did a fucksy wucksy"}; + } + + for i,v in ipairs(stderrors) do + emit quote case uint16([v[1]]) then + self:complain([v]) + end end + end + end + else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug') + end +end + +terra convo:confirm(title: pstring, msg: pstring, cancel: pstring) + var conf = data.view.confirm { + title = title; + query = msg; + cancel = cancel; + } + var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title) + var body = conf:poolstr(&self.srv.pool) -- defer body:free() + var cf = [convo.page] { + title = ti:finalize(); + class = 'query'; + body = body; cache = false; + } + self:stdpage(cf) + --cf.title:free() +end + +convo.methods.assertpow = macro(function(self, pow) + return quote + var ok = true + if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then + ok = false + self:complain(403,'insufficient privileges',['you lack the '..pow:asvalue()..' power and cannot perform this action']) + end + in ok end +end) + +local pstr2mg, mg2pstr +do -- aaaaaaaaaaaaaaaaaaaaaaaa + mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v) + if v.field == 'body' or v[1] == 'body' then return v.type end + end) + terra pstr2mg(p: pstring): mgstr + return mgstr { ptr = p.ptr, len = p.ct } + end + terra mg2pstr(m: mgstr): pstring + return pstring { ptr = m.ptr, ct = m.len } + end +end + +-- CALL ONLY ONCE PER VAR +terra convo:postv_next(name: pstring, start: &pstring) + if self.varbuf.ptr == nil then + self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len) + self.vbofs = self.varbuf.ptr + end + var conv = pstr2mg(@start) + var o = lib.net.mg_http_get_var( + &conv, + name.ptr, self.vbofs, + self.varbuf.ct - (self.vbofs - self.varbuf.ptr) + ) + if o > 0 then + start:advance(name.ct + o + 2) + var r = self.vbofs + self.vbofs = self.vbofs + o + 1 + @(self.vbofs - 1) = 0 + var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) + return norm.ptr, norm.ct + else return nil, 0 end +end +terra convo:postv(name: pstring) + var start = mg2pstr(self.msg.body) + return self:postv_next(name, &start) +end +terra convo:ppostv(name: pstring) + var s,l = self:postv(name) + return pstring { ptr = s, ct = l } +end +do + local struct postiter { co: &convo where: pstring name: pstring } + terra convo:eachpostv(name: pstring) + return postiter { co = self, where = mg2pstr(self.msg.body), name = name } + end + postiter.metamethods.__for = function(self, body) + return quote + while true do + var str, len = self.co:postv_next(self.name, &self.where) + if str == nil then break end + [ body(`pstring {str, len}) ] + end + end + end +end + +terra convo:getv(name: rawstring) + if self.varbuf.ptr == nil then + self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len) + self.vbofs = self.varbuf.ptr + end + var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) + if o > 0 then + var r = self.vbofs + self.vbofs = self.vbofs + o + 1 + @(self.vbofs - 1) = 0 + var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) + return norm.ptr, norm.ct + else return nil, 0 end +end +terra convo:pgetv(name: rawstring) + var s,l = self:getv(name) + return pstring { ptr = s, ct = l } +end + +return convo Index: crypt.t ================================================================== --- crypt.t +++ crypt.t @@ -16,11 +16,12 @@ local ctx = lib.pk.mbedtls_pk_context terra ctx:free() lib.pk.mbedtls_pk_free(self) end local struct hashalg { id: uint8 bytes: intptr } local m = { - pemfile = uint8[const.maxpemsz]; + pemfile = int8[const.maxpemsz]; + derfile = uint8[const.maxdersz]; const = const; algsz = { sha1 = 160/8; sha256 = 256/8; sha512 = 512/8; @@ -73,15 +74,15 @@ end) terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr) return m.spray(dest,sz) end -terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool +terra m.pem(pub: bool, key: &ctx, buf: &int8): bool if pub then - return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0 + return lib.pk.mbedtls_pk_write_pubkey_pem(key, [&uint8](buf), const.maxpemsz) == 0 else - return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0 + return lib.pk.mbedtls_pk_write_key_pem(key, [&uint8](buf), const.maxpemsz) == 0 end end local binblob = lib.mem.ptr(uint8) terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob @@ -114,30 +115,31 @@ lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537) return pk end -terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx) +local binblob = lib.mem.ptr(uint8) +terra m.loadpriv(buf: binblob): lib.stat(ctx) lib.dbg('parsing saved private key') var pk: ctx lib.pk.mbedtls_pk_init(&pk) - var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0) + var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf.ptr, buf.ct, nil, 0) if rt == 0 then return [lib.stat(ctx)] { ok = true, val = pk } else lib.pk.mbedtls_pk_free(&pk) - return [lib.stat(ctx)] { ok = false } + return [lib.stat(ctx)] { ok = false, error = rt } end end -terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx) +terra m.loadpub(buf: binblob): lib.stat(ctx) lib.dbg('parsing saved key') var pk: ctx lib.pk.mbedtls_pk_init(&pk) - var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len) + var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf.ptr, buf.ct) if rt == 0 then return [lib.stat(ctx)] { ok = true, val = pk } else lib.pk.mbedtls_pk_free(&pk) return [lib.stat(ctx)] { ok = false, error = rt } Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -423,10 +423,11 @@ { 'actor degrade', 'alias for `actor rank 0`' }; { 'actor bestow ', 'bestow an epithet upon an actor' }; { 'actor instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' }; { 'actor proscribe', 'globally ban an actor from interacting with your server' }; { 'actor rehabilitate', 'lift a proscription on an actor' }; + { 'actor xkey [pem|der]', 'extract an actor\'s public key in either PEM or DER form' }; { 'actor purge-all ', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' }; }) ] return 1 end if umode.arglist.ct >= 2 then @@ -469,10 +470,11 @@ lib.report('actor purged') else goto cmderr end else goto cmderr end else goto cmderr end elseif lib.str.cmp(mode.arglist(0),'user') == 0 then + if mode.arglist.ct < 3 then goto cmderr end var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) if umode.help then [ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' […]', umode.type.helptxt.opts, cmdhelp { { 'user create', 'add a new user' }; { 'user auth new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' }; @@ -480,10 +482,11 @@ { 'user auth (|all) purge', 'delete all credentials that would allow this user to log in (where possible)' }; { 'user (grant|revoke) (|all)', 'grant or revoke a specific power to or from a user' }; { 'user emasculate', 'strip all administrative powers and rank from a user' }; { 'user forgive', 'restore all default powers to a user' }; { 'user suspend []', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'}; + { 'user xkey [pem|der]', 'extract an user\'s *private* key in either PEM or DER form' }; }) ] return 1 end var handle = umode.arglist(0) var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) @@ -526,10 +529,28 @@ end end usr.ptr.rights.powers = newprivs dlg:actor_save_privs(usr.ptr) + elseif lib.str.cmp(umode.arglist(1),'xkey') == 0 and umode.arglist.ct == 3 then + if not usr then lib.bail('unknown handle') end + if lib.str.cmp(umode.arglist(2),'pem') == 0 then + var pk = lib.crypt.loadpriv(usr().key) + if not pk.ok then + lib.bail('could not parse key! this is probably a bug') + end + var pem: lib.crypt.pemfile + if not lib.crypt.pem(false, &pk.val, &pem[0]) then + lib.bail('could not convert key to PEM! this is probably a bug') + end + lib.io.send(1, pem, lib.str.sz(&pem[0])) + pk.val:free() + elseif lib.str.cmp(umode.arglist(2),'der') == 0 then + -- TODO avoid dumping binary to tty + lib.warn('dumping user\'s \x1b[1mprivate\x1b[m key!') + lib.io.send(1, [&int8](usr().key.ptr), usr().key.ct) + else lib.bail('invalid key format') end elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then var reset = lib.str.cmp(umode.arglist(3),'reset') == 0 if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then -- FIXME enable resetting pws for users who have -- not logged in yet Index: mime.t ================================================================== --- mime.t +++ mime.t @@ -1,16 +1,109 @@ +-- vim: ft=terra local knowntypes = { - ['text/csrc'] = { - ext = 'c', lang = 'c'; - }; - ['text/html'] = { - ext = 'html', lang = 'html'; - unsafe = true; + html = { + ext = 'html', kind = 'markup', unsafe = true, id = { + 'text/html'; + 'application/xhtml+xml'; + 'application/vnd.wap.xhtml+xml'; + }; }; - ['text/x-lua'] = { - ext = 'lua', lang = 'lua'; - }; - ['text/markdown'] = { + flash = { ext = 'swf', kind = 'vm_prog', id = 'application/x-shockwave-flash', unsafe = true, binary = true }; + java = { ext = 'java', kind = 'vm_prog', id = 'application/java', unsafe = true, binary = true }; + css = { ext = 'css', kind = 'lang', id = 'text/css'}; + text = { ext = 'txt', kind = 'text', id = 'text/plain' }; + c = { ext = 'c', kind = 'prog_lang', id = 'text/csrc' }; + xml = { ext = 'xml', kind = 'markup', unsafe = true, id = 'text/xml' }; + lua = { ext = 'lua', kind = 'prog_lang', id = 'text/x-lua' }; + ansi = { ext = 'ans', kind = 'text', id = 'text/x-ansi', doc = true, binary = true}; + mkdown = { ext = 'md', kind = 'text', doc = true; id = 'text/markdown'; formatter = 'smackdown'; - ext = 'md', doc = true; + }; + json = { + ext = 'json', kind = 'lang', id = { + 'application/json'; + 'application/activity+json'; + 'application/ld+json'; + 'application/jrd+json'; + }; }; + svg = { ext = 'svg', kind = 'image', id = 'image/svg+xml' }; + webp = { ext = 'webp', kind = 'image', id = 'image/webp', binary = true }; + png = { ext = 'png', kind = 'image', id = 'image/png', binary = true }; + jpeg = { ext = 'jpg', kind = 'image', id = 'image/jpeg', binary = true }; + + -- wildcard + none = { id = '*/*' }; +} + +local idcache = {} + + +local pstr = lib.str.t +local filekind = lib.enum [[none image text lang prog_lang markup vm_prog]] +local struct mime { + key: pstr + canonical: pstr + safe: bool + binary: bool + ext: pstr + kind: filekind.t + output: lib.http.mime.t +} + +local typestore = {} +for typecode, ty in pairs(knowntypes) do + ty.key = typecode + if type(ty.id) == 'string' then ty.id = {ty.id} end + for i, mime in ipairs(ty.id) do + idcache[mime] = ty + end + + local op = lib.http.mime[typecode] + if op == nil then op = lib.http.mime.none end + print(typecode,op) + + ty.offset = #typestore + typestore[#typestore + 1] = `mime { + key = typecode; + canonical = [ty.id[1]]; + safe = [not ty.unsafe]; + ext = [ty.ext or `pstr{nil,0}]; + kind = [ty.kind and filekind[ty.kind] or filekind.none]; + binary = [ty.binary or false]; + output = [op]; + } + +end + +local typedex = global(`array([typestore])) +local struct mimemapping { + string: pstr + type: &mime +} + +local typemap_l = {} +for mime, ty in pairs(idcache) do + typemap_l[#typemap_l + 1] = `mimemapping { + string = mime; + type = &typedex[ [ty.offset] ]; + } + +end +local typemap = global(`array([typemap_l])); + + +return { + type = mime; + types = knowntypes; + tbl = idcache; + typedex = typedex; + lookup = terra(m: pstr): &mime + for i=0, [#typemap_l] do + if m:cmp(typemap[i].string) then + lib.io.fmt('returning type %s %u\n', typemap[i].type.key, typemap[i].type.output) + return typemap[i].type + end + end + return nil + end; } ADDED munge.t Index: munge.t ================================================================== --- munge.t +++ munge.t @@ -0,0 +1,26 @@ +-- vim: ft=terra +local m={} +local pstr = lib.str.t + +terra m.datetime(pool: &lib.mem.pool, when: lib.osclock.time_t) + -- formats a unix epoch time as a dumbfuck XSD datetime spec + var td: lib.osclock.tm + if lib.osclock.gmtime_r(&when, &td) == nil then + return pstr.null() + end + + var tpl = [lib.tpl.mk ('@#year:-@MM@#month:-@dd@#day:T'.. + '@hh@#hour::@mm@#min::@ss@#sec:Z')] { + year = td.tm_year + 1900, month = td.tm_mon + 1, day = td.tm_mday; + hour = td.tm_hour, min = td.tm_min, sec = td.tm_sec; + MM = lib.trn(td.tm_mon+1 < 10, '0', ''); + dd = lib.trn(td.tm_mday < 10, '0', ''); + ss = lib.trn(td.tm_sec < 10, '0', ''); + mm = lib.trn(td.tm_min < 10, '0', ''); + hh = lib.trn(td.tm_min < 10, '0', ''); + } + + return tpl:poolstr(pool) +end + +return m Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -256,11 +256,11 @@ elseif #tbl >= 2^8 then ty = uint16 end local o = { t = ty, members = tbl } local strings = {} for i, name in ipairs(tbl) do o[name] = `[ty]([i - 1]) - strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]} + strings[i] = `[lib.str.t]{ptr=[name], ct=[#name]} end o._str = terra(val: ty) var l = array([strings]) return l[val] end @@ -442,12 +442,13 @@ lib.load { 'mem', 'math', 'str', 'file', 'crypt', 'ipc'; 'http', 'html', 'session', 'tpl', 'store', 'acl'; + 'mime'; -- mimetype database & whitelist 'smackdown'; -- md-alike parser - 'conv'; -- miscellaneous conversion/munging functions + 'munge'; -- miscellaneous conversion/munging functions } local be = {} for _, b in pairs(config.backends) do be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))() Index: route.t ================================================================== --- route.t +++ route.t @@ -284,11 +284,11 @@ else co:complain(404, 'no such documentation', 'invalid documentation URL') end end -terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t) +terra http.tweet_page(co: &lib.srv.convo, path: hpath) var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct) if not ok then co:complain(400, 'bad post ID', 'that post ID is not valid') return end @@ -308,11 +308,11 @@ if path.ct == 3 then var lnk: lib.str.acc lnk:compose('/post/', path(1)) var lnkp = lnk:finalize() defer lnkp:free() if post:ref() and path(2):cmp(lib.str.lit 'snitch') then - if meth_get(meth) then + if meth_get(co.method) then var ui = data.view.report { badtweet = lib.render.tweet(co, post.ptr, nil); clnk = lnkp; } @@ -328,14 +328,14 @@ elseif post:ref() and post(0).author ~= co.who.id then co:complain(403, 'forbidden', 'you cannot alter other people\'s posts') return elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then if not co:assertpow('edit') then return end - if meth_get(meth) then + if meth_get(co.method) then lib.render.compose(co, post.ptr, nil) return - elseif meth == method.post then + elseif co.method == method.post then var newbody = co:postv('post')._0 var newacl = co:postv('acl')._0 var newsubj = co:postv('subject')._0 if newbody ~= nil then post(0).body = newbody end if newacl ~= nil then post(0).acl = newacl end @@ -343,33 +343,33 @@ post(0):save(true) co:reroute(lnkp.ptr) end return elseif path(2):cmp(lib.str.lit 'del') then - if meth_get(meth) then + if meth_get(co.method) then var conf: data.view.confirm if post:ref() then conf = data.view.confirm { - title = 'delete post'; - query = 'are you sure you want to delete this post?'; + title = 'delete post'; + query = 'are you sure you want to delete this post?'; cancel = lnkp } else conf = data.view.confirm { - title = 'cancel retweet'; - query = 'are you sure you want to undo this retweet?'; + title = 'cancel retweet'; + query = 'are you sure you want to undo this retweet?'; cancel = '/'; } end var body = conf:poolstr(&co.srv.pool) --defer body:free() co:stdpage([lib.srv.convo.page] { - title = 'post :: delete'; - class = 'query'; + title = 'post :: delete'; + class = 'query'; body = body; cache = false; }) return - elseif meth == method.post then + elseif co.method == method.post then var act = co:ppostv('act') if act:cmp('confirm') then if post:ref() then post().source:post_destroy(post().id) elseif rt.kind ~= 0 then @@ -380,27 +380,27 @@ else goto badop end end else goto badurl end end - if post:ref() and meth == method.post then + if post:ref() and co.method == method.post then if co.aid == 0 then goto noauth end var act = co:ppostv('act') - if act:cmp( 'like') and not co.srv:post_liked_uid(co.who.id,pid) then + if act:cmp('like') and not co.srv:post_liked_uid(co.who.id,pid) then co.srv:post_like(co.who.id, pid, false) post.ptr.likes = post.ptr.likes + 1 - elseif act:cmp( 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then + elseif act:cmp('dislike') and co.srv:post_liked_uid(co.who.id,pid) then co.srv:post_like(co.who.id, pid, true) post.ptr.likes = post.ptr.likes - 1 - elseif act:cmp( 'rt') then + elseif act:cmp('rt') then co.srv:post_retweet(co.who.id, pid, false) post.ptr.rts = post.ptr.rts + 1 - elseif act:cmp( 'post') then + elseif act:cmp('post') then var replytext = co:ppostv('post') var acl = co:ppostv('acl') var subj = co:ppostv('subject') - if not acl then acl = 'all' end + if not acl then acl = 'all' end if not replytext then goto badop end var reply = lib.store.post { author = co.who.id, parent = pid; subject = subj.ptr, acl = acl.ptr, body = replytext.ptr; @@ -410,16 +410,21 @@ else goto badop end end if not post then goto badurl end - lib.render.tweet_page(co, path, post.ptr) + if co:matchmime(lib.http.mime.html) then + lib.render.tweet_page(co, path, post.ptr) + elseif co:matchmime(lib.http.mime.json) then + co:json(lib.api.lp.tweet(co, post.ptr, false)) + else goto notacc end do return end ::noauth:: do co:fail(401) return end ::badurl:: do co:fail(404) return end ::badop :: do co:fail(405) return end + ::notacc:: do co:fail(406) return end end local terra credsec_for_uid(co: &lib.srv.convo, uid: uint64) var act = co:ppostv('act') @@ -467,11 +472,11 @@ if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy') return false end - var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL + var pkres = lib.crypt.loadpub(binblob{rsapub.ptr,rsapub.ct+1}) -- needs NUL if not pkres.ok then co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file') return false end var pk = pkres.val @@ -974,11 +979,11 @@ else -- hierarchical routes var path = lib.http.hier(&co.srv.pool, uri) --defer path:free() if path.ct > 1 and path(0):cmp('user') then http.actor_profile_uid(co, path) elseif path.ct > 1 and path(0):cmp('post') then - http.tweet_page(co, path, meth) + http.tweet_page(co, path) elseif path(0):cmp('tl') then http.timeline(co, path) elseif path(0):cmp('.well-known') then if path(1):cmp('webfinger') then if not co:matchmime(lib.http.mime.json) then goto nacc end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -1,18 +1,9 @@ -- vim: ft=terra local util = lib.util local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } local pstring = lib.mem.ptr(int8) -local mimetypes = { - {'html', 'text/html'}; - {'json', 'application/json'}; - {'json', 'application/activity+json'}; - {'json', 'application/ld+json'}; - {'mkdown', 'text/markdown'}; - {'text', 'text/plain'}; - {'ansi', 'text/x-ansi'}; -} local struct srv local struct cfgcache { secret: pstring pol_sec: secmode.t @@ -151,76 +142,17 @@ self:comp() self.posted = lib.osclock.time(nil) self.discovered = self.posted self.chgcount = 0 self.edited = 0 + self.uri = nil -- only for foreign posts + self.convoheaduri = nil -- ditto self.id = s:post_create(self) return self.id end -local struct convo { - srv: &srv - con: &lib.net.mg_connection - msg: &lib.net.mg_http_message - aid: uint64 -- 0 if logged out - aid_issue: lib.store.timepoint - who: &lib.store.actor -- who we're logged in as, if aid ~= 0 - peer: lib.store.inet - reqtype: lib.http.mime.t -- negotiated content type - method: lib.http.method.t - live_last: lib.store.timepoint - uploads: lib.mem.vec(lib.http.upload) - body: lib.str.t --- cache - ui_hue: uint16 - navbar: lib.mem.ptr(int8) - actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries --- private - varbuf: lib.mem.ptr(int8) - vbofs: &int8 -} - -struct convo.page { - title: pstring - body: pstring - class: pstring - cache: bool -} - -local usrdefs = { - str = { - ['acl-follow' ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'}; - ['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'}; - }; -} - -terra convo:matchmime(mime: lib.http.mime.t): bool - return self.reqtype == [lib.http.mime.none] - or self.reqtype == mime -end - -terra convo:usercfg_str(uid: uint64, setting: pstring): pstring - var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting) - if not set then - [(function() - local q = quote return pstring.null() end - for key, dfl in pairs(usrdefs.str) do - local rv - if dfl.cfgfld then - rv = quote - var cf = self.srv.cfg.[dfl.cfgfld] - in terralib.select(not cf, pstring([dfl.fallback]), cf) end - elseif dfl.lit then rv = dfl.lit end - q = quote - if setting:cmp([key]) then return [rv] else [q] end - end - end - return q - end)()] - else return set end -end - +local convo = terralib.loadfile 'convo.t'(srv) -- 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 { @@ -231,367 +163,13 @@ terra getpeer(con: &lib.net.mg_connection) return [&strucheader](con).peer end end -terra convo:uid2actor_live(uid: uint64) - var actor = self.srv:actor_fetch_uid(uid) - if actor:ref() then - if self.aid ~= 0 and self.who.id ~= uid then - actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid) - else -- defensive branch - actor(0).relationship = lib.store.relationship { - agent = 0, patient = uid; - rel = [lib.store.relation.null], - recip = [lib.store.relation.null], - } - end - end - return actor -end - -terra convo:uid2actor(uid: uint64) - var actor: &lib.store.actor = nil - for j = 0, self.actorcache.top do - if uid == self.actorcache(j).ptr.id then - actor = self.actorcache(j).ptr - break - end - end - if actor == nil then - actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr - end - return actor -end - -terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header)) - var doc = data.view.docskel { - instance = self.srv.cfg.instance; - title = pg.title; - body = pg.body; - class = pg.class; - navlinks = self.navbar; - attr = ''; - } - var attrbuf: int8[32] - if self.aid ~= 0 and self.ui_hue ~= 323 then - var hdecbuf: int8[21] - var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20]) - lib.str.cpy(&attrbuf[0], ' style="--hue:') - lib.str.cpy(&attrbuf[14], hdec) - var len = &hdecbuf[20] - hdec - lib.str.cpy(&attrbuf[14] + len, '"') - doc.attr = &attrbuf[0] - end - - if self.method == [lib.http.method.head] - then doc:head(self.con,code,hdrs) - else doc:send(self.con,code,hdrs) - end -end - -terra convo:statpage(code: uint16, pg: convo.page) - var hdrs = array( - lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, - lib.http.header { key = 'Cache-Control', value = 'no-store' } - ) - self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] { - ptr = &hdrs[0]; - ct = [hdrs.type.N] - lib.trn(pg.cache,1,0); - }) -end - -terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint) - var nbuf: int8[21] - var hdrs = array( - lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, - lib.http.header { key = 'Cache-Control', value = 'no-store' }, - lib.http.header { - key = 'X-Live-Newest-Artifact'; - value = lib.math.decstr(lastup, &nbuf[20]); - }, - lib.http.header { key = 'Content-Length', value = '0' } - ) - if self.live_last ~= 0 and self.live_last == lastup then - lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200)) - for i = 0, [hdrs.type.N] do - lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value) - end - lib.net.mg_printf(self.con, '\r\n') - else - self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] { - ptr = &hdrs[0], ct = 3 - }) - end -end - -terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end - -terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8)) - var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n" - if not lockdown then lockhdr = "" end - lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr) - lib.net.mg_send(self.con, data.ptr, data.ct) - lib.net.mg_send(self.con, '\r\n', 2) -end - -terra convo:json(data: pstring) - self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob()) -end - -terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8)) - -- TODO this is not a satisfactory solution; it's a bandaid on a gaping - -- chest wound. ultimately we need to compile a whitelist of safe mime - -- types as part of mimelib, but that is no small task. for now, this - -- will keep the patient from immediately bleeding out - if mime:cmp('text/html') or - mime:cmp('text/xml') or - mime:cmp('application/xhtml+xml') or - mime:cmp('application/vnd.wap.xhtml+xml') - then -- danger will robinson - mime = 'text/plain' - elseif mime:cmp('application/x-shockwave-flash') then - mime = 'application/octet-stream' - end - self:bytestream_trusted(true, mime, data) -end - -terra convo:reroute_cookie(dest: rawstring, cookie: rawstring) - var hdrs = array( - lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, - lib.http.header { key = 'Location', value = dest }, - lib.http.header { key = 'Set-Cookie', value = cookie } - ) - - var body = data.view.docskel { - instance = self.srv.cfg.instance.ptr; - title = 'rerouting'; - body = 'you are being redirected'; - class = 'error'; - navlinks = ''; - attr = ''; - } - - body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] { - ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0) - }) -end - -terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end - -terra convo:installkey(dest: rawstring, aid: uint64) - var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] - do var p = &sesskey[0] - p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) - p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p) - lib.dbg('sending cookie ',{&sesskey[0],15}) - p = lib.str.ncpy(p, '; Path=/', 9) - end - self:reroute_cookie(dest, &sesskey[0]) -end - -terra convo:stra(sz: intptr) -- convenience function - var s: lib.str.acc - s:pool(&self.srv.pool,sz) - return s -end - -convo.methods.qstr = macro(function(self, ...) -- convenience string builder - local exp = {...} - return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize() -end) - -terra convo:complain(code: uint16, title: rawstring, msg: rawstring) - if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end - - if self:matchmime(lib.http.mime.html) then - var body = [convo.page] { - title = self:qstr('error :: ', title); - body = self:qstr('

',title,'

',msg,'

'); - class = 'error'; - cache = false; - } - - self:statpage(code, body) - else - var pg = lib.http.page { respcode = code, body = pstring.null() } - var ctt = lib.http.mime.none - if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json - pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}'] - {ekind = title, edesc = msg}):poolstr(&self.srv.pool) - elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text - pg.body = self:qstr('error: ',title,'\n',msg) - elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown - pg.body = self:qstr('# error :: ',title,'\n\n',msg) - elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi - pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg) - end - var cthdr = lib.http.header { 'Content-Type', 'text/plain' } - if ctt == lib.http.mime.none then - pg.headers.ct = 0 - else - pg.headers = lib.typeof(pg.headers) { &cthdr, 1 } - switch ctt do - case [ctt.type](lib.http.mime.json) then - cthdr.value = 'application/json' - end - escape - for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2] - if key ~= 'json' then - emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end - end - end - end - end - end - pg:send(self.con) - end -end - -terra convo:fail(code: uint16) - switch code do - escape - local stderrors = { - {400, 'bad request', "the action you have attempted on this resource is not meaningful"}; - {401, 'unauthorized', "this resource is not available at your clearance level"}; - {403, 'forbidden', "we can neither confirm nor deny the existence of this resource"}; - {404, 'resource not found', "that resource is not extant on or known to this server"}; - {405, 'method not allowed', "the method you have attempted on this resource is not meaningful"}; - {406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"}; - {500, 'internal server error', "parsav did a fucksy wucksy"}; - } - - for i,v in ipairs(stderrors) do - emit quote case uint16([v[1]]) then - self:complain([v]) - end end - end - end - else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug') - end -end - -terra convo:confirm(title: pstring, msg: pstring, cancel: pstring) - var conf = data.view.confirm { - title = title; - query = msg; - cancel = cancel; - } - var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title) - var body = conf:poolstr(&self.srv.pool) -- defer body:free() - var cf = [convo.page] { - title = ti:finalize(); - class = 'query'; - body = body; cache = false; - } - self:stdpage(cf) - --cf.title:free() -end - -convo.methods.assertpow = macro(function(self, pow) - return quote - var ok = true - if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then - ok = false - self:complain(403,'insufficient privileges',['you lack the '..pow:asvalue()..' power and cannot perform this action']) - end - in ok end -end) - -local pstr2mg, mg2pstr -do -- aaaaaaaaaaaaaaaaaaaaaaaa - mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v) - if v.field == 'body' or v[1] == 'body' then return v.type end - end) - terra pstr2mg(p: pstring): mgstr - return mgstr { ptr = p.ptr, len = p.ct } - end - terra mg2pstr(m: mgstr): pstring - return pstring { ptr = m.ptr, ct = m.len } - end -end - --- CALL ONLY ONCE PER VAR -terra convo:postv_next(name: pstring, start: &pstring) - if self.varbuf.ptr == nil then - self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len) - self.vbofs = self.varbuf.ptr - end - var conv = pstr2mg(@start) - var o = lib.net.mg_http_get_var( - &conv, - name.ptr, self.vbofs, - self.varbuf.ct - (self.vbofs - self.varbuf.ptr) - ) - if o > 0 then - start:advance(name.ct + o + 2) - var r = self.vbofs - self.vbofs = self.vbofs + o + 1 - @(self.vbofs - 1) = 0 - var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) - return norm.ptr, norm.ct - else return nil, 0 end -end -terra convo:postv(name: pstring) - var start = mg2pstr(self.msg.body) - return self:postv_next(name, &start) -end -terra convo:ppostv(name: pstring) - var s,l = self:postv(name) - return pstring { ptr = s, ct = l } -end -do - local struct postiter { co: &convo where: pstring name: pstring } - terra convo:eachpostv(name: pstring) - return postiter { co = self, where = mg2pstr(self.msg.body), name = name } - end - postiter.metamethods.__for = function(self, body) - return quote - while true do - var str, len = self.co:postv_next(self.name, &self.where) - if str == nil then break end - [ body(`pstring {str, len}) ] - end - end - end -end - -terra convo:getv(name: rawstring) - if self.varbuf.ptr == nil then - self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len) - self.vbofs = self.varbuf.ptr - end - var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) - if o > 0 then - var r = self.vbofs - self.vbofs = self.vbofs + o + 1 - @(self.vbofs - 1) = 0 - var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) - return norm.ptr, norm.ct - else return nil, 0 end -end -terra convo:pgetv(name: rawstring) - var s,l = self:getv(name) - return pstring { ptr = s, ct = l } -end - local route = {} -- these are defined in route.t, as they need access to renderers terra route.dispatch_http :: {&convo, lib.mem.ptr(int8)} -> {} -local mimevar = symbol(lib.mem.ref(int8)) -local mimeneg = `lib.http.mime.none - -for i, t in ipairs(mimetypes) do - local name, mime = t[1], t[2] - mimeneg = quote - var ret: lib.http.mime.t - if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then - ret = [lib.http.mime[name]] - else ret = [mimeneg] end - in ret end -end - local handle = { http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque) var server = [&srv](userdata) var mgpeer = getpeer(con) -- var pbuf: int8[128] @@ -636,17 +214,18 @@ -- first, check for an accept header. if it's there, we need to -- iterate over the values and pick the highest-priority one do var acc = lib.http.findheader(msg, 'Accept') -- TODO handle q-value if acc ~= nil and acc.ptr ~= nil then - var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr } + var mimevar = [pstring] { ptr = acc.ptr } + lib.dbg('accept header is ', {acc.ptr,acc.ct}) var i = 0 while i < acc.ct do if acc.ptr[i] == @',' or acc.ptr[i] == @';' then mimevar.ct = (acc.ptr+i) - mimevar.ptr - var t = [mimeneg] - if t ~= lib.http.mime.none then - co.reqtype = t + var mk = lib.mime.lookup(mimevar) + if mk ~= nil and mk.output ~= lib.http.mime.none then + co.reqtype = mk.output goto foundtype end if acc.ptr[i] == @';' then -- fast-forward over q for j=i+1,acc.ct do i=j @@ -663,16 +242,16 @@ end i=i+1 end if co.reqtype == lib.http.mime.none then mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr) - co.reqtype = [mimeneg] - if co.reqtype == lib.http.mime.none then - co.reqtype = lib.http.mime.html + var mk = lib.mime.lookup(mimevar) + if mk ~= nil and mk.output ~= lib.http.mime.none then + co.reqtype = mk.output end end - else co.reqtype = lib.http.mime.html end + end ::foundtype::end -- 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 @@ -880,10 +459,12 @@ upmap:free() end end end + var mtt = lib.http.mime._str(co.reqtype) + lib.dbg('routing with negotiated type of ', {mtt.ptr,mtt.ct}) route.dispatch_http(&co, uri) ::fail:: if co.uploads.run > 0 then for i=0,co.uploads.sz do Index: store.t ================================================================== --- store.t +++ store.t @@ -215,10 +215,11 @@ else lib.bail('invalid mode on timeline range!') end end struct m.post { id: uint64 + uri: str author: uint64 subject: str body: str acl: str posted: m.timepoint @@ -491,10 +492,15 @@ circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64) circle_members_add_uid: {&m.source, uint64, uint64} -> {} circle_members_del_uid: {&m.source, uint64, uint64} -> {} circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle) + thread_top_find: {&m.source, uint64} -> uint64 + -- NOTE: this won't work if conversations are broken across multiple data sources! + -- if this is a thing that's likely to happen, the overlord-side wrapper for this + -- function (srv.t) should implement a more sophisticated algorithm over all the + -- data sources, instead of just stopping when one parent is found thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64 -- instantiate an artifact in the database, either installing a new -- artifact or returning the id of an existing artifact with the same hash Index: str.t ================================================================== --- str.t +++ str.t @@ -569,22 +569,35 @@ cur = cont end return acc:finalize() end -terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t +terra m.acc:qesc(str: m.t, wrap: bool) -- escape double-quotes - var a: m.acc a:pool(pool, str.ct + str.ct/2) - if wrap then a:lpush '"' end + if wrap then self:lpush '"' end for i=0, str.ct do - if str(i) == @'"' then a:lpush '\\"' - elseif str(i) == @'\\' then a:lpush '\\\\' + if str(i) == @'"' then self:lpush '\\"' + elseif str(i) == @'\\' then self:lpush '\\\\' + elseif str(i) == @'\n' then self:lpush '\\n' + elseif str(i) == @'\t' then self:lpush '\\t' elseif str(i) < 0x20 then -- for json var hex = lib.math.hexbyte(str(i)) - a:lpush('\\u00'):push(&hex[0], 2) - else a:push(str.ptr + i,1) end + self:lpush('\\u00'):push(&hex[0], 2) + else self:push(str.ptr + i,1) end end - if wrap then a:lpush '"' end + if wrap then self:lpush '"' end + return self +end + +terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t + -- convenience function + var a: m.acc a:pool(pool, 2 + str.ct + str.ct/2) + a:qesc(str,wrap) return a:finalize() end + +terra m.acc:qpush(str: m.t) + -- convenience adaptor + return self:qesc(str, false) +end return m Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -36,18 +36,23 @@ str = str:gsub(' ?', file) end) + local detritus = "" for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+):?()') do if string.sub(str,start-1,start-1) ~= '\\' then - segs[#segs+1] = string.sub(str,last,start-1) + local suffix = "" + if mode == '$' then suffix = '"' end + segs[#segs+1] = detritus .. string.sub(str,last,start-1) .. suffix + detritus = '' fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) } last = stop + if mode == '$' then detritus = '"' end end end - segs[#segs+1] = string.sub(str,last) + segs[#segs+1] = detritus .. string.sub(str,last) for i, s in ipairs(segs) do segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o) constlen = constlen + string.len(segs[i]) end @@ -116,11 +121,14 @@ if fields[idx] and fields[idx].mode then local f = fields[idx] local fp = `symself.[f.key] local sanexp local nulexp - if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true) + if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, false) + -- we use the detritus mechanism rather than the quote-wrap mechanism bc, apart + -- from being faster, 0-length strings cannot be sanitized into -- >0-length + -- strings due to how nullity is indicated (to wit, if fp == 0, ptr can be wild) elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false) elseif f.mode == '#' then sanexp = quote var ibuf: int8[21] var ptr = lib.math.decstr(fp, &ibuf[20])