Diff

mod_data_access/mod_data_access.lua @ 669:dd7d30c175d4

mod_data_access: Cleanup and update to new HTTP API
author Kim Alvefur <zash@zash.se>
date Mon, 21 May 2012 22:10:28 +0200
parent 486:b84493ef1d1d
child 1018:7e060edbb548
line wrap: on
line diff
--- a/mod_data_access/mod_data_access.lua	Mon May 21 21:30:51 2012 +0200
+++ b/mod_data_access/mod_data_access.lua	Mon May 21 22:10:28 2012 +0200
@@ -2,9 +2,10 @@
 -- By Kim Alvefur <zash@zash.se>
 
 local t_concat = table.concat;
+local t_insert = table.insert;
 local jid_prep = require "util.jid".prep;
 local jid_split = require "util.jid".split;
-local um_test_pw = require "core.usermanager".test_password;
+local test_password = require "core.usermanager".test_password;
 local is_admin = require "core.usermanager".is_admin
 local dm_load = require "util.datamanager".load;
 local dm_store = require "util.datamanager".store;
@@ -12,16 +13,9 @@
 local dm_list_store = require "util.datamanager".list_store;
 local dm_list_append = require "util.datamanager".list_append;
 local b64_decode = require "util.encodings".base64.decode;
-local http = require "net.http";
-local urldecode  = http.urldecode;
-local urlencode  = http.urlencode;
-local function http_response(code, message, extra_headers)
-	local response = {
-		status = code .. " " .. message;
-		body = message .. "\n"; }
-	if extra_headers then response.headers = extra_headers; end
-	return response
-end
+local saslprep = require "util.encodings".stringprep.saslprep;
+local realm = module:get_host() .. "/" .. module:get_name();
+module:depends"http";
 
 local encoders = {
 	lua = require "util.serialization".serialize,
@@ -35,122 +29,124 @@
 	["text/x-lua"] = "lua"; lua = "text/x-lua";
 	["application/json"] = "json"; json = "application/json";
 }
---[[
-encoders.xml = function(data)
-	return "<?xml version='1.0' encoding='utf-8'?><todo:write-this-serializer/>";
-end --]]
 
-local allowed_methods = {
-	GET = true, "GET",
-	PUT = true, "PUT",
-	POST = true, "POST",
-}
-
-local function handle_request(method, body, request)
-	if not allowed_methods[method] then
-		return http_response(405, "Method Not Allowed", {["Allow"] = t_concat(allowed_methods, ", ")});
-	end
-
-	if not request.headers["authorization"] then
-		return http_response(401, "Unauthorized",
-		{["WWW-Authenticate"]='Basic realm="WallyWorld"'})
+local function require_valid_user(f)
+	return function(event, path)
+		local request = event.request;
+		local response = event.response;
+		local headers = request.headers;
+		if not headers.authorization then
+			response.headers.www_authenticate = ("Basic realm=%q"):format(realm);
+			return 401
+		end
+		local from_jid, password = b64_decode(headers.authorization:match"[^ ]*$"):match"([^:]*):(.*)";
+		from_jid = jid_prep(from_jid);
+		password = saslprep(password);
+		if from_jid and password then
+			local user, host = jid_split(from_jid);
+			local ok, err = test_password(user, host, password);
+			if ok and user and host then
+				return f(event, path, from_jid);
+			elseif err then
+				module:log("debug", "User failed authentication: %s", err);
+			end
+		end
+		return 401
 	end
-	local user, password = b64_decode(request.headers.authorization
-		:match("[^ ]*$") or ""):match("([^:]*):(.*)");
-	user = jid_prep(user);
-	if not user or not password then return http_response(400, "Bad Request"); end
-	local user_node, user_host = jid_split(user)
-	if not hosts[user_host] then return http_response(401, "Unauthorized"); end
+end
 
-	module:log("debug", "authz %s", user)
-	if not um_test_pw(user_node, user_host, password) then
-		return http_response(401, "Unauthorized");
-	end
+local function handle_request(event, path, authed_user)
+	local request, response = event.request, event.response;
 
-	module:log("debug", "spliting path");
-	local path = {};
-	for i in string.gmatch(request.url.path, "[^/]+") do
-		table.insert(path, i);
+	--module:log("debug", "spliting path");
+	local path_items = {};
+	for i in string.gmatch(path, "[^/]+") do
+		t_insert(path_items, i);
 	end
-	table.remove(path, 1); -- the first /data
-	module:log("debug", "split path, got %d parts: %s", #path, table.concat(path, ", "));
+	--module:log("debug", "split path, got %d parts: %s", #path_items, table.concat(path_items, ", "));
 
-	if #path < 3 then
-		module:log("debug", "since we need at least 3 parts, adding %s/%s", user_host, user_node);
-		table.insert(path, 1, user_node);
-		table.insert(path, 1, user_host);
+	local user_node, user_host = jid_split(authed_user);
+	if #path_items < 3 then
+		--module:log("debug", "since we need at least 3 parts, adding %s/%s", user_host, user_node);
+		t_insert(path_items, 1, user_node);
+		t_insert(path_items, 1, user_host);
 		--return http_response(400, "Bad Request");
 	end
 
-	if #path < 3 then
-		return http_response(404, "Not Found");
+	if #path_items < 3 then
+		return 404;
 	end
 
-	local p_host, p_user, p_store, p_type = unpack(path);
+	local p_host, p_user, p_store, p_type = unpack(path_items);
 	
 	if not p_store or not p_store:match("^[%a_]+$") then
-		return http_response(404, "Not Found");
+		return 404;
 	end
 
-	if user_host ~= path[1] or user_node ~= path[2] then
+	if user_host ~= path_items[1] or user_node ~= path_items[2] then
 		-- To only give admins acces to anything, move the inside of this block after authz
-		module:log("debug", "%s wants access to %s@%s[%s], is admin?", user, p_user, p_host, p_store)
-		if not is_admin(user, p_host) then
-			return http_response(403, "Forbidden");
+		--module:log("debug", "%s wants access to %s@%s[%s], is admin?", authed_user, p_user, p_host, p_store)
+		if not is_admin(user_node, p_host) then
+			return 403;
 		end
 	end
 
+	local method = request.method;
 	if method == "GET" then
 		local data = dm_load(p_user, p_host, p_store);
 
-		data = data or dm_load_list(p_user, p_host, p_store);
+		data = data or dm_list_load(p_user, p_host, p_store);
 
 		--TODO Use the Accept header
-		content_type = p_type or "json";
+		local content_type = p_type or "json";
 		if data and encoders[content_type] then 
-			return {
-				status = "200 OK",
-				body = encoders[content_type](data) .. "\n",
-				headers = {["content-type"] = content_type_map[content_type].."; charset=utf-8"}
-			};
+			response.headers.content_type = content_type_map[content_type].."; charset=utf-8";
+			return encoders[content_type](data);
 		else
-			return http_response(404, "Not Found");
+			return 404;
 		end
-	else -- POST or PUT
+	elseif method == "POST" or method == "PUT" then
+		local body = request.body;
 		if not body then
-			return http_response(400, "Bad Request")
+
+			return 400;
 		end
-		local content_type, content = request.headers["content-type"], body;
+		local content_type, content = request.headers.content_type, body;
 		content_type = content_type and content_type_map[content_type]
-		module:log("debug", "%s: %s", content_type, tostring(content));
+		--module:log("debug", "%s: %s", content_type, tostring(content));
 		content = content_type and decoders[content_type] and decoders[content_type](content);
-		module:log("debug", "%s: %s", type(content), tostring(content));
+		--module:log("debug", "%s: %s", type(content), tostring(content));
 		if not content then
-			return http_response(400, "Bad Request")
+			return 400;
 		end
 		local ok, err
 		if method == "PUT" then
 			ok, err = dm_store(p_user, p_host, p_store, content);
 		elseif method == "POST" then
 			ok, err = dm_list_append(p_user, p_host, p_store, content);
-		elseif method == "DELETE" then
-			dm_store(p_user, p_host, p_store, nil);
-			dm_list_store(p_user, p_host, p_store, nil);
 		end
 		if ok then
-			return http_response(201, "Created", { Location = t_concat({"/data",p_host,p_user,p_store}, "/") });
+			response.headers.location = t_concat({module:http_url(nil,"/data"),p_host,p_user,p_store}, "/");
+			return 201;
 		else
-			return { status = "500 Internal Server Error", body = err }
+			response.headers.debug = err;
+			return 500;
 		end
+	elseif method == "DELETE" then
+		dm_store(p_user, p_host, p_store, nil);
+		dm_list_store(p_user, p_host, p_store, nil);
+		return 204;
 	end
 end
 
-local function setup()
-	local ports = module:get_option("data_access_ports") or { 5280 };
-	require "net.httpserver".new_from_config(ports, handle_request, { base = "data" });
-end
-if prosody.start_time then -- already started
-	setup();
-else
-	prosody.events.add_handler("server-started", setup);
-end
+local handle_request_with_auth = require_valid_user(handle_request);
+
+module:provides("http", {
+	default_path = "/data";
+	route = {
+		["GET /*"] = handle_request_with_auth,
+		["PUT /*"] = handle_request_with_auth,
+		["POST /*"] = handle_request_with_auth,
+		["DELETE /*"] = handle_request_with_auth,
+	};
+});