Mercurial > prosody-modules
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 |
