aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKim Alvefur <zash@zash.se>2021-01-26 03:19:17 +0100
committerKim Alvefur <zash@zash.se>2021-01-26 03:19:17 +0100
commit5b1f88524210c73ab0a91c1f0d4d493e78707bb7 (patch)
treed1dfc3940b89b83640520cbe0b924e6273936d77
parente91db8d93543bf2a3a4ba2bff73ef8be55e029eb (diff)
downloadprosody-5b1f88524210c73ab0a91c1f0d4d493e78707bb7.tar.gz
prosody-5b1f88524210c73ab0a91c1f0d4d493e78707bb7.zip
mod_http_file_share: Let's write another XEP-0363 implementation
This variant is meant to improve upon mod_http_upload in some ways: * Handle files much of arbitrary size efficiently * Allow GET and PUT URLs to be different * Remember Content-Type sent by client * Avoid dependency on mod_http_files * Built-in way to delegate storage to another httpd
-rw-r--r--CHANGES1
-rw-r--r--doc/doap.xml9
-rw-r--r--plugins/mod_http_file_share.lua191
-rw-r--r--spec/scansion/http_upload.scs26
-rw-r--r--spec/scansion/prosody.cfg.lua2
5 files changed, 229 insertions, 0 deletions
diff --git a/CHANGES b/CHANGES
index f80cb9b0..592ecdef 100644
--- a/CHANGES
+++ b/CHANGES
@@ -20,6 +20,7 @@ TRUNK
- mod_external_services (XEP-0215)
- util.error for encapsulating errors
- MUC: support for XEP-0421 occupant identifiers
+- mod_http_file_share: File sharing via HTTP (XEP-0363)
0.11.0
======
diff --git a/doc/doap.xml b/doc/doap.xml
index f0538580..277aa5d1 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -617,6 +617,15 @@
</implements>
<implements>
<xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/>
+ <xmpp:version>1.0.0</xmpp:version>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:since>0.12.0</xmpp:since>
+ <xmpp:note>mod_http_file_share</xmpp:note>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/>
<xmpp:version>1.1.0</xmpp:version>
<xmpp:status>partial</xmpp:status>
diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua
new file mode 100644
index 00000000..0cb610d9
--- /dev/null
+++ b/plugins/mod_http_file_share.lua
@@ -0,0 +1,191 @@
+-- Prosody IM
+-- Copyright (C) 2021 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0363: HTTP File Upload
+-- Again, from the top!
+
+local t_insert = table.insert;
+local jid = require "util.jid";
+local st = require "util.stanza";
+local url = require "socket.url";
+local dm = require "core.storagemanager".olddm;
+local jwt = require "util.jwt";
+local errors = require "util.error";
+
+local namespace = "urn:xmpp:http:upload:0";
+
+module:depends("http");
+module:depends("disco");
+
+module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload"));
+module:add_feature(namespace);
+
+local uploads = module:open_store("uploads", "archive");
+-- id, <request>, time, owner
+
+local secret = module:get_option_string(module.name.."_secret", require"util.id".long());
+
+function may_upload(uploader, filename, filesize, filetype) -- > boolean, error
+ -- TODO authz
+ return true;
+end
+
+function get_authz(uploader, filename, filesize, filetype, slot)
+ return "Bearer "..jwt.sign(secret, {
+ sub = uploader;
+ filename = filename;
+ filesize = filesize;
+ filetype = filetype;
+ slot = slot;
+ exp = os.time()+300;
+ });
+end
+
+function get_url(slot, filename)
+ 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, slot);
+ if filename then
+ t_insert(slot_url.path, filename);
+ slot_url.path.is_directory = false;
+ else
+ slot_url.path.is_directory = true;
+ end
+ slot_url.path = url.build_path(slot_url.path);
+ return url.build(slot_url);
+end
+
+function handle_slot_request(event)
+ local stanza, origin = event.stanza, event.origin;
+
+ local request = st.clone(stanza.tags[1], true);
+ local filename = request.attr.filename;
+ local filesize = tonumber(request.attr.size);
+ local filetype = request.attr["content-type"];
+ local uploader = jid.bare(stanza.attr.from);
+
+ local may, why_not = may_upload(uploader, filename, filesize, filetype);
+ if not may then
+ origin.send(st.error_reply(stanza, why_not));
+ return true;
+ end
+
+ local slot, storage_err = errors.coerce(uploads:append(nil, nil, request, os.time(), uploader))
+ if not slot then
+ origin.send(st.error_reply(stanza, storage_err));
+ return true;
+ end
+
+ local authz = get_authz(uploader, filename, filesize, filetype, slot);
+ local slot_url = get_url(slot, filename);
+ local upload_url = slot_url;
+
+ local reply = st.reply(stanza)
+ :tag("slot", { xmlns = namespace })
+ :tag("get", { url = slot_url }):up()
+ :tag("put", { url = upload_url })
+ :text_tag("header", authz, {name="Authorization"})
+ :reset();
+
+ origin.send(reply);
+ return true;
+end
+
+function handle_upload(event, path) -- PUT /upload/:slot
+ local request = event.request;
+ local authz = request.headers.authorization;
+ if not authz or not authz:find"^Bearer ." then
+ return 403;
+ end
+ local authed, upload_info = jwt.verify(secret, authz:match("^Bearer (.*)"));
+ if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then
+ return 401;
+ end
+ if upload_info.exp < os.time() then
+ return 410;
+ end
+ if not path or upload_info.slot ~= path:match("^[^/]+") then
+ return 400;
+ end
+
+ local filename = dm.getpath(upload_info.slot, module.host, module.name, nil, true);
+
+ if not request.body_sink then
+ local fh, err = errors.coerce(io.open(filename.."~", "w"));
+ if not fh then
+ return err;
+ end
+ request.body_sink = fh;
+ if request.body == false then
+ return true;
+ end
+ end
+
+ if request.body then
+ local written, err = errors.coerce(request.body_sink:write(request.body));
+ if not written then
+ return err;
+ end
+ request.body = nil;
+ end
+
+ if request.body_sink then
+ local uploaded, err = errors.coerce(request.body_sink:close());
+ if uploaded then
+ assert(os.rename(filename.."~", filename));
+ return 201;
+ else
+ assert(os.remove(filename.."~"));
+ return err;
+ end
+ end
+
+end
+
+function handle_download(event, path) -- GET /uploads/:slot+filename
+ local request, response = event.request, event.response;
+ local slot_id = path:match("^[^/]+");
+ -- TODO cache
+ local slot, when = errors.coerce(uploads:get(nil, slot_id));
+ if not slot then
+ module:log("debug", "uploads:get(%q) --> not-found, %s", slot_id, when);
+ return 404;
+ end
+ module:log("debug", "uploads:get(%q) --> %s, %d", slot_id, slot, when);
+ local last_modified = os.date('!%a, %d %b %Y %H:%M:%S GMT', when);
+ if request.headers.if_modified_since == last_modified then
+ return 304;
+ end
+ local filename = dm.getpath(slot_id, module.host, module.name);
+ local handle, ferr = errors.coerce(io.open(filename));
+ if not handle then
+ return ferr or 410;
+ end
+ response.headers.last_modified = last_modified;
+ response.headers.content_length = slot.attr.size;
+ response.headers.content_type = slot.attr["content-type"];
+ response.headers.content_disposition = string.format("attachment; filename=%q", slot.attr.filename);
+
+ response.headers.cache_control = "max-age=31556952, immutable";
+ response.headers.content_security_policy = "default-src 'none'; frame-ancestors 'none';"
+
+ return response:send_file(handle);
+ -- TODO
+ -- Set security headers
+end
+
+-- TODO periodic cleanup job
+
+module:hook("iq-get/host/urn:xmpp:http:upload:0:request", handle_slot_request);
+
+module:provides("http", {
+ streaming_uploads = true;
+ route = {
+ ["PUT /*"] = handle_upload;
+ ["GET /*"] = handle_download;
+ }
+ });
diff --git a/spec/scansion/http_upload.scs b/spec/scansion/http_upload.scs
new file mode 100644
index 00000000..a683483e
--- /dev/null
+++ b/spec/scansion/http_upload.scs
@@ -0,0 +1,26 @@
+[Client] Romeo
+ password: password
+ jid: filesharingenthusiast@localhost/krxLaE3s
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq to='upload.localhost' type='get' id='932c02fe-4461-4ad4-9c85-54863294b4dc' xml:lang='en'>
+ <request content-type='text/plain' filename='verysmall.dat' xmlns='urn:xmpp:http:upload:0' size='5'/>
+ </iq>
+
+Romeo receives:
+ <iq id='932c02fe-4461-4ad4-9c85-54863294b4dc' from='upload.localhost' type='result'>
+ <slot xmlns='urn:xmpp:http:upload:0'>
+ <get url='{scansion:any}'/>
+ <put url='{scansion:any}'>
+ <header name='Authorization'></header>
+ </put>
+ </slot>
+ </iq>
+
+Romeo disconnects
+
+# recording ended on 2021-01-27T22:10:46Z
diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua
index d147db54..ca28b6ba 100644
--- a/spec/scansion/prosody.cfg.lua
+++ b/spec/scansion/prosody.cfg.lua
@@ -131,3 +131,5 @@ Component "conference.localhost" "muc"
Component "pubsub.localhost" "pubsub"
storage = "memory"
+
+Component "upload.localhost" "http_file_share"