parsav  route.t at [93aea04a05]

File route.t artifact 881176d2e1 part of check-in 93aea04a05


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

terra http.user_notices(co: &lib.srv.convo, meth: method.t)
	if meth == method.post then
		var act = co:ppostv('act')
		if act:cmp(lib.str.plit'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)
	if meth == method.post then
		goto badop
	end

	if 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:tostr() defer pg:free()
			co:stdpage([lib.srv.convo.page] {
				title = lib.str.plit'media :: upload';
				class = lib.str.plit'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(lib.str.plit'desc') then
						desc = up.body
					elseif up.field:cmp(lib.str.plit'folder') then
						folder = up.body
					elseif up.field:cmp(lib.str.plit'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
	else
		if meth == method.post then goto badop end
		lib.render.media_gallery(co,path,co.who.id,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
	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()
		var safemime = mime
		-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
		-- chest wound. ultimately we need to compile a whitelist of safe mime
		-- types as part of mimelib, but that is no small task. for now, this
		-- will keep the patient from immediately bleeding out
		if mime:cmp(lib.str.plit'text/html') or
			mime:cmp(lib.str.plit'text/xml') or
			mime:cmp(lib.str.plit'application/xhtml+xml') or
			mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
		then -- danger will robinson
			safemime = lib.str.plit'text/plain'
		elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
			safemime = lib.str.plit'application/octet-stream'
		end
		lib.net.mg_printf(co.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", safemime.ct, safemime.ptr, data.ct + 2)
		lib.net.mg_send(co.con, data.ptr, data.ct)
		lib.net.mg_send(co.con, '\r\n', 2)
	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(lib.str.plit '/notices') then
		if co.aid == 0 then co:reroute('/login') return end
		http.user_notices(co,meth)
	elseif uri:cmp(lib.str.plit '/compose') then
		if co.aid == 0 then co:reroute('/login') return end
		http.post_compose(co,meth)
	elseif uri:cmp(lib.str.plit '/login') then
		if co.aid == 0
			then http.login_form(co, meth)
			else co:reroute('/')
		end
	elseif uri:cmp(lib.str.plit '/logout') 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('media')) then
			if co.aid == 0 then goto unauth end
			http.media_manager(co, path, meth)
		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