comparison mod_http_upload/mod_http_upload.lua @ 2702:caabb980d1d8

Merge commit
author tmolitor <thilo@eightysoft.de>
date Mon, 24 Apr 2017 20:57:23 +0200
parents 785465f8af3d
children d48faff92490
comparison
equal deleted inserted replaced
2701:d96831e46b64 2702:caabb980d1d8
1 -- mod_http_upload 1 -- mod_http_upload
2 -- 2 --
3 -- Copyright (C) 2015 Kim Alvefur 3 -- Copyright (C) 2015-2017 Kim Alvefur
4 -- 4 --
5 -- This file is MIT/X11 licensed. 5 -- This file is MIT/X11 licensed.
6 -- 6 --
7 -- Implementation of HTTP Upload file transfer mechanism used by Conversations 7 -- Implementation of HTTP Upload file transfer mechanism used by Conversations
8 -- 8 --
11 local st = require"util.stanza"; 11 local st = require"util.stanza";
12 local lfs = require"lfs"; 12 local lfs = require"lfs";
13 local url = require "socket.url"; 13 local url = require "socket.url";
14 local dataform = require "util.dataforms".new; 14 local dataform = require "util.dataforms".new;
15 local datamanager = require "util.datamanager"; 15 local datamanager = require "util.datamanager";
16 local array = require "util.array";
16 local t_concat = table.concat; 17 local t_concat = table.concat;
17 local t_insert = table.insert; 18 local t_insert = table.insert;
18 local s_upper = string.upper; 19 local s_upper = string.upper;
19 local have_id, id = pcall(require, "util.id"); -- Only available in 0.10+ 20 local have_id, id = pcall(require, "util.id"); -- Only available in 0.10+
20 local uuid = require"util.uuid".generate; 21 local uuid = require"util.uuid".generate;
26 return table.concat({ ... }, package.config:sub(1,1)); 27 return table.concat({ ... }, package.config:sub(1,1));
27 end 28 end
28 29
29 -- config 30 -- config
30 local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 1024 * 1024); -- 1 MB 31 local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 1024 * 1024); -- 1 MB
32 local quota = module:get_option_number(module.name .. "_quota");
33 local max_age = module:get_option_number(module.name .. "_expire_after");
34 local allowed_file_types = module:get_option_set(module.name .. "_allowed_file_types");
31 35
32 --- sanity 36 --- sanity
33 local parser_body_limit = module:context("*"):get_option_number("http_max_content_size", 10*1024*1024); 37 local parser_body_limit = module:context("*"):get_option_number("http_max_content_size", 10*1024*1024);
34 if file_size_limit > parser_body_limit then 38 if file_size_limit > parser_body_limit then
35 module:log("warn", "%s_file_size_limit exceeds HTTP parser limit on body size, capping file size to %d B", 39 module:log("warn", "%s_file_size_limit exceeds HTTP parser limit on body size, capping file size to %d B",
39 43
40 -- depends 44 -- depends
41 module:depends("http"); 45 module:depends("http");
42 module:depends("disco"); 46 module:depends("disco");
43 47
48 local http_files = module:depends("http_files");
49 local mime_map = module:shared("/*/http_files/mime").types;
50
44 -- namespaces 51 -- namespaces
45 local namespace = "urn:xmpp:http:upload:0"; 52 local namespace = "urn:xmpp:http:upload:0";
46 local legacy_namespace = "urn:xmpp:http:upload"; 53 local legacy_namespace = "urn:xmpp:http:upload";
47 54
48 -- identity and feature advertising 55 -- identity and feature advertising
64 local pending_slots = module:shared("upload_slots"); 71 local pending_slots = module:shared("upload_slots");
65 72
66 local storage_path = module:get_option_string(module.name .. "_path", join_path(prosody.paths.data, module.name)); 73 local storage_path = module:get_option_string(module.name .. "_path", join_path(prosody.paths.data, module.name));
67 lfs.mkdir(storage_path); 74 lfs.mkdir(storage_path);
68 75
69 local function handle_request(origin, stanza, xmlns, filename, filesize) 76 local function expire(username, host)
77 if not max_age then return true; end
78 local uploads, err = datamanager.list_load(username, host, module.name);
79 if not uploads then return true; end
80 uploads = array(uploads);
81 local expiry = os.time() - max_age;
82 local upload_window = os.time() - 900;
83 uploads:filter(function (item)
84 local filename = item.filename;
85 if item.dir then
86 filename = join_path(storage_path, item.dir, item.filename);
87 end
88 if item.time < expiry then
89 local deleted, whynot = os.remove(filename);
90 if not deleted then
91 module:log("warn", "Could not delete expired upload %s: %s", filename, whynot or "delete failed");
92 end
93 return false;
94 elseif item.time < upload_window and not lfs.attributes(filename) then
95 return false; -- File was not uploaded or has been deleted since
96 end
97 return true;
98 end);
99 return datamanager.list_store(username, host, module.name, uploads);
100 end
101
102 local function check_quota(username, host, does_it_fit)
103 if not quota then return true; end
104 local uploads, err = datamanager.list_load(username, host, module.name);
105 if not uploads then return true; end
106 local sum = does_it_fit or 0;
107 for _, item in ipairs(uploads) do
108 sum = sum + item.size;
109 end
110 return sum < quota;
111 end
112
113 local function handle_request(origin, stanza, xmlns, filename, filesize, mimetype)
114 local username, host = origin.username, origin.host;
70 -- local clients only 115 -- local clients only
71 if origin.type ~= "c2s" then 116 if origin.type ~= "c2s" then
72 module:log("debug", "Request for upload slot from a %s", origin.type); 117 module:log("debug", "Request for upload slot from a %s", origin.type);
73 origin.send(st.error_reply(stanza, "cancel", "not-authorized")); 118 origin.send(st.error_reply(stanza, "cancel", "not-authorized"));
74 return true; 119 return true;
77 if not filename or filename:find("/") then 122 if not filename or filename:find("/") then
78 module:log("debug", "Filename %q not allowed", filename or ""); 123 module:log("debug", "Filename %q not allowed", filename or "");
79 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid filename")); 124 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid filename"));
80 return true; 125 return true;
81 end 126 end
127 expire(username, host);
82 if not filesize then 128 if not filesize then
83 module:log("debug", "Missing file size"); 129 module:log("debug", "Missing file size");
84 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size")); 130 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size"));
85 return true; 131 return true;
86 elseif filesize > file_size_limit then 132 elseif filesize > file_size_limit then
87 module:log("debug", "File too large (%d > %d)", filesize, file_size_limit); 133 module:log("debug", "File too large (%d > %d)", filesize, file_size_limit);
88 origin.send(st.error_reply(stanza, "modify", "not-acceptable", "File too large") 134 origin.send(st.error_reply(stanza, "modify", "not-acceptable", "File too large")
89 :tag("file-too-large", {xmlns=xmlns}) 135 :tag("file-too-large", {xmlns=xmlns})
90 :tag("max-file-size"):text(tostring(file_size_limit))); 136 :tag("max-file-size"):text(tostring(file_size_limit)));
91 return true; 137 return true;
92 end 138 elseif not check_quota(username, host, filesize) then
139 module:log("debug", "Upload of %dB by %s would exceed quota", filesize, origin.full_jid);
140 origin.send(st.error_reply(stanza, "wait", "resource-constraint", "Quota reached"));
141 return true;
142 end
143
144 if mime_map then
145 local file_ext = filename:match("%.([^.]+)$");
146 if not mimetype then
147 mimetype = "application/octet-stream";
148 if file_ext then
149 mimetype = mime_map[file_ext] or mimetype;
150 end
151 else
152 if (not file_ext and mimetype ~= "application/octet-stream") or (file_ext and mime_map[file_ext] ~= mimetype) then
153 origin.send(st.error_reply(stanza, "modify", "bad-request", "MIME type does not match file extension"));
154 return true;
155 end
156 end
157 end
158
159 if allowed_file_types then
160 if not (allowed_file_types:contains(mimetype) or allowed_file_types:contains(mimetype:gsub("/.*", "/*"))) then
161 origin.send(st.error_reply(stanza, "cancel", "not-allowed", "File type not allowed"));
162 return true;
163 end
164 end
165
93 local reply = st.reply(stanza); 166 local reply = st.reply(stanza);
94 reply:tag("slot", { xmlns = xmlns }); 167 reply:tag("slot", { xmlns = xmlns });
95 168
96 local random_dir; 169 local random_dir;
97 repeat random_dir = uuid(); 170 repeat random_dir = uuid();
98 until lfs.mkdir(join_path(storage_path, random_dir)) 171 until lfs.mkdir(join_path(storage_path, random_dir))
99 or not lfs.attributes(join_path(storage_path, random_dir, filename)) 172 or not lfs.attributes(join_path(storage_path, random_dir, filename))
100 173
101 datamanager.list_append(origin.username, origin.host, module.name, { 174 local ok = datamanager.list_append(username, host, module.name, {
102 filename = join_path(storage_path, random_dir, filename), size = filesize, time = os.time() }); 175 filename = filename, dir = random_dir, size = filesize, time = os.time() });
176
177 if not ok then
178 origin.send(st.error_reply(stanza, "wait", "internal-server-failure"));
179 return true;
180 end
181
103 local slot = random_dir.."/"..filename; 182 local slot = random_dir.."/"..filename;
104 pending_slots[slot] = origin.full_jid; 183 pending_slots[slot] = origin.full_jid;
184
185 module:add_timer(900, function()
186 pending_slots[slot] = nil;
187 end);
188
105 local base_url = module:http_url(); 189 local base_url = module:http_url();
106 local slot_url = url.parse(base_url); 190 local slot_url = url.parse(base_url);
107 slot_url.path = url.parse_path(slot_url.path or "/"); 191 slot_url.path = url.parse_path(slot_url.path or "/");
108 t_insert(slot_url.path, random_dir); 192 t_insert(slot_url.path, random_dir);
109 t_insert(slot_url.path, filename); 193 t_insert(slot_url.path, filename);
121 module:hook("iq/host/"..namespace..":request", function (event) 205 module:hook("iq/host/"..namespace..":request", function (event)
122 local stanza, origin = event.stanza, event.origin; 206 local stanza, origin = event.stanza, event.origin;
123 local request = stanza.tags[1]; 207 local request = stanza.tags[1];
124 local filename = request.attr.filename; 208 local filename = request.attr.filename;
125 local filesize = tonumber(request.attr.size); 209 local filesize = tonumber(request.attr.size);
126 return handle_request(origin, stanza, namespace, filename, filesize); 210 local mimetype = request.attr["content-type"];
211 return handle_request(origin, stanza, namespace, filename, filesize, mimetype);
127 end); 212 end);
128 213
129 module:hook("iq/host/"..legacy_namespace..":request", function (event) 214 module:hook("iq/host/"..legacy_namespace..":request", function (event)
130 local stanza, origin = event.stanza, event.origin; 215 local stanza, origin = event.stanza, event.origin;
131 local request = stanza.tags[1]; 216 local request = stanza.tags[1];
132 local filename = request:get_child_text("filename"); 217 local filename = request:get_child_text("filename");
133 local filesize = tonumber(request:get_child_text("size")); 218 local filesize = tonumber(request:get_child_text("size"));
134 return handle_request(origin, stanza, legacy_namespace, filename, filesize); 219 local mimetype = request:get_child_text("content-type");
220 return handle_request(origin, stanza, legacy_namespace, filename, filesize, mimetype);
135 end); 221 end);
136 222
137 -- http service 223 -- http service
138 local function upload_data(event, path) 224 local function upload_data(event, path)
139 local uploader = pending_slots[path]; 225 local uploader = pending_slots[path];
215 else 301 else
216 response.conn:close(); 302 response.conn:close();
217 end 303 end
218 end 304 end
219 305
220 local serve_uploaded_files = module:depends("http_files").serve(storage_path); 306 local serve_uploaded_files = http_files.serve(storage_path);
221 307
222 local function serve_head(event, path) 308 local function serve_head(event, path)
223 event.response.send = send_response_sans_body; 309 event.response.send = send_response_sans_body;
224 return serve_uploaded_files(event, path); 310 return serve_uploaded_files(event, path);
225 end 311 end