-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
local struct srv
local struct cfgcache {
secret: pstring
pol_sec: secmode.t
pol_reg: bool
credmgd: bool
maxupsz: intptr
instance: pstring
overlord: &srv
ui_cue_staff: pstring
ui_cue_founder: pstring
ui_hue: uint16
nranks: uint16
maxinvites: uint16
master: uint64
}
local struct srv {
sources: lib.mem.ptr(lib.store.source)
webmgr: lib.net.mg_mgr
webcon: &lib.net.mg_connection
cfg: cfgcache
id: rawstring
}
terra cfgcache:free() -- :/
self.secret:free()
self.instance:free()
self.ui_cue_staff:free()
self.ui_cue_founder:free()
end
terra srv:post_enum_author_uid(uid: uint64, r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
for i=0,self.sources.ct do var src = self.sources.ptr + i
if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then
var lst = src:post_enum_author_uid(uid,r)
all:assure(all.sz + lst.ct)
for j=0, lst.ct do all:push(lst.ptr[j]) end
lst:free()
end
end
return all
end
terra srv:timeline_instance_fetch(r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
for i=0,self.sources.ct do var src = self.sources.ptr + i
if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then
var lst = src:timeline_instance_fetch(r)
all:assure(all.sz + lst.ct)
for j=0, lst.ct do all:push(lst.ptr[j]) end
lst:free()
end
end
return all
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
local fn = f.field or f[1]
local ft = f.type or f[2]
if fn == meth then
rt = ft.type.returntype
if rt == bool then tk = simple
elseif rt.type == 'integer' then tk = oid
elseif rt.stat_basetype then tk = stat
elseif rt.ptr_basetype then tk = ptr end
break
end
end
local r = symbol(rt)
local succ = label()
if tk == primary then
return quote
var [r]
for i=0,self.sources.ct do var src = self.sources.ptr + i
if src.handle ~= nil and src.backend.[meth] ~= nil then
r = src:[meth]([expr])
goto [succ]
end
end
lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
::[succ]::;
in r end
else local ok, empty
if tk == ptr then
ok = `r.ptr ~= nil
empty = `[rt]{ptr=nil,ct=0}
elseif tk == stat then
ok = `r.ok == true
empty = `[rt]{ok=false,error=1}
elseif tk == simple then
ok = `r == true
empty = `false
elseif tk == oid then
ok = `r ~= 0
empty = `0
end
return quote
var [r] = empty
for i=0,self.sources.ct do var src = self.sources.ptr + i
if src.handle ~= nil and src.backend.[meth] ~= nil then
r = src:[meth]([expr])
if [ok] then break
else r = empty end
end
end
in r end
end
end)
terra lib.store.post:publish(s: &srv)
self:comp()
self.posted = lib.osclock.time(nil)
self.discovered = self.posted
self.chgcount = 0
self.edited = 0
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
}
-- 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: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(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(lib.str.plit'text/html') or
mime:cmp(lib.str.plit'text/xml') or
mime:cmp(lib.str.plit'application/xhtml+xml') or
mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
then -- danger will robinson
mime = lib.str.plit'text/plain'
elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
mime = lib.str.plit'application/octet-stream'
end
lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2)
lib.net.mg_send(self.con, data.ptr, data.ct)
lib.net.mg_send(self.con, '\r\n', 2)
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:complain(code: uint16, title: rawstring, msg: rawstring)
if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
var ti: lib.str.acc ti:compose('error :: ', title)
var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>')
var body = [convo.page] {
title = ti:finalize();
body = bo:finalize();
class = lib.str.plit 'error';
cache = false;
}
self:statpage(code, body)
body.title:free()
body.body:free()
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:compose('confirm :: ', title)
var body = conf:tostr() defer body:free()
var cf = [convo.page] {
title = ti:finalize();
class = lib.str.plit '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)
-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
if self.varbuf.ptr == nil then
self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
self.vbofs = self.varbuf.ptr
end
var o = lib.net.mg_http_get_var(&self.msg.body, 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:ppostv(name: rawstring)
var s,l = self:postv(name)
return pstring { ptr = s, ct = l }
end
terra convo:getv(name: rawstring)
if self.varbuf.ptr == nil then
self.varbuf = lib.mem.heapa(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), lib.http.method.t} -> {}
local mimetypes = {
{'html', 'text/html'};
{'json', 'application/json'};
{'mkdown', 'text/markdown'};
{'text', 'text/plain'};
{'ansi', 'text/x-ansi'};
}
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 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
-- needs to check for an X-Forwarded-For header from nginx and
-- use that instead of the peer iff peer is ::1/127.1 FIXME
-- maybe also haproxy support?
switch event_kind do
case lib.net.MG_EV_HTTP_MSG then
lib.dbg('routing HTTP request')
var msg = [&lib.net.mg_http_message](event)
var co = convo {
con = con, srv = server, msg = msg;
aid = 0, aid_issue = 0, who = nil;
reqtype = lib.http.mime.none;
peer = peer, live_last = 0;
} co.varbuf.ptr = nil
co.navbar.ptr = nil
co.actorcache.top = 0
co.actorcache.cur = 0
co.ui_hue = server.cfg.ui_hue
co.body.ptr = msg.body.ptr co.body.ct = msg.body.len
-- 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 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
goto foundtype
end
if acc.ptr[i] == @';' then -- fast-forward over q
for j=i+1,acc.ct do i=j
if acc.ptr[j] == @',' then break end
end
end
while i < acc.ct and -- fast-forward over ws
acc.ptr[i+1] == @' ' or
acc.ptr[i+1] == @'\t'
do i=i+1 end
mimevar.ptr = acc.ptr + i + 1
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
end
end
else co.reqtype = lib.http.mime.html 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
var cookies_p = lib.http.findheader(msg, 'Cookie')
if cookies_p ~= nil and cookies_p.ptr ~= 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, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename],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, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
goto nocookie
end
::foundcookie:: do
var aid, tp = 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 co.aid_issue = tp end
end ::nocookie::;
end
if co.aid ~= 0 then
var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue)
if sess.ok == false then co.aid = 0 co.aid_issue = 0 else
co.who = usr.ptr
co.who.rights.powers = server:actor_powers_fetch(co.who.id)
var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent')
if hueok then co.ui_hue = userhue end
end
end
var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival')
if livelast_p ~= nil and livelast_p.ptr ~= nil then
var ll, ok = lib.math.decparse(pstring{ptr = livelast_p.ptr, ct = livelast_p.ct - 1})
if ok then co.live_last = ll 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
co.method = [lib.http.method.get]
elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.post]
elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.head]
elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.options]
else
co:complain(400,'unknown method','you have submitted an invalid http request')
goto fail
end
-- check for a content-type header, and see if it's a multipart/
-- form-data encoded POST request so we can handle file uploads
co.uploads.sz = 0 co.uploads.run = 0
if co.method == [lib.http.method.post] then
var ctt = lib.http.findheader(msg, 'Content-Type')
if ctt ~= nil then
if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then
var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20)
if lib.str.ncmp(p,'boundary=',9) ~= 0 then
co:complain(400,'bad request','unrecognized content-type')
goto fail
end
var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)}
co.method = lib.http.method.post_file
co.uploads:init(8)
var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize()
var upmap = lib.str.splitmap(co.body,bsr,8)
-- first entry may not be preceded by header-break
if lib.str.find(upmap(0), pstring {
ptr = bsr.ptr + 2, ct = bsr.ct - 2
}):ref() then
upmap(0).ptr = upmap(0).ptr + (bsr.ct - 2)
upmap(0).ct = upmap(0).ct - (bsr.ct - 2)
end
-- last entry is weird
do var lsr = (lib.str.acc{}):compose('\r\n--',boundary,'--\r\n'):finalize()
var lsent = upmap.ptr + (upmap.ct - 1)
var halt = lib.str.find(@lsent, lsr)
if halt:ref() then
lsent.ct = halt.ptr - lsent.ptr
end
lsr:free() end
for i=0,upmap.ct do
var hdrbrk = lib.str.find(upmap(i), lib.str.plit'\r\n\r\n')
if hdrbrk:ref() then
var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct}
var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6)
var ctt = pstring.null()
var ctd = pstring.null()
for j=0, hdrs.ct do
var brk = lib.str.find(hdrs(j),lib.str.plit':')
if brk:ref() then
var hdr = pstring{hdrs(j).ptr,hdrs(j).ct - brk.ct}
var val = pstring{brk.ptr+1, brk.ct-1}:ffw()
if hdr:cmp(lib.str.plit'Content-Type') then
ctt = val
elseif hdr:cmp(lib.str.plit'Content-Disposition') then
ctd = val
end
end
end
if ctd:ref() then
var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free()
if ctdvals(0):cmp(lib.str.plit'form-data') and ctdvals.ct > 1 then
var fld = pstring.null()
var file = pstring.null()
for j=1, ctdvals.ct do var v = ctdvals(j):ffw()
var x = lib.str.find(v,lib.str.plit'=')
if x:ref() then
var key = pstring{v.ptr, v.ct - x.ct}
var val = pstring{x.ptr + 1, x.ct - 1}
var decval, ofs, sp = lib.str.toknext(val,@';',true)
if key:cmp(lib.str.plit'name') then
fld = decval
elseif key:cmp(lib.str.plit'filename') then
file = decval
else decval:free() end
end
end
if fld:ref() then
var nextup = co.uploads:new()
if ctt:ref() then
nextup.ctype = ctt
else
nextup.ctype = pstring.null()
end
nextup.body = pstring {
ptr = hdrbrk.ptr + 4;
ct = hdrbrk.ct - 4;
}
nextup.ctype = ctt
nextup.field = fld
nextup.filename = file
end
end
end
end
end
bsr:free()
upmap:free()
end
end
end
route.dispatch_http(&co, uri, co.method)
if co.uploads.run > 0 then
for i=0,co.uploads.sz do
co.uploads(i).filename:free()
co.uploads(i).field:free()
end
co.uploads:free()
end
::fail::
if co.aid ~= 0 then lib.mem.heapf(co.who) end
if co.varbuf.ptr ~= nil then co.varbuf:free() end
if co.navbar.ptr ~= nil then co.navbar:free() end
co.actorcache:free()
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(256)
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_stats(uid: uint64)
var stats = lib.store.actor_stats {
posts = 0, mutuals = 0;
follows = 0, followers = 0;
}
for i=0,self.sources.ct do
var s = self.sources.ptr[i]:actor_stats(uid)
stats.posts = stats.posts + s.posts
stats.mutuals = stats.mutuals + s.mutuals
stats.followers = stats.followers + s.followers
stats.follows = stats.follows + s.follows
end
return stats
end
terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
var cs: lib.store.credset cs:clear()
var ok = false
for i=0,self.sources.ct do
var set, iok = self.sources(i):actor_auth_how(ip, usn)
if iok then
cs = cs + set
ok = iok
end
end
return cs, ok
end
terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
for i=0,self.sources.ct do
if self.sources(i).backend ~= nil and
self.sources(i).backend.actor_auth_pw ~= nil then
var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
if aid ~= 0 then
if uid == 0 then
lib.dbg('new user just logged in, creating account entry')
var kbuf: uint8[lib.crypt.const.maxdersz]
var na = lib.store.actor.mk(&kbuf[0])
na.handle = newhnd.ptr
var newuid: uint64
if self.sources(i).backend.actor_create ~= nil then
newuid = self.sources(i):actor_create(&na)
else newuid = self:actor_create(&na) end
if self.sources(i).backend.actor_auth_register_uid ~= nil then
self.sources(i):actor_auth_register_uid(aid,newuid)
end
end
return aid
end
end
end
return 0
end
terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
self.overlord = o
self:load()
end
terra srv:setup(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
end
terra srv:start(iname: rawstring)
self:conprep(lib.store.prepmode.full)
self.cfg:init(self)
var dbbind = self:conf_get('bind')
if iname == nil then iname = lib.proc.getenv('parsav_instance') end
if iname == nil then
self.id = self.cfg.instance.ptr;
-- let this leak -- it'll be needed for the lifetime of the process anyway
else self.id = iname end
if iname ~= nil then
lib.report('parsav instance "',iname,'" starting')
end
var envbind = lib.proc.getenv('parsav_bind')
var bind: rawstring
if envbind ~= nil then
bind = envbind
elseif dbbind.ptr ~= nil then
bind = dbbind.ptr
else bind = '[::1]: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, self)
if dbbind.ptr ~= nil then dbbind:free() end
end
terra srv:poll()
lib.net.mg_mgr_poll(&self.webmgr,300)
end
terra srv:shutdown()
lib.net.mg_mgr_free(&self.webmgr)
for i=0,self.sources.ct do var src = self.sources.ptr + i
lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
src:close()
end
self.sources:free()
end
terra cfgcache:cfint(name: rawstring, default: intptr)
var str = self.overlord:conf_get(name)
if str.ptr ~= nil then
var i,ok = lib.math.decparse(str)
if ok then default = i else
lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead')
end
str:free()
end
return default
end
terra cfgcache:cfbool(name: rawstring, default: bool)
var str = self.overlord:conf_get(name)
if str.ptr ~= nil then
if str:cmp(lib.str.plit 'true') or str:cmp(lib.str.plit 'on') or
str:cmp(lib.str.plit 'yes') or str:cmp(lib.str.plit '1') then
default = true
elseif str:cmp(lib.str.plit 'false') or str:cmp(lib.str.plit 'off') or
str:cmp(lib.str.plit 'no') or str:cmp(lib.str.plit '0') then
default = false
else
lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead')
end
str:free()
end
return default
end
terra cfgcache:load()
self.instance = self.overlord:conf_get('instance-name')
self.secret = self.overlord:conf_get('server-secret')
self.pol_reg = self:cfbool('policy-self-register', false)
do self.credmgd = false
var sreg = self.overlord:conf_get('credential-store')
if sreg:ref() then
if lib.str.cmp(sreg.ptr, 'managed') == 0
then self.credmgd = true
else self.credmgd = false
end
sreg:free()
end end
do self.maxupsz = [1024 * 100] -- 100 kilobyte default
var sreg = self.overlord:conf_get('maximum-artifact-size')
if sreg:ref() then
var sz, ok = lib.math.fsz_parse(sreg)
if ok then self.maxupsz = sz else
lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit')
end
sreg:free() end
end
self.pol_sec = secmode.lockdown
var smode = self.overlord:conf_get('policy-security')
if smode.ptr ~= nil then
if lib.str.cmp(smode.ptr, 'public') == 0 then
self.pol_sec = secmode.public
elseif lib.str.cmp(smode.ptr, 'private') == 0 then
self.pol_sec = secmode.private
elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
self.pol_sec = secmode.lockdown
elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
self.pol_sec = secmode.isolate
end
smode:free()
end
self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
self.nranks = self:cfint('user-ranks',10)
self.maxinvites = self:cfint('max-invites',64)
var webmaster = self.overlord:conf_get('master')
if webmaster:ref() then defer webmaster:free()
var wma = self.overlord:actor_fetch_xid(webmaster)
if not wma then
lib.warn('the webmaster specified in the configuration store does not seem to exist or is not known to this instance; preceding as if no master defined. if the master is a remote user, you can rectify this with the `actor ',{webmaster.ptr,webmaster.ct},' instantiate` and `conf refresh` commands')
else
self.master = wma(0).id
wma:free()
end
end
self.ui_cue_staff = self.overlord:conf_get('ui-profile-cue-staff')
self.ui_cue_founder = self.overlord:conf_get('ui-profile-cue-master')
end
return {
overlord = srv;
convo = convo;
route = route;
secmode = secmode;
}