-- vim: ft=terra
local r = lib.srv.route
local method = lib.http.method
local pstring = lib.mem.ptr(int8)
local rstring = lib.mem.ref(int8)
local hpath = lib.mem.ptr(rstring)
local http = {}
terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end
terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t)
var rel: lib.store.relationship
if co.aid ~= 0 then
rel = co.srv:actor_rel_calc(co.who.id, actor.id)
if meth == method.post then
var act = co:ppostv('act')
if act:cmp(lib.str.plit 'follow') and not rel.rel.follow() then
if rel.recip.block() then
co:complain(403,'blocked','you cannot follow a user you are blocked by') return
end
(rel.rel.follow << true)
co.srv:actor_rel_create([lib.store.relation.idvmap.follow], co.who.id, actor.id)
elseif act:cmp(lib.str.plit 'unfollow') and rel.rel.follow() then
(rel.rel.follow << false)
co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], co.who.id, actor.id)
end
end
else
rel.rel:clear()
rel.recip:clear()
end
lib.render.user_page(co, actor, &rel)
end
terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
for i=2,uri.ct do
if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
end
if handle.ct == 0 then
handle.ct = uri.ct - 2
uri:advance(uri.ct)
elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end
lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})
var path = lib.http.hier(uri) defer path:free()
for i=0,path.ct do
lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct})
end
var actor = co.srv:actor_fetch_xid(handle)
if actor.ptr == nil then
co:complain(404,'no such user','no such user known to this server')
return
end
defer actor:free()
http.actor_profile(co,actor.ptr,meth)
end
terra http.actor_profile_uid (
co: &lib.srv.convo,
path: lib.mem.ptr(lib.mem.ref(int8)),
meth: method.t
)
if path.ct < 2 then
co:complain(404,'bad url','invalid user url')
return
end
var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
if not ok then
co:complain(400, 'bad user ID', 'that user ID is not valid')
return
end
var actor = co.srv:actor_fetch_uid(uid)
if actor.ptr == nil then
co:complain(404, 'no such user', 'no user by that ID is known to this instance')
return
end
defer actor:free()
http.actor_profile(co,actor.ptr,meth)
end
terra http.login_form(co: &lib.srv.convo, meth: method.t)
if meth_get(meth) then
-- request a username
lib.render.login(co, nil, nil, lib.str.plit(nil))
elseif meth == method.post then
var usn, usnl = co:postv('user')
var am, aml = co:postv('authmethod')
var chrs, chrsl = co:postv('response')
var cs, authok = co.srv:actor_auth_how(co.peer, usn)
var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] {
ptr = usn, ct = usnl
})
if authok == false then
lib.render.login(co, nil, nil, lib.str.plit'access denied')
return
end
var fakeact = false
var fakeactor: lib.store.actor
if act.ptr == nil then
-- the user is known to us but has not yet claimed an
-- account on the server. create a template for the
-- account that will be created once they log in
fakeact = true
fakeactor = lib.store.actor {
id = 0, handle = usn, nym = nil;
origin = 0, bio = nil;
key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0};
epithet = nil;
}
act.ct = 1
act.ptr = &fakeactor
act.ptr.rights = lib.store.rights_default()
end
if am == nil then
-- pick an auth method
lib.render.login(co, act.ptr, &cs, lib.str.plit(nil))
else var aid: uint64 = 0
lib.dbg('authentication attempt beginning')
-- attempt login with provided method
if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
aid = co.srv:actor_auth_pw(co.peer,
[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
lib.dbg('using otp auth')
-- ยทยทยท --
else lib.dbg('invalid auth method') end
-- error out
if aid == 0 then
lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
else
co:installkey('/',aid)
end
end
if act.ptr ~= nil and fakeact == false then act:free() end
else
::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
end
return
end
terra http.post_compose(co: &lib.srv.convo, meth: method.t)
if not co:assertpow('post') then return end
--if co.who.rights.powers.post() == false then
--co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action')
if meth_get(meth) then
lib.render.compose(co, nil, nil)
elseif meth == method.post then
var text, textlen = co:postv("post")
var acl, acllen = co:postv("acl")
var subj, subjlen = co:postv("subject")
if text == nil or acl == nil then
co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
return
end
if subj == nil then subj = '' end
var p = lib.store.post {
author = co.who.id, acl = acl;
body = text, subject = subj;
}
var newid = p:publish(co.srv)
var idbuf: int8[lib.math.shorthand.maxlen]
var idlen = lib.math.shorthand.gen(newid, idbuf)
var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
co:reroute(redirto.buf)
end
end
terra http.timeline(co: &lib.srv.convo, mode: hpath)
lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1]))
end
terra http.documentation(co: &lib.srv.convo, path: hpath)
if path.ct == 2 then
lib.render.docpage(co,path(1))
elseif path.ct == 1 then
lib.render.docpage(co, rstring.null())
else
co:complain(404, 'no such documentation', 'invalid documentation URL')
end
end
terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
if not ok then
co:complain(400, 'bad post ID', 'that post ID is not valid')
return
end
var post = co.srv:post_fetch(pid)
if not post then
co:complain(404, 'post not found', 'no such post is known to this server')
return
end
defer post:free()
if path.ct == 3 then
var lnk: lib.str.acc lnk:compose('/post/', path(1))
var lnkp = lnk:finalize() defer lnkp:free()
if post(0).author ~= co.who.id then
co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
return
elseif path(2):cmp(lib.str.lit 'edit') then
if meth_get(meth) then
lib.render.compose(co, post.ptr, nil)
return
elseif meth == method.post then
var newbody = co:postv('post')._0
var newacl = co:postv('acl')._0
var newsubj = co:postv('subject')._0
if newbody ~= nil then post(0).body = newbody end
if newacl ~= nil then post(0).acl = newacl end
if newsubj ~= nil then post(0).subject = newsubj end
post(0):save(true)
co:reroute(lnkp.ptr)
end
return
elseif path(2):cmp(lib.str.lit 'del') then
if meth_get(meth) then
var conf = data.view.confirm {
title = lib.str.plit 'delete post';
query = lib.str.plit 'are you sure you want to delete this post?';
cancel = lnkp
}
var body = conf:tostr() defer body:free()
co:stdpage([lib.srv.convo.page] {
title = lib.str.plit 'post :: delete';
class = lib.str.plit 'query';
body = body; cache = false;
})
return
elseif meth == method.post then
var act = co:ppostv('act')
if act:cmp(lib.str.plit 'confirm') then
post(0).source:post_destroy(post(0).id)
co:reroute('/') -- TODO maybe return to parent or conversation if possible
return
else goto badop end
end
else goto badurl end
end
if meth == method.post then
if co.aid == 0 then goto noauth end
var act = co:ppostv('act')
if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
co.srv:post_like(co.who.id, pid, false)
post.ptr.likes = post.ptr.likes + 1
elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
co.srv:post_like(co.who.id, pid, true)
post.ptr.likes = post.ptr.likes - 1
elseif act:cmp(lib.str.plit 'rt') then
co.srv:post_retweet(co.who.id, pid, false)
post.ptr.rts = post.ptr.rts + 1
elseif act:cmp(lib.str.plit 'post') then
var replytext = co:ppostv('post')
var acl = co:ppostv('acl')
var subj = co:ppostv('subject')
if not acl then acl = lib.str.plit 'all' end
if not replytext then goto badop end
var reply = lib.store.post {
author = co.who.id, parent = pid;
subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
}
reply:publish(co.srv)
else goto badop end
end
lib.render.tweet_page(co, path, post.ptr)
do return end
::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
end
terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
var msg = pstring.null()
-- first things first, do priv checks
if path.ct >= 1 then
if not co.who.rights.powers.config() and (
path(1):cmp(lib.str.lit 'srv') or
path(1):cmp(lib.str.lit 'badge') or
path(1):cmp(lib.str.lit 'emoji')
) then goto nopriv
elseif not co.who.rights.powers.rebrand() and (
path(1):cmp(lib.str.lit 'brand')
) then goto nopriv
elseif not co.who.rights.powers.account() and (
path(1):cmp(lib.str.lit 'profile') or
path(1):cmp(lib.str.lit 'acct')
) then goto nopriv
elseif not co.who.rights.powers:affect_users() and (
path(1):cmp(lib.str.lit 'users')
) then goto nopriv end
end
if meth == method.post and path.ct >= 1 then
var user_refresh = false var fail = false
if path(1):cmp(lib.str.lit 'profile') then
lib.dbg('updating profile')
co.who.bio = co:postv('bio')._0
co.who.nym = co:postv('nym')._0
if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
co.who.source:actor_save(co.who)
var act = co:ppostv('act')
var resethue = false
if act:ref() then
resethue = act:cmp(lib.str.plit 'reset-hue')
end
if not resethue then
var shue = co:ppostv('hue')
var nhue, okhue = lib.math.decparse(shue)
if okhue and nhue ~= co.ui_hue then
if nhue == co.srv.cfg.ui_hue
then resethue = true
else co.srv:actor_conf_int_set(co.who.id, 'ui-accent', nhue)
end
co.ui_hue = nhue
end
end
if resethue then
co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
co.ui_hue = co.srv.cfg.ui_hue
end
msg = lib.str.plit 'profile changes saved'
--user_refresh = true -- not really necessary here, actually
elseif path(1):cmp(lib.str.lit 'sec') then
var act = co:ppostv('act')
if act:cmp(lib.str.plit 'invalidate') then
lib.dbg('setting user\'s cookie validation time to now')
co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
co:installkey('/conf/sec',co.aid)
return
end
elseif path(1):cmp(lib.str.lit 'users') then
if path.ct >= 3 then
var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
if ok then
var usr = co.srv:actor_fetch_uid(userid)
if usr:ref() then defer usr:free()
if not co.who:overpowers(usr.ptr) then goto nopriv end
end
end
elseif path.ct == 2 then
end
end
if user_refresh then -- refresh the user info for the renderer
var usr = co.srv:actor_fetch_uid(co.who.id)
lib.mem.heapf(co.who)
co.who = usr.ptr
end
var go,golen = co:getv('go')
if not fail and go ~= nil then
co:reroute(go)
return
end
end
lib.render.conf(co,path,msg)
do return end
::nopriv:: co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action')
end
do local branches = quote end
local filename, flen = symbol(&int8), symbol(intptr)
local page = symbol(lib.http.page)
local send = label()
local storage = data.stmap
for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
local d = data.static[id]
branches = quote [branches];
if lib.str.ncmp(filename, id, lib.math.biggest([#id], flen)) == 0 then
page.headers.ptr[0].value = mime;
page.body = [lib.mem.ptr(int8)] {
ptr = storage[([i-1])].ptr;
ct = storage[([i-1])].ct;
}
goto [send]
end
end
end
terra http.static_content(co: &lib.srv.convo, [filename], [flen])
var hdrs = array(
lib.http.header{'Content-Type',nil},
lib.http.header{'Cache-Control','max-age=2592000'} -- TODO immutable?
)
var [page] = lib.http.page {
respcode = 200;
headers = [lib.mem.ptr(lib.http.header)] {
ptr = &hdrs[0], ct = 1
}
}
[branches]
do return false end
::[send]:: page:send(co.con) return true
end
end
terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
-- TODO retrieve user avatars
co:reroute('/s/default-avatar.webp')
end
-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
co.navbar = lib.render.nav(co)
-- some routes are non-hierarchical, and can be resolved with a simple strcmp
-- we run through those first before giving up and parsing the URI
if uri.ptr == nil or uri.ptr[0] ~= @'/' then
co:complain(404, 'what the hell', 'how did you do that')
elseif uri.ct == 1 then -- root
if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
http.login_form(co, meth)
else http.timeline(co, hpath {ptr=nil}) end
elseif uri.ptr[1] == @'@' then
http.actor_profile_xid(co, uri, meth)
elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
if not meth_get(meth) then goto wrongmeth end
if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
if co.aid == 0 then co:reroute('/login') return end
http.post_compose(co,meth)
elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
if co.aid == 0
then http.login_form(co, meth)
else co:reroute('/')
end
elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
if co.aid == 0
then goto notfound
else co:reroute_cookie('/','auth=; Path=/')
end
else -- hierarchical routes
var path = lib.http.hier(uri) defer path:free()
if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
http.actor_profile_uid(co, path, meth)
elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
http.tweet_page(co, path, meth)
elseif path(0):cmp(lib.str.lit('tl')) then
http.timeline(co, path)
elseif path(0):cmp(lib.str.lit('doc')) then
if not meth_get(meth) then goto wrongmeth end
http.documentation(co, path)
elseif path(0):cmp(lib.str.lit('conf')) then
if co.aid == 0 then goto unauth end
http.configure(co,path,meth)
else goto notfound end
end
do return end
::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
end