Software /
code /
prosody-modules
Changeset
6026:37c77676303b
mod_http_upload: Obsolete this module, it is superseded by mod_http_file_share included in Prosody since 0.12
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Sun, 27 Oct 2024 13:16:34 +0100 |
parents | 6025:a5fff4995862 |
children | 6027:98951dab5851 |
files | mod_http_upload/README.md mod_http_upload/mod_http_upload.lua |
diffstat | 2 files changed, 2 insertions(+), 553 deletions(-) [+] |
line wrap: on
line diff
--- a/mod_http_upload/README.md Sun Oct 27 13:14:25 2024 +0100 +++ b/mod_http_upload/README.md Sun Oct 27 13:16:34 2024 +0100 @@ -1,111 +1,10 @@ --- description: HTTP File Upload +superseded_by: mod_http_file_share labels: -- Stage-Alpha +- Stage-Obsolete --- -Introduction -============ - -This module implements [XEP-0363], versions 0.2 and 0.3, which let -clients upload files over HTTP. - -Configuration -============= - -mod\_http\_upload relies on Prosodys HTTP server and mod\_http for -serving HTTP requests. See [Prosodys HTTP server documentation][doc:http] -for information about how to configure ports, HTTP Host names etc. - -The module can be added as a new Component definition: - -``` {.lua} -Component "upload.example.org" "http_upload" -``` - -It should **not** be added to modules_enabled. - -## Discoverability - -Prosody makes subdomains of your VirtualHosts easily discoverable by -clients. To make the component discoverable by other hosts where the -component is **not a subdomain** of the VirtualHost, you can use -[`disco_items`][doc:modules:mod_disco#configuration]. - -``` {.lua} -VirtualHost "foo.example.org" -disco_items = { - { "upload.example.com" }, -} -``` - -## Access - -You may want to give upload access to additional entities such as components -by using the `http_upload_access` config option. - -``` {.lua} -http_upload_access = {"gateway.example.com"}; -``` - -Limits ------- - -### Max size - -A maximum file size can be set by: - -``` {.lua} -http_upload_file_size_limit = 123 -- bytes -``` - -Default is 1MB (1024\*1024). - -This can not be set over the value of `http_max_content_size` (default 10M). -Consider [mod_http_upload_external] instead of attempting to increase -this limit. - -### Max age - -Files can be set to be deleted after some time: - -``` lua -http_upload_expire_after = 60 * 60 * 24 * 7 -- a week in seconds -``` - -Expired files are deleted when a new upload slot is requested, - -A command exists to invoke expiry: - -``` -prosodyctl mod_http_upload expire [list of users or hosts] -``` - -### User quota - -A total maximum size of all uploaded files per user can be set by: - -``` lua -http_upload_quota = 1234 -- bytes -``` - -A request for a slot that would take an user over quota is denied. - -Path ----- - -By default, uploaded files are put in a sub-directory of the default -Prosody storage path (usually `/var/lib/prosody`). This can be changed: - -``` {.lua} -http_upload_path = "/path/to/uploaded/files" -``` - -Compatibility -============= - -Works with Prosody 0.11.x and later. - In Prosody 0.12 and later, consider switching to [mod_http_file_share](https://prosody.im/doc/modules/mod_http_file_share) which is distributed with Prosody. You can migrate existing files using [mod_migrate_http_upload](https://modules.prosody.im/mod_migrate_http_upload.html).
--- a/mod_http_upload/mod_http_upload.lua Sun Oct 27 13:14:25 2024 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,450 +0,0 @@ --- mod_http_upload --- --- Copyright (C) 2015-2018 Kim Alvefur --- --- This file is MIT/X11 licensed. --- --- Implementation of HTTP Upload file transfer mechanism used by Conversations --- - --- depends -module:depends("http"); -module:depends("disco"); - -if module:http_url():match("^http://") then - error("File upload MUST happen with TLS but it isn’t enabled, see https://prosody.im/doc/http for how to fix this issue"); -end - --- imports -local st = require"util.stanza"; -local lfs = require"lfs"; -local url = require "socket.url"; -local dataform = require "util.dataforms".new; -local datamanager = require "util.datamanager"; -local array = require "util.array"; -local t_concat = table.concat; -local t_insert = table.insert; -local s_upper = string.upper; -local httpserver = require "net.http.server"; -local have_id, id = pcall(require, "util.id"); -- Only available in 0.10+ -local uuid = require"util.uuid".generate; -local jid = require "util.jid"; -if have_id then - uuid = id.medium; -end - -local function join_path(...) -- COMPAT util.path was added in 0.10 - return table.concat({ ... }, package.config:sub(1,1)); -end - --- config -local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 1024 * 1024); -- 1 MB -local quota = module:get_option_number(module.name .. "_quota"); -local max_age = module:get_option_number(module.name .. "_expire_after"); -local access = module:get_option_set(module.name .. "_access", {}); - ---- sanity -local parser_body_limit = module:context("*"):get_option_number("http_max_content_size", 10*1024*1024); -if file_size_limit > parser_body_limit then - module:log("warn", "%s_file_size_limit exceeds HTTP parser limit on body size, capping file size to %d B", - module.name, parser_body_limit); - file_size_limit = parser_body_limit; -end - -if prosody.hosts[module.host].type == "local" then - module:log("warn", "mod_%s loaded on a user host, this may be incompatible with some client software, see docs for correct usage", module.name); -end - -local http_files; - -if prosody.process_type == "prosody" then - http_files = require "net.http.files"; -else - http_files = module:depends"http_files"; -end - -local mime_map = module:shared("/*/http_files/mime").types; -if not mime_map then - mime_map = { - html = "text/html", htm = "text/html", - xml = "application/xml", - txt = "text/plain", - css = "text/css", - js = "application/javascript", - png = "image/png", - gif = "image/gif", - jpeg = "image/jpeg", jpg = "image/jpeg", - svg = "image/svg+xml", - }; - module:shared("/*/http_files/mime").types = mime_map; - - local mime_types, err = io.open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r"); - if mime_types then - local mime_data = mime_types:read("*a"); - mime_types:close(); - setmetatable(mime_map, { - __index = function(t, ext) - local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; - t[ext] = typ; - return typ; - end - }); - end -end - --- namespaces -local namespace = "urn:xmpp:http:upload:0"; -local legacy_namespace = "urn:xmpp:http:upload"; - --- identity and feature advertising -module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload")); -module:add_feature(namespace); -module:add_feature(legacy_namespace); - -module:add_extension(dataform { - { name = "FORM_TYPE", type = "hidden", value = namespace }, - { name = "max-file-size", type = "text-single" }, -}:form({ ["max-file-size"] = ("%d"):format(file_size_limit) }, "result")); - -module:add_extension(dataform { - { name = "FORM_TYPE", type = "hidden", value = legacy_namespace }, - { name = "max-file-size", type = "text-single" }, -}:form({ ["max-file-size"] = ("%d"):format(file_size_limit) }, "result")); - --- state -local pending_slots = module:shared("upload_slots"); - -local storage_path = module:get_option_string(module.name .. "_path", join_path(prosody.paths.data, module.name)); -lfs.mkdir(storage_path); - -local function expire(username, host) - if not max_age then return true; end - local uploads, err = datamanager.list_load(username, host, module.name); - if err then return false, err; end - if not uploads then return true; end - uploads = array(uploads); - local expiry = os.time() - max_age; - local upload_window = os.time() - 900; - local before = #uploads; - uploads:filter(function (item) - local filename = item.filename; - if item.dir then - filename = join_path(storage_path, item.dir, item.filename); - end - if item.time < expiry then - local deleted, whynot = os.remove(filename); - if not deleted then - module:log("warn", "Could not delete expired upload %s: %s", filename, whynot or "delete failed"); - end - os.remove(filename:match("^(.*)[/\\]")); - return false; - elseif item.time < upload_window and not lfs.attributes(filename) then - return false; -- File was not uploaded or has been deleted since - end - return true; - end); - local after = #uploads; - if before == after then return true end -- nothing changed, skip write - return datamanager.list_store(username, host, module.name, uploads); -end - -local function check_quota(username, host, does_it_fit) - if not quota then return true; end - local uploads, err = datamanager.list_load(username, host, module.name); - if err then - return false; - elseif not uploads then - if does_it_fit then - return does_it_fit < quota; - end - return true; - end - local sum = does_it_fit or 0; - for _, item in ipairs(uploads) do - sum = sum + item.size; - end - return sum < quota; -end - -local measure_slot = function () end -if module.measure then - -- COMPAT 0.9 - -- module:measure was added in 0.10 - measure_slot = module:measure("slot", "sizes"); -end - -local function handle_request(origin, stanza, xmlns, filename, filesize) - local username, host = origin.username, origin.host; - - local user_bare = jid.bare(stanza.attr.from); - local user_host = jid.host(user_bare); - - -- local clients or whitelisted jids/hosts only - if not (origin.type == "c2s" or access:contains(user_bare) or access:contains(user_host)) then - module:log("debug", "Request for upload slot from a %s", origin.type); - return nil, st.error_reply(stanza, "cancel", "not-authorized"); - end - -- validate - if not filename or filename:find("/") then - module:log("debug", "Filename %q not allowed", filename or ""); - return nil, st.error_reply(stanza, "modify", "bad-request", "Invalid filename"); - end - expire(username, host); - if not filesize then - module:log("debug", "Missing file size"); - return nil, st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size"); - elseif filesize > file_size_limit then - module:log("debug", "File too large (%d > %d)", filesize, file_size_limit); - return nil, st.error_reply(stanza, "modify", "not-acceptable", "File too large") - :tag("file-too-large", {xmlns=xmlns}) - :tag("max-file-size"):text(("%d"):format(file_size_limit)); - elseif not check_quota(username, host, filesize) then - module:log("debug", "Upload of %dB by %s would exceed quota", filesize, user_bare); - return nil, st.error_reply(stanza, "wait", "resource-constraint", "Quota reached"); - end - - local random_dir = uuid(); - local created, err = lfs.mkdir(join_path(storage_path, random_dir)); - - if not created then - module:log("error", "Could not create directory for slot: %s", err); - return nil, st.error_reply(stanza, "wait", "internal-server-error"); - end - - local ok = datamanager.list_append(username, host, module.name, { - filename = filename, dir = random_dir, size = filesize, time = os.time() }); - - if not ok then - return nil, st.error_reply(stanza, "wait", "internal-server-error"); - end - - local slot = random_dir.."/"..filename; - pending_slots[slot] = user_bare; - - module:add_timer(900, function() - pending_slots[slot] = nil; - end); - - measure_slot(filesize); - - origin.log("debug", "Given upload slot %q", slot); - - local base_url = module:http_url(); - local slot_url = url.parse(base_url); - slot_url.path = url.parse_path(slot_url.path or "/"); - t_insert(slot_url.path, random_dir); - t_insert(slot_url.path, filename); - slot_url.path.is_directory = false; - slot_url.path = url.build_path(slot_url.path); - slot_url = url.build(slot_url); - return slot_url; -end - --- hooks -module:hook("iq/host/"..namespace..":request", function (event) - local stanza, origin = event.stanza, event.origin; - local request = stanza.tags[1]; - local filename = request.attr.filename; - local filesize = tonumber(request.attr.size); - - local slot_url, err = handle_request(origin, stanza, namespace, filename, filesize); - if not slot_url then - origin.send(err); - return true; - end - - local reply = st.reply(stanza) - :tag("slot", { xmlns = namespace }) - :tag("get", { url = slot_url }):up() - :tag("put", { url = slot_url }):up() - :up(); - origin.send(reply); - return true; -end); - -module:hook("iq/host/"..legacy_namespace..":request", function (event) - local stanza, origin = event.stanza, event.origin; - local request = stanza.tags[1]; - local filename = request:get_child_text("filename"); - local filesize = tonumber(request:get_child_text("size")); - - local slot_url, err = handle_request(origin, stanza, legacy_namespace, filename, filesize); - if not slot_url then - origin.send(err); - return true; - end - - local reply = st.reply(stanza) - :tag("slot", { xmlns = legacy_namespace }) - :tag("get"):text(slot_url):up() - :tag("put"):text(slot_url):up() - :up(); - origin.send(reply); - return true; -end); - -local measure_upload = function () end -if module.measure then - -- COMPAT 0.9 - -- module:measure was added in 0.10 - measure_upload = module:measure("upload", "sizes"); -end - --- http service -local function set_cross_domain_headers(response) - local headers = response.headers; - headers.access_control_allow_methods = "GET, PUT, OPTIONS"; - headers.access_control_allow_headers = "Content-Type"; - headers.access_control_max_age = "7200"; - headers.access_control_allow_origin = response.request.headers.origin or "*"; - return response; -end - -local function upload_data(event, path) - set_cross_domain_headers(event.response); - - local uploader = pending_slots[path]; - if not uploader then - module:log("warn", "Attempt to upload to unknown slot %q", path); - return; -- 404 - end - local random_dir, filename = path:match("^([^/]+)/([^/]+)$"); - if not random_dir then - module:log("warn", "Invalid file path %q", path); - return 400; - end - if #event.request.body > file_size_limit then - module:log("warn", "Uploaded file too large %d bytes", #event.request.body); - return 400; - end - pending_slots[path] = nil; - local full_filename = join_path(storage_path, random_dir, filename); - if lfs.attributes(full_filename) then - module:log("warn", "File %s exists already, not replacing it", full_filename); - return 409; - end - local fh, ferr = io.open(full_filename, "w"); - if not fh then - module:log("error", "Could not open file %s for upload: %s", full_filename, ferr); - return 500; - end - local ok, err = fh:write(event.request.body); - if not ok then - module:log("error", "Could not write to file %s for upload: %s", full_filename, err); - os.remove(full_filename); - return 500; - end - ok, err = fh:close(); - if not ok then - module:log("error", "Could not write to file %s for upload: %s", full_filename, err); - os.remove(full_filename); - return 500; - end - measure_upload(#event.request.body); - module:log("info", "File uploaded by %s to slot %s", uploader, random_dir); - return 201; -end - --- FIXME Duplicated from net.http.server - -local codes = require "net.http.codes"; -local headerfix = setmetatable({}, { - __index = function(t, k) - local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": "; - t[k] = v; - return v; - end -}); - -local function send_response_sans_body(response, body) - if response.finished then return; end - response.finished = true; - response.conn._http_open_response = nil; - - local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]); - local headers = response.headers; - if type(body) == "string" then - headers.content_length = #body; - elseif io.type(body) == "file" then - headers.content_length = body:seek("end"); - body:close(); - end - - local output = { status_line }; - for k,v in pairs(headers) do - t_insert(output, headerfix[k]..v); - end - t_insert(output, "\r\n\r\n"); - -- Here we *don't* add the body to the output - - response.conn:write(t_concat(output)); - if response.on_destroy then - response:on_destroy(); - response.on_destroy = nil; - end - if response.persistent then - response:finish_cb(); - else - response.conn:close(); - end -end - -local serve_uploaded_files = http_files.serve({ path = storage_path, mime_map = mime_map }); - -local function serve_head(event, path) - set_cross_domain_headers(event.response); - event.response.send = send_response_sans_body; - event.response.send_file = send_response_sans_body; - return serve_uploaded_files(event, path); -end - -if httpserver.send_head_response then - -- Prosody will take care of HEAD requests since hg:3f4c25425589 - serve_head = nil -end - -local function serve_hello(event) - event.response.headers.content_type = "text/html;charset=utf-8" - return "<!DOCTYPE html>\n<h1>Hello from mod_"..module.name.." on "..module.host.."!</h1>\n"; -end - -module:provides("http", { - route = { - ["GET"] = serve_hello; - ["GET /"] = serve_hello; - ["GET /*"] = serve_uploaded_files; - ["HEAD /*"] = serve_head; - ["PUT /*"] = upload_data; - - ["OPTIONS /*"] = function (event) - set_cross_domain_headers(event.response); - return ""; - end; - }; -}); - -module:log("info", "URL: <%s> - Ensure this can be reached by users", module:http_url()); -module:log("info", "Storage path: '%s'", storage_path); - -function module.command(args) - datamanager = require "core.storagemanager".olddm; - -- luacheck: ignore 421/user - if args[1] == "expire" and args[2] then - local split = require "util.jid".prepped_split; - for i = 2, #args do - local user, host = split(args[i]); - if user then - assert(expire(user, host)); - else - for user in assert(datamanager.users(host, module.name, "list")) do - expire(user, host); - end - end - end - else - print("prosodyctl mod_http_upload expire [host or user@host]+") - print("\tProcess upload expiry for the given list of hosts and/or users"); - return 1; - end -end -