-- vim: ft=terra
local pstring = lib.mem.ptr(int8)
local binblob = lib.mem.ptr(uint8)
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 a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid
from parsav_actors as a
left join parsav_servers as s
on a.origin = s.id
where a.id = $1::bigint
]];
};
actor_fetch_xid = {
params = {pstring}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
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 or
$1::text = ('@' || a.handle))
]];
};
actor_auth_pw = {
params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
select a.aid from parsav_auth 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
]];
};
actor_enum_local = {
params = {}, sql = [[
select id, nym, handle, origin, bio,
null::text, rank, quota, key,
extract(epoch from knownsince)::bigint,
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.avataruri, a.rank, a.quota, a.key,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid
from parsav_actors a
left join parsav_servers s on s.id = a.origin
]];
};
actor_stats = {
params = {uint64}, sql = ([[
with tweets as (
select from parsav_posts where author = $1::bigint
),
follows as (
select relatee as user from parsav_rels
where relator = $1::bigint and kind = <follow>
),
followers as (
select relator as user from parsav_rels
where relatee = $1::bigint and kind = <follow>
),
mutuals as (select * from follows intersect select * from followers)
select count(tweets.*)::bigint,
count(follows.*)::bigint,
count(followers.*)::bigint,
count(mutuals.*)::bigint
from tweets, follows, followers, mutuals
]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
};
actor_auth_how = {
params = {rawstring, lib.store.inet}, sql = [[
with mts as (select a.kind from parsav_auth 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
};
actor_session_fetch = {
params = {uint64, lib.store.inet}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
au.restrict,
array['post' ] <@ au.restrict as can_post,
array['edit' ] <@ au.restrict as can_edit,
array['acct' ] <@ au.restrict as can_acct,
array['upload'] <@ au.restrict as can_upload,
array['censor'] <@ au.restrict as can_censor,
array['admin' ] <@ au.restrict as can_admin
from parsav_auth au
left join parsav_actors a on au.uid = a.id
left join parsav_servers s on a.origin = s.id
where au.aid = $1::bigint and au.blacklist = false and
(au.netmask is null or au.netmask >> $2::inet)
]];
};
post_create = {
params = {uint64, rawstring, rawstring, rawstring}, sql = [[
insert into parsav_posts (
author, subject, acl, body,
posted, discovered,
circles, mentions
) values (
$1::bigint, case when $2::text = '' then null else $2::text end,
$3::text, $4::text,
now(), now(), array[]::bigint[], array[]::bigint[]
) returning id
]]; -- TODO array handling
};
instance_timeline_fetch = {
params = {uint64, uint64, uint64, uint64}, sql = [[
select true,
p.id, p.author, p.subject, p.acl, p.body,
extract(epoch from p.posted )::bigint,
extract(epoch from p.discovered)::bigint,
p.parent, null::text
from parsav_posts as p
inner join parsav_actors as a on p.author = a.id
where
($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
(a.origin is null)
order by (p.posted, p.discovered) desc
limit case when $3::bigint = 0 then null
else $3::bigint end
offset $4::bigint
]]
};
}
--($5::bool = false or p.parent is null) and
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!!
if self:null(row,col) then return nil end
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
if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end
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)
--i = @[&uint64](v)
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 = {}, {}, {}, {}, {}, {}
local dumpers = {}
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]])
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got rawstr %s\n'], [args[i]])
elseif ty == lib.store.inet then -- assume not CIDR
counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
casts[i] = quote
var ipbuf: int8[20]
;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
in &ipbuf[0] end
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n'])
elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
counters[i] = `[args[i]].ct
casts[i] = `[&int8]([args[i]].ptr)
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr)
elseif ty.ptr_basetype == bool then
counters[i] = `1
casts[i] = `[&int8]([args[i]].ptr)
-- dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got bool = %hhu\n'], @[args[i]].ptr)
elseif ty:isintegral() then
counters[i] = ty.bytes
casts[i] = `[&int8](&[args[i]])
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
fixers[#fixers + 1] = quote
[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]
--[dumpers]
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_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
--lib.io.fmt("body ptr %p len %llu\n", r:string(row,5), r:len(row,5))
--lib.io.fmt("acl ptr %p len %llu\n", r:string(row,4), r:len(row,4))
var subj: rawstring, sblen: intptr
if r:null(row,3)
then subj = nil sblen = 0
else subj = r:string(row,3) sblen = r:len(row,3)+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 = { `nil, `0 }; --FIXME
}) ]
p.ptr.id = r:int(uint64,row,1)
p.ptr.author = r:int(uint64,row,2)
p.ptr.posted = r:int(uint64,row,6)
p.ptr.discovered = r:int(uint64,row,7)
if r:null(row,8)
then p.ptr.parent = 0
else p.ptr.parent = r:int(uint64,row,8)
end
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)
var a: lib.mem.ptr(lib.store.actor)
var av: rawstring, avlen: intptr
var nym: rawstring, nymlen: intptr
var bio: rawstring, biolen: intptr
if r:null(row,5) then avlen = 0 av = nil else
av = r:string(row,5)
avlen = r:len(row,5)+1
end
if r:null(row,1) then nymlen = 0 nym = nil else
nym = r:string(row,1)
nymlen = r:len(row,1)+1
end
if r:null(row,4) then biolen = 0 bio = nil else
bio = r:string(row,4)
biolen = r:len(row,4)+1
end
a = [ lib.str.encapsulate(lib.store.actor, {
nym = {`nym, `nymlen};
bio = {`bio, `biolen};
avatar = {`av,`avlen};
handle = {`r:string(row, 2); `r:len(row,2) + 1};
xid = {`r:string(row, 10); `r:len(row,10) + 1};
}) ]
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.knownsince = r:int(int64,row, 9);
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
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(src, hash, origin, username, pw)
local validate = function(kind, cred, credlen)
return quote
var r = queries.actor_auth_pw.exec(
[&lib.store.source](src),
username,
kind,
[lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen},
origin)
if r.sz > 0 then -- found a record! stop here
var aid = r:int(uint64, 0,0)
r:free()
return aid
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.ptr), pw.ct, out) ~= 0 then
lib.bail('hashing failure!')
end
[ validate(string.format('pw-sha%u', hash), `&out[0], hash / 8) ]
end
return quote
lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
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_fetch_xid = [terra(src: &lib.store.source, xid: lib.mem.ptr(int8))
var r = queries.actor_fetch_xid.exec(src, xid)
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
): {lib.store.credset, bool}
var cs: lib.store.credset cs:clear();
var r = queries.actor_auth_how.exec(src, username, ip)
if r.sz == 0 then return cs, false end -- just in case
defer r:free()
(cs.pw << r:bool(0,0))
(cs.otp << r:bool(0,1))
(cs.challenge << r:bool(0,2))
(cs.trust << r:bool(0,3))
return cs, true
end];
actor_auth_pw = [terra(
src: &lib.store.source,
ip: lib.store.inet,
username: lib.mem.ptr(int8),
cred: lib.mem.ptr(int8)
): uint64
[ checksha(`src, 256, ip, username, cred) ] -- most common
[ checksha(`src, 512, ip, username, cred) ] -- most secure
[ checksha(`src, 384, ip, username, cred) ] -- weird
[ checksha(`src, 224, ip, username, cred) ] -- weirdest
-- TODO: check pbkdf2-hmac
-- TODO: check OTP
return 0
end];
actor_stats = [terra(src: &lib.store.source, uid: uint64)
var r = queries.actor_stats.exec(src, uid)
if r.sz == 0 then lib.bail('error fetching actor stats!') end
var s: lib.store.actor_stats
s.posts = r:int(uint64, 0, 0)
s.follows = r:int(uint64, 0, 1)
s.followers = r:int(uint64, 0, 2)
s.mutuals = r:int(uint64, 0, 3)
return s
end];
actor_session_fetch = [terra(
src: &lib.store.source,
aid: uint64,
ip : lib.store.inet
): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
var r = queries.actor_session_fetch.exec(src, aid, ip)
if r.sz == 0 then goto fail end
do defer r:free()
if r:null(0,0) then goto fail end
var a = row_to_actor(&r, 0)
a.ptr.source = src
var au = [lib.stat(lib.store.auth)] { ok = true }
au.val.aid = aid
au.val.uid = a.ptr.id
if not r:null(0,12) then -- restricted?
au.val.privs:clear()
(au.val.privs.post << r:bool(0,13))
(au.val.privs.edit << r:bool(0,14))
(au.val.privs.acct << r:bool(0,15))
(au.val.privs.upload << r:bool(0,16))
(au.val.privs.censor << r:bool(0,17))
(au.val.privs.admin << r:bool(0,18))
else au.val.privs:fill() end
return au, a
end
::fail:: return [lib.stat (lib.store.auth) ] { ok = false },
[lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
end];
post_create = [terra(
src: &lib.store.source,
post: &lib.store.post
): uint64
var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body)
if r.sz == 0 then return 0 end
defer r:free()
var id = r:int(uint64,0,0)
return id
end];
instance_timeline_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
var r = pqr { sz = 0 }
if rg.mode == 0 then
r = queries.instance_timeline_fetch.exec(src,rg.from_time,rg.to_time,0,0)
elseif rg.mode == 1 then
r = queries.instance_timeline_fetch.exec(src,rg.from_time,0,rg.to_idx,0)
elseif rg.mode == 2 then
r = queries.instance_timeline_fetch.exec(src,0,rg.to_time,0,rg.from_idx)
elseif rg.mode == 3 then
r = queries.instance_timeline_fetch.exec(src,0,0,rg.to_idx,rg.from_idx)
end
var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL
return ret
end];
}
return b