-- vim: ft=terra
local queries = {
conf_get = {
params = {rawstring}, sql = [[
select value from parsav_config
where key = $1::text limit 1
]];
};
conf_set = {
params = {rawstring,rawstring}, sql = [[
insert into parsav_config (key, value)
values ($1::text, $2::text)
on conflict (key) do update set value = $2::text
]];
};
conf_reset = {
params = {rawstring}, sql = [[
delete from parsav_config where
key = $1::text
]];
};
actor_fetch_uid = {
params = {uint64}, sql = [[
select
id, nym, handle, origin,
bio, rank, quota, key
from parsav_actors
where id = $1::bigint
]];
};
actor_fetch_xid = {
params = {rawstring}, sql = [[
select a.id, a.nym, a.handle, a.origin,
a.bio, a.rank, a.quota, a.key, $1::text,
coalesce(s.domain,
(select value from parsav_config
where key='domain' limit 1)) as domain
from parsav_actors as a
left join parsav_servers as s
on a.origin = s.id
where $1::text = (a.handle || '@' || domain) or
$1::text = ('@' || a.handle || '@' || domain) or
(a.origin is null and $1::text = ('@' || a.handle))
]];
};
actor_enum_local = {
params = {}, sql = [[
select id, nym, handle, origin,
bio, rank, quota, key,
handle ||'@'||
(select value from parsav_config
where key='domain' limit 1) as xid
from parsav_actors where origin is null
]];
};
actor_enum = {
params = {}, sql = [[
select a.id, a.nym, a.handle, a.origin,
a.bio, a.rank, a.quota, a.key,
a.handle ||'@'||
coalesce(s.domain,
(select value from parsav_config
where key='domain' limit 1)) as xid
from parsav_actors a
left join parsav_servers s on s.id = a.origin
]];
};
}
local struct pqr {
sz: intptr
res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
terra pqr:null(row: intptr, col: intptr)
return (lib.pq.PQgetisnull(self.res, row, col) == 1)
end
terra pqr:len(row: intptr, col: intptr)
return lib.pq.PQgetlength(self.res, row, col)
end
terra pqr:cols() return lib.pq.PQnfields(self.res) end
terra pqr:string(row: intptr, col: intptr) -- not to be exported!!
var v = lib.pq.PQgetvalue(self.res, row, col)
-- var r: lib.mem.ptr(int8)
-- r.ct = lib.str.sz(v)
-- r.ptr = v
return v
end
terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE
return [lib.mem.ptr(uint8)] {
ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col));
ct = lib.pq.PQgetlength(self.res, row, col);
}
end
terra pqr:String(row: intptr, col: intptr) -- suitable to be exported
var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) }
s.ct = lib.pq.PQgetlength(self.res, row, col)
return s
end
terra pqr:bool(row: intptr, col: intptr)
var v = lib.pq.PQgetvalue(self.res, row, col)
if @v == 0x01 then return true else return false end
end
terra pqr:cidr(row: intptr, col: intptr)
var v = lib.pq.PQgetvalue(self.res, row, col)
var i: lib.store.inet
if v[0] == 0x02 then i.pv = 4
elseif v[0] == 0x03 then i.pv = 6
else lib.bail('invalid CIDR type in stream') end
i.fixbits = v[1]
if v[2] ~= 0x1 then lib.bail('expected CIDR but got inet from stream') end
if i.pv == 4 and v[3] ~= 0x04 or i.pv == 6 and v[3] ~= 0x10 then
lib.bail('CIDR failed length sanity check') end
var sz: intptr if i.pv == 4 then sz = 4 else sz = 16 end
for j=0,sz do i.v6[j] = v[4 + j] end -- 😬
return i
end
pqr.methods.int = macro(function(self, ty, row, col)
return quote
var i: ty:astype()
var v = lib.pq.PQgetvalue(self.res, row, col)
lib.math.netswap_ip(ty, v, &i)
in i end
end)
local pqt = {
[lib.store.inet] = function(cidr)
local tycode = cidr and 0x01 or 0x00
return terra(i: lib.store.inet, buf: &uint8)
var sz: intptr
if i.pv == 4 then sz = 4 else sz = 16 end
if buf == nil then buf = [&uint8](lib.mem.heapa_raw(sz + 4)) end
if i.pv == 4 then buf[0] = 0x02
elseif i.pv == 6 then buf[0] = 0x03 end
if cidr then -- our local 'inet' is not quite orthogonal to the
-- postgres inet type; tweak it to match (ignore port)
buf[1] = i.fixbits
elseif i.pv == 6 then buf[1] = 128
else buf[1] = 32 end
buf[2] = tycode
buf[3] = sz
for j=0,sz do buf[4 + j] = i.v6[j] end -- 😬
return buf
end
end;
}
local con = symbol(&lib.pq.PGconn)
local prep = {}
local sqlsquash = function(s) return s:gsub('%s+',' '):gsub('^%s*(.-)%s*$','%1') end
for k,q in pairs(queries) do
local qt = sqlsquash(q.sql)
local stmt = 'parsavpg_' .. k
prep[#prep + 1] = quote
var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
defer lib.pq.PQclear(res)
if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then
if res == nil then
lib.bail('grievous error occurred preparing ',k,' statement')
end
lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
end
lib.dbg('prepared PGSQL statement ',k)
end
local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
for i, ty in ipairs(q.params) do
args[i] = symbol(ty)
ft[i] = `1
if ty == rawstring then
counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
casts[i] = `[&int8]([args[i]])
elseif ty:isintegral() then
counters[i] = ty.bytes
casts[i] = `[&int8](&[args[i]])
fixers[#fixers + 1] = quote
--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
[args[i]] = lib.math.netswap(ty, [args[i]])
end
end
end
terra q.exec(src: &lib.store.source, [args])
var params = arrayof([&int8], [casts])
var params_sz = arrayof(int, [counters])
var params_ft = arrayof(int, [ft])
[fixers]
var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
[#args], params, params_sz, params_ft, 1)
if res == nil then
lib.bail(['grievous error occurred executing '..k..' against database'])
elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
lib.bail(['PGSQL database procedure '..k..' failed\n'],
lib.pq.PQresultErrorMessage(res))
end
var ct = lib.pq.PQntuples(res)
if ct == 0 then
lib.pq.PQclear(res)
return pqr {0, nil}
else
return pqr {ct, res}
end
end
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
var a: lib.mem.ptr(lib.store.actor)
if r:cols() >= 8 then
a = [ lib.str.encapsulate(lib.store.actor, {
nym = {`r:string(row, 1); `r:len(row,1) + 1};
handle = {`r:string(row, 2); `r:len(row,2) + 1};
bio = {`r:string(row, 4); `r:len(row,4) + 1};
xid = {`r:string(row, 8); `r:len(row,8) + 1};
}) ]
else
a = [ lib.str.encapsulate(lib.store.actor, {
nym = {`r:string(row, 1); `r:len(row,1) + 1};
handle = {`r:string(row, 2); `r:len(row,2) + 1};
bio = {`r:string(row, 4); `r:len(row,4) + 1};
}) ]
a.ptr.xid = nil
end
a.ptr.id = r:int(uint64, row, 0);
a.ptr.rights = lib.store.rights_default();
a.ptr.rights.rank = r:int(uint16, row, 5);
a.ptr.rights.quota = r:int(uint32, row, 6);
if r:null(row,7) then
a.ptr.key.ct = 0 a.ptr.key.ptr = nil
else
a.ptr.key = r:bin(row,7)
end
if r:null(row,3) then a.ptr.origin = 0
else a.ptr.origin = r:int(uint64,row,3) end
return a
end
local checksha = function(hnd, query, hash, origin, username, pw)
local inet_buf = symbol(uint8[4 + 16])
local validate = function(kind, cred, credlen)
return quote
var osz: intptr if origin.pv == 4 then osz = 4 else osz = 16 end
var formats = arrayof([int], 1,1,1,1)
var params = arrayof([&int8], username, kind,
[&int8](&cred), [&int8](&inet_buf))
var lens = arrayof(int, lib.str.sz(username), [#kind], credlen, osz + 4)
var res = lib.pq.PQexecParams([&lib.pq.PGconn](hnd), query, 4, nil,
params, lens, formats, 1)
if res == nil then
lib.bail('grievous failure checking pwhash')
elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
lib.warn('pwhash query failed: ', lib.pq.PQresultErrorMessage(res), '\n', query)
else
var r = pqr {
sz = lib.pq.PQntuples(res);
res = res;
}
if r.sz > 0 then -- found a record! stop here
var aid = r:int(uint64, 0,0)
r:free()
return aid
end
end
end
end
local out = symbol(uint8[64])
local vdrs = {}
local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)]
vdrs[#vdrs+1] = quote
if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg),
[&uint8](pw), lib.str.sz(pw), out) ~= 0 then
lib.bail('hashing failure!')
end
[ validate(string.format('pw-sha%u', hash), out, hash / 8) ]
end
return quote
lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
var [inet_buf]
[pqt[lib.store.inet](false)](origin, inet_buf)
var [out]
[vdrs]
lib.dbg(['could not find password hash'])
end
end
local b = `lib.store.backend {
id = "pgsql";
open = [terra(src: &lib.store.source): &opaque
lib.report('connecting to postgres database: ', src.string.ptr)
var [con] = lib.pq.PQconnectdb(src.string.ptr)
if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
lib.warn('postgres backend connection failed')
lib.pq.PQfinish(con)
return nil
end
var res = lib.pq.PQexec(con, [[
select pg_catalog.set_config('search_path', 'public', false)
]])
if res == nil then
lib.warn('critical failure to secure postgres connection')
lib.pq.PQfinish(con)
return nil
end
defer lib.pq.PQclear(res)
if lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
lib.warn('failed to secure postgres connection')
lib.pq.PQfinish(con)
return nil
end
[prep]
return con
end];
close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];
conf_get = [terra(src: &lib.store.source, key: rawstring)
var r = queries.conf_get.exec(src, key)
if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
defer r:free()
return r:String(0,0)
end
end];
conf_set = [terra(src: &lib.store.source, key: rawstring, val: rawstring)
queries.conf_set.exec(src, key, val):free() end];
conf_reset = [terra(src: &lib.store.source, key: rawstring)
queries.conf_reset.exec(src, key):free() end];
actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64)
var r = queries.actor_fetch_uid.exec(src, uid)
if r.sz == 0 then
return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var a = row_to_actor(&r, 0)
a.ptr.source = src
return a
end
end];
actor_enum = [terra(src: &lib.store.source)
var r = queries.actor_enum.exec(src)
if r.sz == 0 then
return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var mem = lib.mem.heapa([&lib.store.actor], r.sz)
for i=0,r.sz do mem.ptr[i] = row_to_actor(&r, i).ptr end
return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
end
end];
actor_enum_local = [terra(src: &lib.store.source)
var r = queries.actor_enum_local.exec(src)
if r.sz == 0 then
return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var mem = lib.mem.heapa([&lib.store.actor], r.sz)
for i=0,r.sz do mem.ptr[i] = row_to_actor(&r, i).ptr end
return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
end
end];
actor_auth_how = [terra(
src: &lib.store.source,
ip: lib.store.inet,
username: rawstring
)
var authview = src:conf_get('auth-source') defer authview:free()
var a: lib.str.acc defer a:free()
a:compose('with mts as (select a.kind from ',authview,[' ' .. sqlsquash [[as a
left join parsav_actors as u on u.id = a.uid
where (a.uid is null or u.handle = $1::text or (
a.uid = 0 and a.name = $1::text
)) and
(a.netmask is null or a.netmask >> $2::inet) and
blacklist = false)
select
(select count(*) from mts where kind like 'pw-%') > 0,
(select count(*) from mts where kind like 'otp-%') > 0,
(select count(*) from mts where kind like 'challenge-%') > 0,
(select count(*) from mts where kind = 'trust') > 0 ]]]) -- cheat
var cs: lib.store.credset cs:clear();
var ipbuf: int8[20]
;[pqt[lib.store.inet](false)](ip, [&uint8](&ipbuf))
var ipbl: intptr if ip.pv == 4 then ipbl = 8 else ipbl = 20 end
var params = arrayof(rawstring, username, [&int8](&ipbuf))
var params_sz = arrayof(int, lib.str.sz(username), ipbl)
var params_ft = arrayof(int, 1, 1)
var res = lib.pq.PQexecParams([&lib.pq.PGconn](src.handle), a.buf, 2, nil,
params, params_sz, params_ft, 1)
if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
if res == nil then
lib.bail('grievous error occurred checking for auth methods')
end
lib.bail('could not get auth methods for user ',username,':\n',lib.pq.PQresultErrorMessage(res))
end
var r = pqr { res = res, sz = lib.pq.PQntuples(res) }
if r.sz == 0 then return cs end -- just in case
(cs.pw << r:bool(0,0))
(cs.otp << r:bool(0,1))
(cs.challenge << r:bool(0,2))
(cs.trust << r:bool(0,3))
lib.pq.PQclear(res)
return cs
end];
actor_auth_pw = [terra(
src: &lib.store.source,
ip: lib.store.inet,
username: rawstring,
cred: rawstring
)
var authview = src:conf_get('auth-source') defer authview:free()
var a: lib.str.acc defer a:free()
a:compose('select a.aid from ',authview,[' ' .. sqlsquash [[as a
left join parsav_actors as u on u.id = a.uid
where (a.uid is null or u.handle = $1::text or (
a.uid = 0 and a.name = $1::text
)) and
(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
(a.netmask is null or a.netmask >> $4::inet)
order by blacklist desc limit 1]]])
[ checksha(`src.handle, `a.buf, 256, ip, username, cred) ] -- most common
[ checksha(`src.handle, `a.buf, 512, ip, username, cred) ] -- most secure
[ checksha(`src.handle, `a.buf, 384, ip, username, cred) ] -- weird
[ checksha(`src.handle, `a.buf, 224, ip, username, cred) ] -- weirdest
-- TODO: check pbkdf2-hmac
-- TODO: check OTP
return 0
end];
}
return b