parsav  Artifact [2e402a5b93]

Artifact 2e402a5b93f6bbb22cb3ec774975c4c00338e0fca842c4dd40bd095440219d5b:


-- 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