-- vim: ft=terra
local util = dofile 'common.lua'
local struct srv
local struct cfgcache {
secret: lib.mem.ptr(int8)
instance: lib.mem.ptr(int8)
overlord: &srv
}
local struct srv {
sources: lib.mem.ptr(lib.store.source)
webmgr: lib.net.mg_mgr
webcon: &lib.net.mg_connection
cfg: cfgcache
}
terra cfgcache:free() -- :/
self.secret:free()
self.instance:free()
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
if tk == primary then
return `self.sources.ptr[0]:[meth]([expr])
else local ok, empty
local r = symbol(rt)
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 then
r = src:[meth]([expr])
if [ok] then break
else r = empty end
end
end
in r end
end
end)
local struct convo {
srv: &srv
con: &lib.net.mg_connection
msg: &lib.net.mg_http_message
aid: uint64 -- 0 if logged out
who: &lib.store.actor -- who we're logged in as, if aid ~= 0
}
-- this is unfortunately necessary to work around a terra bug
-- it can't seem to handle forward-declarations of structs in C
local getpeer
do local struct strucheader {
next: &lib.net.mg_connection
mgr: &lib.net.mg_mgr
peer: lib.net.mg_addr
}
terra getpeer(con: &lib.net.mg_connection)
return [&strucheader](con).peer
end
end
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()
var body = data.view.docskel {
instance = self.srv.cfg.instance.ptr;
title = ti.buf;
body = msg;
class = 'error';
}
if body.body == nil then
body.body = "i'm sorry, dave. i can't let you do that"
end
body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
ptr = &hdrs[0], ct = [hdrs.type.N]
})
end
local urimatch = macro(function(uri, ptn)
return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
end)
local route = {} -- these are defined in route.t, as they need access to renderers
terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
local handle = {
http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
var server = [&srv](ext)
var mgpeer = getpeer(con)
var peer = lib.store.inet { port = mgpeer.port; }
if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
if peer.pv == 6 then
for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
else -- v4
@[&uint32](&peer.v4) = mgpeer.ip
end
-- the peer property is currently broken and there is precious
-- little i can do about this -- it always reports a peer v4 IP
-- of 0.0.0.0, altho the port seems to come through correctly.
-- for now i'm leaving it as is, but note that netmask restrictions
-- WILL NOT WORK until upstream gets its shit together. FIXME
switch event do
case lib.net.MG_EV_HTTP_MSG then
lib.dbg('routing HTTP request')
var msg = [&lib.net.mg_http_message](p)
var co = convo {
con = con, srv = server, msg = msg;
aid = 0, who = nil;
}
-- we need to check if there's any cookies sent with the request,
-- and if so, whether they contain any credentials. this will be
-- used to set the auth parameters in the http conversation
var cookies_p = lib.http.findheader(msg, 'Cookie')
if cookies_p ~= nil then
var cookies = cookies_p.ptr
var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
var i = 0 while i < cookies_p.ct and
cookies[i] ~= 0 and
cookies[i] ~= @'\r' and
cookies[i] ~= @'\n' do -- cover all the bases
if val.ptr == nil then
if cookies[i] == @'=' then
key.ct = (cookies + i) - key.ptr
val.ptr = cookies + i + 1
end
i = i + 1
else
if cookies[i] == @';' then
val.ct = (cookies + i) - val.ptr
if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then
goto foundcookie
end
i = i + 1
i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
key.ptr = cookies + i
val.ptr = nil
else i = i + 1 end
end
end
if val.ptr == nil then goto nocookie end
val.ct = (cookies + i) - val.ptr
if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then
goto nocookie
end
::foundcookie:: do
var aid = lib.session.cookie_interpret(server.cfg.secret,
[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
lib.osclock.time(nil))
if aid ~= 0 then co.aid = aid end
end ::nocookie::;
end
if co.aid ~= 0 then
var sess, usr = co.srv:actor_session_fetch(co.aid, peer)
if sess.ok == false then co.aid = 0 else co.who = usr.ptr end
end
var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)
var uri = uridec
if urideclen == -1 then
for i = 0,msg.uri.len do
if msg.uri.ptr[i] == @'+'
then uri.ptr[i] = @' '
else uri.ptr[i] = msg.uri.ptr[i]
end
end
uri.ct = msg.uri.len
else uri.ct = urideclen end
lib.dbg('routing URI ', {uri.ptr, uri.ct})
if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
route.dispatch_http(&co, uri, [lib.http.method.get])
else
co:complain(400,'unknown method','you have submitted an invalid http request')
end
if co.aid ~= 0 then lib.mem.heapf(co.who) end
end
end
end;
}
local terra cfg(s: &srv, befile: rawstring)
lib.report('configuring backends from ', befile)
var fr = lib.file.open(befile, [lib.file.mode.read])
if fr.ok == false then
lib.bail('could not open configuration file ', befile)
end
var f = fr.val
var c: lib.mem.vec(lib.store.source) c:init(8)
var text: lib.str.acc text:init(64)
do var buf: int8[64]
while true do
var ct = f:read(buf, [buf.type.N])
if ct == 0 then break end
text:push(buf, ct)
end
end
f:close()
var cur = text.buf
var segs: tuple(&int8, &int8)[3] = array(
{[&int8](0),[&int8](0)},
{[&int8](0),[&int8](0)},
{[&int8](0),[&int8](0)}
)
var segdup = [terra(s: {rawstring, rawstring})
var sz = s._1 - s._0
var str = s._0
return [lib.mem.ptr(int8)] {
ptr = lib.str.ndup(str, sz);
ct = sz;
}
end]
var fld = 0
while (cur - text.buf) < text.sz do
if segs[fld]._0 == nil then
if not (@cur == @' ' or @cur == @'\t' or @cur == @'\n') then
segs[fld] = {cur, nil}
end
else
if fld < 2 and @cur == @' ' or @cur == @'\t' then
segs[fld]._1 = cur
fld = fld + 1
segs[fld] = {nil, nil}
elseif @cur == @'\n' or cur == text.buf + (text.sz-1) then
if fld < 2 then lib.bail('incomplete backend line in ', befile) else
segs[fld]._1 = cur
var src = c:new()
src.id = segdup(segs[0])
src.string = segdup(segs[2])
src.backend = nil
for i = 0,[lib.store.backends.type.N] do
if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then
src.backend = &lib.store.backends[i]
break
end
end
if src.backend == nil then
lib.bail('unknown backend in ', befile)
end
src.handle = nil
fld = 0
segs[0] = {nil, nil}
end
end
end
cur = cur + 1
end
text:free()
if c.sz > 0 then
s.sources = c:crush()
else
s.sources.ptr = nil
s.sources.ct = 0
end
end
terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
var cs: lib.store.credset cs:clear()
for i=0,self.sources.ct do
var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn)
cs = cs + set
end
return cs
end
terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
self.overlord = o
self:load()
end
srv.methods.start = terra(self: &srv, befile: rawstring)
cfg(self, befile)
var success = false
if self.sources.ct == 0 then lib.bail('no data sources specified') end
for i=0,self.sources.ct do var src = self.sources.ptr + i
lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
src.handle = src.backend.open(src)
if src.handle ~= nil then success = true end
end
if not success then
lib.bail('could not connect to any data sources!')
end
self.cfg:init(self)
var dbbind = self:conf_get('bind')
var envbind = lib.proc.getenv('parsav_bind')
var bind: rawstring
if envbind ~= nil then
bind = envbind
elseif dbbind.ptr ~= nil then
bind = dbbind.ptr
else bind = '[::]:10917' end
lib.report('binding to ', bind)
lib.net.mg_mgr_init(&self.webmgr)
self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)
var buf: int8[lib.session.maxlen]
var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0])
buf[len] = 0
var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil))
lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid)
if dbbind.ptr ~= nil then dbbind:free() end
end
srv.methods.poll = terra(self: &srv)
lib.net.mg_mgr_poll(&self.webmgr,1000)
end
srv.methods.shutdown = terra(self: &srv)
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:load()
self.instance = self.overlord:conf_get('instance-name')
self.secret = self.overlord:conf_get('server-secret')
end
return {
overlord = srv;
convo = convo;
route = route;
}