-- 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 binblob = lib.mem.ptr(uint8)
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 rel.recip.block() then
if act:cmp( 'follow') or act:cmp( 'subscribe') then
co:complain(403,'blocked','you cannot follow a user you are blocked by') return
end
end
if act:cmp( 'block') and not rel.rel.block() then
(rel.rel.block << true) ; (rel.recip.follow << false)
co.srv:actor_rel_create([lib.store.relation.idvmap.block], co.who.id, actor.id)
co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], actor.id, co.who.id)
else
[(function()
local tests = quote co:complain(400,'bad request','the action you have attempted on this user is not meaningful') return end
for i,v in ipairs(lib.store.relation.members) do
tests = quote
if [v ~= 'block'] and act:cmp(([v])) and not rel.rel.[v]() then -- rely on dead code elimination :/
(rel.rel.[v] << true)
co.srv:actor_rel_create([lib.store.relation.idvmap[v]], co.who.id, actor.id)
elseif act:cmp((['un'..v])) and rel.rel.[v]() then
(rel.rel.[v] << false)
co.srv:actor_rel_destroy([lib.store.relation.idvmap[v]], co.who.id, actor.id)
else [tests] end
end
end
return tests
end)()]
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(&co.srv.pool, 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, pstring.null())
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, '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, pstring.null())
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,
pstring {ptr=usn,ct=usnl},
pstring {ptr=chrs,ct=chrsl})
elseif lib.str.ncmp('challenge', am, lib.math.biggest(9,aml)) == 0 and chrs ~= nil then
lib.dbg('challenge attempt beginning')
var s_time = co:ppostv('time')
var s_vfy = co:ppostv('vfy')
var token = co:ppostv('token')
if s_time:ref() and s_vfy:ref() and token:ref() then
lib.dbg('checking hmac validity')
var vftok = co:stra(128) vftok:ppush(token):ppush(s_time)
var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), vftok:finalize())
var vfy, vfyok = lib.math.shorthand.parse(s_vfy.ptr, s_vfy.ct)
if vfyok and lib.math.truncate64(hmac.ptr,hmac.ct) == vfy then
lib.dbg('checking expiration time')
var time, timeok = lib.math.shorthand.parse(s_time.ptr, s_time.ct)
if timeok and lib.osclock.time(nil) - time < [2 * 60] then -- two minutes
lib.dbg('decoding base64')
var bin = co.srv.pool:alloc(uint8, chrsl)
var binlen: intptr
if lib.b64.mbedtls_base64_decode(bin.ptr, bin.ct, &binlen, [&uint8](chrs), chrsl) == 0 then
lib.dbg('running signature <',{chrs,chrsl},'> against challenge keys for token [', {token.ptr,token.ct}, ']')
aid = co.srv:actor_auth_challenge(co.peer,
pstring {usn,usnl}, binblob{bin.ptr,binlen}, token)
end
end
end
end
elseif lib.str.ncmp('otp', am, lib.math.biggest(3,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, '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;
parent = 0;
}
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)
var rt: lib.store.notice
if not post then
rt = co.srv:post_act_fetch_notice(pid)
if rt.kind ~= lib.store.noticetype.rt then
co:complain(404, 'post not found', 'no such post is known to this server')
return
elseif rt.who ~= co.who.id then
co:complain(403, 'forbidden', 'you cannot cancel other people\'s retweets')
return
end
end
defer post:free() -- NOP on null
if path.ct == 3 then
var lnk: lib.str.acc lnk:compose('/post/', path(1))
var lnkp = lnk:finalize() defer lnkp:free()
if post:ref() and post(0).author ~= co.who.id then
co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
return
elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
if not co:assertpow('edit') then return end
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
if post:ref() then
conf = data.view.confirm {
title = 'delete post';
query = 'are you sure you want to delete this post?';
cancel = lnkp
}
else
conf = data.view.confirm {
title = 'cancel retweet';
query = 'are you sure you want to undo this retweet?';
cancel = '/';
}
end
var fr = co.srv.pool:frame()
var body = conf:poolstr(&co.srv.pool) --defer body:free()
co:stdpage([lib.srv.convo.page] {
title = 'post :: delete';
class = 'query';
body = body; cache = false;
})
co.srv.pool:reset(fr)
return
elseif meth == method.post then
var act = co:ppostv('act')
if act:cmp( 'confirm') then
if post:ref() then
post(0).source:post_destroy(post(0).id)
elseif rt.kind ~= 0 then
co.srv:post_act_cancel(pid)
end
co:reroute('/') -- TODO maybe return to parent or conversation if possible
return
else goto badop end
end
else goto badurl end
end
if post:ref() and meth == method.post then
if co.aid == 0 then goto noauth end
var act = co:ppostv('act')
if act:cmp( '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( '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( 'rt') then
co.srv:post_retweet(co.who.id, pid, false)
post.ptr.rts = post.ptr.rts + 1
elseif act:cmp( 'post') then
var replytext = co:ppostv('post')
var acl = co:ppostv('acl')
var subj = co:ppostv('subject')
if not acl then acl = '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
if not post then goto badurl 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
local terra
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
var act = co:ppostv('act')
if not act then return true end
lib.dbg('handling credential action')
if act:cmp( 'invalidate') then
lib.dbg('setting user\'s cookie validation time to now')
co.who.source:auth_sigtime_user_alter(uid, 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('?',co.aid)
return false
elseif act:cmp('revoke') then
var s_cred = co:ppostv('cred')
if s_cred:ref() then
var cred, ok = lib.math.shorthand.parse(s_cred.ptr, s_cred.ct)
if ok then
co.srv:auth_destroy_aid_uid(cred,co.who.id)
end
end
return true
elseif act:cmp('newcred') then
var cmt = co:ppostv('comment')
var pw = co:ppostv('newpw')
var rsapub = co:ppostv('newrsa'):blob()
var aid: uint64 = 0
if pw:ref() then
var cpw = co:ppostv('rptpw')
if not pw:cmp(cpw) then
co:complain(400,'enrollment failure','the passwords you supplied do not match')
return false
end
aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
elseif rsapub:ref() then
var sig = co:ppostv('sig')
var nonce = co:ppostv('nonce')
var s_noncevld = co:ppostv('noncevld')
var noncevld, ok = lib.math.shorthand.parse(s_noncevld.ptr, s_noncevld.ct)
if not ok then
co:complain(403,'try harder next time','you call that cryptanalysis?')
return false
end
var fr = co.srv.pool:frame()
var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
return false
end
var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
if not pkres.ok then
co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
return false
end
var pk = pkres.val
defer pk:free()
var decoded = co.srv.pool:alloc(uint8,sig.ct)
var decoded_sz: intptr = 0
if lib.b64.mbedtls_base64_decode(decoded.ptr,sig.ct,&decoded_sz,[&uint8](sig.ptr),sig.ct) ~= 0 then
co:complain(400,'invalid signature','the signature you supplied is not encoded in valid base64')
return false
end
var vfy, secl = lib.crypt.verify(&pk, nonce.ptr, nonce.ct, decoded.ptr, decoded_sz)
if not vfy then
co:complain(403,'verification failed','the signature you supplied does not match the required nonce')
return false
end
var dbuf: uint8[lib.crypt.const.maxdersz]
var derkey = lib.crypt.der(true, &pk, &dbuf[0])
aid = co.srv:auth_attach_rsa(co.who.id, false, derkey, cmt)
co.srv.pool:reset(fr)
end
if aid ~= 0 then
lib.dbg('setting credential restrictions')
var privs = [(function()
local check = quote end
local me = symbol(lib.store.privset)
for i,v in ipairs(lib.store.privset.members) do
check = quote [check]
var val = co:pgetv(['allow-' .. v])
if val:ref() and val:cmp('on')
then ([me].[v] << true)
else ([me].[v] << false)
end
end
end
return quote
var [me]
[check]
in [me] end
end)()]
privs:dump()
if privs:sz() > 0 then
lib.dbg('installing credential restrictions')
lib.io.fmt('on priv %llu\n',aid)
co.srv:auth_privs_set(aid, privs)
end
lib.dbg('setting netmask restrictions')
var nm = co:pgetv('netmask')
end
co:reroute('?')
return false
end
co:complain(400,'bad request','the operation you have requested is not meaningful in this context')
return false
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 >= 2 then
if not co.who.rights.powers.config() and (
path(1):cmp('srv') or
path(1):cmp('badge') or
path(1):cmp('emoji')
) then goto nopriv
elseif not co.who.rights.powers.rebrand() and (
path(1):cmp('brand')
) then goto nopriv
elseif not co.who.rights.powers.account() and (
path(1):cmp('profile') or
path(1):cmp('sec') or
path(1):cmp('avi') or
path(1):cmp('ui')
) 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( '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 = 'profile changes saved'
--user_refresh = true -- not really necessary here, actually
elseif path(1):cmp('sec') then
if not credsec_for_uid(co, co.who.id) then return end
elseif path(1):cmp('avi') then
var act = co:ppostv('act')
if act:ref() and act:cmp('clear') then
co.who.avatarid = 0
co.who.source:actor_save(co.who)
msg = 'avatar reset to default'
else goto badop end
elseif path(1):cmp('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
usr:free()
goto nopriv
end
else goto badop end
defer usr:free()
if path.ct == 4 then
if path(3):cmp(lib.str.lit 'cred') then
if not credsec_for_uid(co, userid) then return end
end
elseif path.ct == 3 then
var purgestr = co:ppostv("purgestr")
var purgekey = co:ppostv("purgekey")
if purgestr:ref() and purgekey:ref() and purgestr(0) ~= 0 then
if purgestr:cmp(purgekey) then -- destroying account! :O
co.srv:actor_purge_uid(userid)
co:reroute('/conf/users')
return
else msg = 'purge confirmation failed' end
end
var epithet = co:ppostv("epithet")
var s_rank = co:ppostv("rank")
var s_invites = co:ppostv("invites")
var s_quota = co:ppostv("quota")
var ch_staff = co:ppostv("staff")
var torank: uint16 = usr(0).rights.rank
if ch_staff:ref() and ch_staff:cmp('on') then
if s_rank:ref() then
var rank, rok = lib.math.decparse(s_rank)
if rok and rank <= co.srv.cfg.nranks then
torank = rank
end
elseif usr(0).rights.rank == 0 then
torank = co.who.rights.rank + 1
end
else torank = 0 end
if co.who.id ~= userid and co.who.rights.rank > 0 then
if (co.who.rights.powers.elevate() and
(torank < usr(0).rights.rank or usr(0).rights.rank == 0) and
(torank > co.who.rights.rank or co.who.rights.rank == 1))
or (co.who.rights.powers.demote() and
(torank > usr(0).rights.rank or torank == 0))
then usr(0).rights.rank = torank end
end
if s_invites:ref() then
var n_invites, n_invites_ok = lib.math.decparse(s_invites)
if n_invites_ok and n_invites ~= usr(0).rights.invites then
if (n_invites > usr(0).rights.invites and
co.who.rights.powers.elevate() and
co.who.rights.powers.invite())
or (n_invites < usr(0).rights.invites and
co.who.rights.powers.demote())
then usr(0).rights.invites = n_invites end
end
end
if (co.who.id ~= userid or co.who.rights.rank == 1) and s_quota:ref() then
var n_quota, n_quota_ok = lib.math.decparse(s_quota)
if n_quota_ok and n_quota ~= usr(0).rights.quota then
if (co.who.rights.powers.elevate() and
((n_quota == 0 and co.who.rights.quota == 0 or co.who.rights.rank == 1) or
(n_quota ~= 0 and (n_quota > usr(0).rights.quota and
(co.who.rights.quota == 0 or
co.who.rights.quota >= n_quota or
co.who.rights.rank == 1)))))
or (co.who.rights.powers.demote() and n_quota ~= 0 and
(n_quota < usr(0).rights.quota or
co.who.rights.rank == 1))
then usr(0).rights.quota = n_quota end
end
end
if co.who.rights.powers.herald() and
(co.who.id ~= userid or
co.srv.cfg.pol_autoherald or
co.who.rights.rank == 1) then
if epithet:ref() and epithet(0) ~= 0 then
usr(0).epithet = epithet.ptr
else
usr(0).epithet = nil
end
end
if co.who.id ~= userid then
-- update powers
end
co.srv:actor_save(usr.ptr)
if not msg then msg = 'user record updated' end
end
end
elseif path.ct == 2 and meth == method.post then
var act = co:ppostv('act')
if act:cmp('create') then
var newname = co:ppostv('handle')
if not newname or not lib.store.actor.handle_validate(newname.ptr) then
co:complain(400,'invalid handle','the handle you have requested is not valid')
end
var tu = co.srv:actor_fetch_xid(newname)
if tu:ref() then tu:free()
co:complain(409,'handle clash','that handle conflicts with one that already exists')
return
end
var kbuf: uint8[lib.crypt.const.maxdersz]
var na = lib.store.actor.mk(&kbuf[0])
na.handle = newname.ptr
var newuid = co.srv:actor_create(&na)
var shid: int8[lib.math.shorthand.maxlen]
var shidlen = lib.math.shorthand.gen(newuid, &shid[0])
var url = lib.str.acc{}:compose('/conf/users/',pstring{&shid[0],shidlen}):finalize() defer url:free()
co:reroute(url.ptr)
return
elseif act:cmp('inst') then
else goto badop end
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:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end
::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end
end
terra http.user_notices(co: &lib.srv.convo, meth: method.t)
if meth == method.post then
var act = co:ppostv('act')
if act:cmp('clear') then
co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
co:reroute('/')
return
else goto badop end
end
lib.render.notices(co)
do return end
::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end
terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t, uid: uint64)
if co.aid ~= 0 and co.who.id == uid and path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
if meth == method.get then
var view = data.view.media_upload {
folders = ''
}
var pg = view:poolstr(&co.srv.pool) -- defer pg:free()
co:stdpage([lib.srv.convo.page] {
title = 'media :: upload';
class = 'media upload';
cache = false; body = pg;
})
elseif meth == method.post_file then
var desc = pstring.null()
var folder = pstring.null()
var mime = pstring.null()
var name = pstring.null()
var body = binblob.null()
for i=0, co.uploads.sz do var up = co.uploads.storage.ptr + i
if up.body.ct > 0 then
if up.field:cmp('desc') then
desc = up.body
elseif up.field:cmp('folder') then
folder = up.body
elseif up.field:cmp('file') then
mime = up.ctype
body = binblob {ptr = [&uint8](up.body.ptr), ct = up.body.ct}
name = up.filename
end
end
end
if not body then goto badop end
if body.ct > co.srv.cfg.maxupsz then
co:complain(403, 'file too long', "the file you have attempted to upload exceeds the maximum length permitted by this server's upload policy. if it is an image or video, try compressing it at a lower quality setting or resolution")
return
end
var id = co.srv:artifact_instantiate(body,mime)
if id == 0 then
co:complain(500,'upload failed','artifact rejected. either the server is running out of space or this file is banned from the server')
return
end
co.srv:artifact_expropriate(co.who.id,id,desc,folder)
var idbuf: int8[lib.math.shorthand.maxlen]
var idlen = lib.math.shorthand.gen(id,&idbuf[0])
var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
co:reroute(url.ptr)
url:free()
else goto badop end
elseif co.aid ~= 0 and path.ct == 4 and path(1):cmp(lib.str.lit'a') and meth==method.post then
var act = co:ppostv('act')
if not act or not act:cmp('confirm') then goto badop end
var artid, aok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
if not aok then goto e404 end
var art = co.srv:artifact_fetch(uid,artid)
if not art then goto e404 end
defer art:free()
if path(3):cmp(lib.str.lit'avi') then
-- user wants to set avatar
co.who.avatarid = artid
co.srv:actor_save(co.who)
co:reroute('/conf/avi')
elseif path(3):cmp(lib.str.lit'del') then
co.srv:artifact_disclaim(co.who.id, artid)
co:reroute('/media')
else goto badop end
else
if meth == method.post then goto badop end
lib.render.media_gallery(co,path,uid,nil)
end
do return end
::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
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
var usr = co.srv:actor_fetch_xid(handle)
if not usr then
goto default end
if usr(0).origin == 0 then
if usr(0).avatarid == 0 then goto default end
var avi, mime = co.srv:artifact_load(usr(0).avatarid)
if not avi then goto default end
defer avi:free() defer mime:free()
co:bytestream(mime,avi)
else
co:reroute(usr(0).avatar)
end
do return end
::default:: co:reroute('/s/default-avatar.webp')
end
terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
if not idok then goto e404 end
var data, mime = co.srv:artifact_load(id)
if not data then goto e404 end
do defer data:free() defer mime:free()
co:bytestream(mime,data)
return end
::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
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('/file/', uri.ptr, 6) == 0 then
http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6})
elseif uri:cmp( '/notices') then
if co.aid == 0 then co:reroute('/login') return end
http.user_notices(co,meth)
elseif uri:cmp( '/compose') then
if co.aid == 0 then co:reroute('/login') return end
http.post_compose(co,meth)
elseif uri:cmp( '/login') then
if co.aid == 0
then http.login_form(co, meth)
else co:reroute('/')
end
elseif uri:cmp( '/logout') then
if co.aid == 0
then goto notfound
else co:reroute_cookie('/','auth=; Path=/')
end
else -- hierarchical routes
var path = lib.http.hier(&co.srv.pool, 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('media')) then
if co.aid == 0 then goto unauth end
http.media_manager(co, path, meth, co.who.id)
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