parsav  route.t at [db4c5fd644]

File route.t artifact a9eb70a00e part of check-in db4c5fd644


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

	lib.render.user_page(co, actor.ptr)
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()

	lib.render.user_page(co, actor.ptr)
end

terra http.login_form(co: &lib.srv.convo, meth: method.t)
	if meth == method.get 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 == method.get 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 = co.srv:post_create(&p)

		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 == method.get 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 == method.get 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 goto badop 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
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	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)
			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually
		elseif path(1):cmp(lib.str.lit 'srv') then
			if not co.who.rights.powers.config() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'brand') then
			if not co.who.rights.powers.rebrand() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'users') then
			if not co.who.rights.powers:affect_users() then goto nopriv end

		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
		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})
		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[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')
		return
	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
			-- FIXME display home screen
			http.timeline(co, hpath {ptr=nil})
			goto notfound
		end
		return
	elseif uri.ptr[1] == @'@' then
		http.actor_profile_xid(co, uri, meth)
		return
	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
		if meth ~= method.get then goto wrongmeth end
		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
		return
	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})
		return
	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)
		return
	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
		return
	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
		return
	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 meth ~= method.get and meth ~= method.head 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
		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