comparison 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
parents b84493ef1d1d
children 7e060edbb548
comparison
equal deleted inserted replaced
668:343b115ebbea 669:dd7d30c175d4
1 -- HTTP Access to datamanager 1 -- HTTP Access to datamanager
2 -- By Kim Alvefur <zash@zash.se> 2 -- By Kim Alvefur <zash@zash.se>
3 3
4 local t_concat = table.concat; 4 local t_concat = table.concat;
5 local t_insert = table.insert;
5 local jid_prep = require "util.jid".prep; 6 local jid_prep = require "util.jid".prep;
6 local jid_split = require "util.jid".split; 7 local jid_split = require "util.jid".split;
7 local um_test_pw = require "core.usermanager".test_password; 8 local test_password = require "core.usermanager".test_password;
8 local is_admin = require "core.usermanager".is_admin 9 local is_admin = require "core.usermanager".is_admin
9 local dm_load = require "util.datamanager".load; 10 local dm_load = require "util.datamanager".load;
10 local dm_store = require "util.datamanager".store; 11 local dm_store = require "util.datamanager".store;
11 local dm_list_load = require "util.datamanager".list_load; 12 local dm_list_load = require "util.datamanager".list_load;
12 local dm_list_store = require "util.datamanager".list_store; 13 local dm_list_store = require "util.datamanager".list_store;
13 local dm_list_append = require "util.datamanager".list_append; 14 local dm_list_append = require "util.datamanager".list_append;
14 local b64_decode = require "util.encodings".base64.decode; 15 local b64_decode = require "util.encodings".base64.decode;
15 local http = require "net.http"; 16 local saslprep = require "util.encodings".stringprep.saslprep;
16 local urldecode = http.urldecode; 17 local realm = module:get_host() .. "/" .. module:get_name();
17 local urlencode = http.urlencode; 18 module:depends"http";
18 local function http_response(code, message, extra_headers)
19 local response = {
20 status = code .. " " .. message;
21 body = message .. "\n"; }
22 if extra_headers then response.headers = extra_headers; end
23 return response
24 end
25 19
26 local encoders = { 20 local encoders = {
27 lua = require "util.serialization".serialize, 21 lua = require "util.serialization".serialize,
28 json = require "util.json".encode 22 json = require "util.json".encode
29 }; 23 };
33 }; 27 };
34 local content_type_map = { 28 local content_type_map = {
35 ["text/x-lua"] = "lua"; lua = "text/x-lua"; 29 ["text/x-lua"] = "lua"; lua = "text/x-lua";
36 ["application/json"] = "json"; json = "application/json"; 30 ["application/json"] = "json"; json = "application/json";
37 } 31 }
38 --[[
39 encoders.xml = function(data)
40 return "<?xml version='1.0' encoding='utf-8'?><todo:write-this-serializer/>";
41 end --]]
42 32
43 local allowed_methods = { 33 local function require_valid_user(f)
44 GET = true, "GET", 34 return function(event, path)
45 PUT = true, "PUT", 35 local request = event.request;
46 POST = true, "POST", 36 local response = event.response;
47 } 37 local headers = request.headers;
38 if not headers.authorization then
39 response.headers.www_authenticate = ("Basic realm=%q"):format(realm);
40 return 401
41 end
42 local from_jid, password = b64_decode(headers.authorization:match"[^ ]*$"):match"([^:]*):(.*)";
43 from_jid = jid_prep(from_jid);
44 password = saslprep(password);
45 if from_jid and password then
46 local user, host = jid_split(from_jid);
47 local ok, err = test_password(user, host, password);
48 if ok and user and host then
49 return f(event, path, from_jid);
50 elseif err then
51 module:log("debug", "User failed authentication: %s", err);
52 end
53 end
54 return 401
55 end
56 end
48 57
49 local function handle_request(method, body, request) 58 local function handle_request(event, path, authed_user)
50 if not allowed_methods[method] then 59 local request, response = event.request, event.response;
51 return http_response(405, "Method Not Allowed", {["Allow"] = t_concat(allowed_methods, ", ")}); 60
61 --module:log("debug", "spliting path");
62 local path_items = {};
63 for i in string.gmatch(path, "[^/]+") do
64 t_insert(path_items, i);
52 end 65 end
66 --module:log("debug", "split path, got %d parts: %s", #path_items, table.concat(path_items, ", "));
53 67
54 if not request.headers["authorization"] then 68 local user_node, user_host = jid_split(authed_user);
55 return http_response(401, "Unauthorized", 69 if #path_items < 3 then
56 {["WWW-Authenticate"]='Basic realm="WallyWorld"'}) 70 --module:log("debug", "since we need at least 3 parts, adding %s/%s", user_host, user_node);
57 end 71 t_insert(path_items, 1, user_node);
58 local user, password = b64_decode(request.headers.authorization 72 t_insert(path_items, 1, user_host);
59 :match("[^ ]*$") or ""):match("([^:]*):(.*)");
60 user = jid_prep(user);
61 if not user or not password then return http_response(400, "Bad Request"); end
62 local user_node, user_host = jid_split(user)
63 if not hosts[user_host] then return http_response(401, "Unauthorized"); end
64
65 module:log("debug", "authz %s", user)
66 if not um_test_pw(user_node, user_host, password) then
67 return http_response(401, "Unauthorized");
68 end
69
70 module:log("debug", "spliting path");
71 local path = {};
72 for i in string.gmatch(request.url.path, "[^/]+") do
73 table.insert(path, i);
74 end
75 table.remove(path, 1); -- the first /data
76 module:log("debug", "split path, got %d parts: %s", #path, table.concat(path, ", "));
77
78 if #path < 3 then
79 module:log("debug", "since we need at least 3 parts, adding %s/%s", user_host, user_node);
80 table.insert(path, 1, user_node);
81 table.insert(path, 1, user_host);
82 --return http_response(400, "Bad Request"); 73 --return http_response(400, "Bad Request");
83 end 74 end
84 75
85 if #path < 3 then 76 if #path_items < 3 then
86 return http_response(404, "Not Found"); 77 return 404;
87 end 78 end
88 79
89 local p_host, p_user, p_store, p_type = unpack(path); 80 local p_host, p_user, p_store, p_type = unpack(path_items);
90 81
91 if not p_store or not p_store:match("^[%a_]+$") then 82 if not p_store or not p_store:match("^[%a_]+$") then
92 return http_response(404, "Not Found"); 83 return 404;
93 end 84 end
94 85
95 if user_host ~= path[1] or user_node ~= path[2] then 86 if user_host ~= path_items[1] or user_node ~= path_items[2] then
96 -- To only give admins acces to anything, move the inside of this block after authz 87 -- To only give admins acces to anything, move the inside of this block after authz
97 module:log("debug", "%s wants access to %s@%s[%s], is admin?", user, p_user, p_host, p_store) 88 --module:log("debug", "%s wants access to %s@%s[%s], is admin?", authed_user, p_user, p_host, p_store)
98 if not is_admin(user, p_host) then 89 if not is_admin(user_node, p_host) then
99 return http_response(403, "Forbidden"); 90 return 403;
100 end 91 end
101 end 92 end
102 93
94 local method = request.method;
103 if method == "GET" then 95 if method == "GET" then
104 local data = dm_load(p_user, p_host, p_store); 96 local data = dm_load(p_user, p_host, p_store);
105 97
106 data = data or dm_load_list(p_user, p_host, p_store); 98 data = data or dm_list_load(p_user, p_host, p_store);
107 99
108 --TODO Use the Accept header 100 --TODO Use the Accept header
109 content_type = p_type or "json"; 101 local content_type = p_type or "json";
110 if data and encoders[content_type] then 102 if data and encoders[content_type] then
111 return { 103 response.headers.content_type = content_type_map[content_type].."; charset=utf-8";
112 status = "200 OK", 104 return encoders[content_type](data);
113 body = encoders[content_type](data) .. "\n",
114 headers = {["content-type"] = content_type_map[content_type].."; charset=utf-8"}
115 };
116 else 105 else
117 return http_response(404, "Not Found"); 106 return 404;
118 end 107 end
119 else -- POST or PUT 108 elseif method == "POST" or method == "PUT" then
109 local body = request.body;
120 if not body then 110 if not body then
121 return http_response(400, "Bad Request") 111
112 return 400;
122 end 113 end
123 local content_type, content = request.headers["content-type"], body; 114 local content_type, content = request.headers.content_type, body;
124 content_type = content_type and content_type_map[content_type] 115 content_type = content_type and content_type_map[content_type]
125 module:log("debug", "%s: %s", content_type, tostring(content)); 116 --module:log("debug", "%s: %s", content_type, tostring(content));
126 content = content_type and decoders[content_type] and decoders[content_type](content); 117 content = content_type and decoders[content_type] and decoders[content_type](content);
127 module:log("debug", "%s: %s", type(content), tostring(content)); 118 --module:log("debug", "%s: %s", type(content), tostring(content));
128 if not content then 119 if not content then
129 return http_response(400, "Bad Request") 120 return 400;
130 end 121 end
131 local ok, err 122 local ok, err
132 if method == "PUT" then 123 if method == "PUT" then
133 ok, err = dm_store(p_user, p_host, p_store, content); 124 ok, err = dm_store(p_user, p_host, p_store, content);
134 elseif method == "POST" then 125 elseif method == "POST" then
135 ok, err = dm_list_append(p_user, p_host, p_store, content); 126 ok, err = dm_list_append(p_user, p_host, p_store, content);
136 elseif method == "DELETE" then
137 dm_store(p_user, p_host, p_store, nil);
138 dm_list_store(p_user, p_host, p_store, nil);
139 end 127 end
140 if ok then 128 if ok then
141 return http_response(201, "Created", { Location = t_concat({"/data",p_host,p_user,p_store}, "/") }); 129 response.headers.location = t_concat({module:http_url(nil,"/data"),p_host,p_user,p_store}, "/");
130 return 201;
142 else 131 else
143 return { status = "500 Internal Server Error", body = err } 132 response.headers.debug = err;
133 return 500;
144 end 134 end
135 elseif method == "DELETE" then
136 dm_store(p_user, p_host, p_store, nil);
137 dm_list_store(p_user, p_host, p_store, nil);
138 return 204;
145 end 139 end
146 end 140 end
147 141
148 local function setup() 142 local handle_request_with_auth = require_valid_user(handle_request);
149 local ports = module:get_option("data_access_ports") or { 5280 }; 143
150 require "net.httpserver".new_from_config(ports, handle_request, { base = "data" }); 144 module:provides("http", {
151 end 145 default_path = "/data";
152 if prosody.start_time then -- already started 146 route = {
153 setup(); 147 ["GET /*"] = handle_request_with_auth,
154 else 148 ["PUT /*"] = handle_request_with_auth,
155 prosody.events.add_handler("server-started", setup); 149 ["POST /*"] = handle_request_with_auth,
156 end 150 ["DELETE /*"] = handle_request_with_auth,
151 };
152 });