-- 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,true) -- 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('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
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 <strong>'..pow:asvalue()..'</strong> 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