Comparison

plugins/mod_http_file_share.lua @ 11309:b59aed75dc5e

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
author Kim Alvefur <zash@zash.se>
date Tue, 26 Jan 2021 03:19:17 +0100
child 11310:d1a0f2e918c0
comparison
equal deleted inserted replaced
11308:5d4d90d1eabb 11309:b59aed75dc5e
1 -- Prosody IM
2 -- Copyright (C) 2021 Kim Alvefur
3 --
4 -- This project is MIT/X11 licensed. Please see the
5 -- COPYING file in the source package for more information.
6 --
7 -- XEP-0363: HTTP File Upload
8 -- Again, from the top!
9
10 local t_insert = table.insert;
11 local jid = require "util.jid";
12 local st = require "util.stanza";
13 local url = require "socket.url";
14 local dm = require "core.storagemanager".olddm;
15 local jwt = require "util.jwt";
16 local errors = require "util.error";
17
18 local namespace = "urn:xmpp:http:upload:0";
19
20 module:depends("http");
21 module:depends("disco");
22
23 module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload"));
24 module:add_feature(namespace);
25
26 local uploads = module:open_store("uploads", "archive");
27 -- id, <request>, time, owner
28
29 local secret = module:get_option_string(module.name.."_secret", require"util.id".long());
30
31 function may_upload(uploader, filename, filesize, filetype) -- > boolean, error
32 -- TODO authz
33 return true;
34 end
35
36 function get_authz(uploader, filename, filesize, filetype, slot)
37 return "Bearer "..jwt.sign(secret, {
38 sub = uploader;
39 filename = filename;
40 filesize = filesize;
41 filetype = filetype;
42 slot = slot;
43 exp = os.time()+300;
44 });
45 end
46
47 function get_url(slot, filename)
48 local base_url = module:http_url();
49 local slot_url = url.parse(base_url);
50 slot_url.path = url.parse_path(slot_url.path or "/");
51 t_insert(slot_url.path, slot);
52 if filename then
53 t_insert(slot_url.path, filename);
54 slot_url.path.is_directory = false;
55 else
56 slot_url.path.is_directory = true;
57 end
58 slot_url.path = url.build_path(slot_url.path);
59 return url.build(slot_url);
60 end
61
62 function handle_slot_request(event)
63 local stanza, origin = event.stanza, event.origin;
64
65 local request = st.clone(stanza.tags[1], true);
66 local filename = request.attr.filename;
67 local filesize = tonumber(request.attr.size);
68 local filetype = request.attr["content-type"];
69 local uploader = jid.bare(stanza.attr.from);
70
71 local may, why_not = may_upload(uploader, filename, filesize, filetype);
72 if not may then
73 origin.send(st.error_reply(stanza, why_not));
74 return true;
75 end
76
77 local slot, storage_err = errors.coerce(uploads:append(nil, nil, request, os.time(), uploader))
78 if not slot then
79 origin.send(st.error_reply(stanza, storage_err));
80 return true;
81 end
82
83 local authz = get_authz(uploader, filename, filesize, filetype, slot);
84 local slot_url = get_url(slot, filename);
85 local upload_url = slot_url;
86
87 local reply = st.reply(stanza)
88 :tag("slot", { xmlns = namespace })
89 :tag("get", { url = slot_url }):up()
90 :tag("put", { url = upload_url })
91 :text_tag("header", authz, {name="Authorization"})
92 :reset();
93
94 origin.send(reply);
95 return true;
96 end
97
98 function handle_upload(event, path) -- PUT /upload/:slot
99 local request = event.request;
100 local authz = request.headers.authorization;
101 if not authz or not authz:find"^Bearer ." then
102 return 403;
103 end
104 local authed, upload_info = jwt.verify(secret, authz:match("^Bearer (.*)"));
105 if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then
106 return 401;
107 end
108 if upload_info.exp < os.time() then
109 return 410;
110 end
111 if not path or upload_info.slot ~= path:match("^[^/]+") then
112 return 400;
113 end
114
115 local filename = dm.getpath(upload_info.slot, module.host, module.name, nil, true);
116
117 if not request.body_sink then
118 local fh, err = errors.coerce(io.open(filename.."~", "w"));
119 if not fh then
120 return err;
121 end
122 request.body_sink = fh;
123 if request.body == false then
124 return true;
125 end
126 end
127
128 if request.body then
129 local written, err = errors.coerce(request.body_sink:write(request.body));
130 if not written then
131 return err;
132 end
133 request.body = nil;
134 end
135
136 if request.body_sink then
137 local uploaded, err = errors.coerce(request.body_sink:close());
138 if uploaded then
139 assert(os.rename(filename.."~", filename));
140 return 201;
141 else
142 assert(os.remove(filename.."~"));
143 return err;
144 end
145 end
146
147 end
148
149 function handle_download(event, path) -- GET /uploads/:slot+filename
150 local request, response = event.request, event.response;
151 local slot_id = path:match("^[^/]+");
152 -- TODO cache
153 local slot, when = errors.coerce(uploads:get(nil, slot_id));
154 if not slot then
155 module:log("debug", "uploads:get(%q) --> not-found, %s", slot_id, when);
156 return 404;
157 end
158 module:log("debug", "uploads:get(%q) --> %s, %d", slot_id, slot, when);
159 local last_modified = os.date('!%a, %d %b %Y %H:%M:%S GMT', when);
160 if request.headers.if_modified_since == last_modified then
161 return 304;
162 end
163 local filename = dm.getpath(slot_id, module.host, module.name);
164 local handle, ferr = errors.coerce(io.open(filename));
165 if not handle then
166 return ferr or 410;
167 end
168 response.headers.last_modified = last_modified;
169 response.headers.content_length = slot.attr.size;
170 response.headers.content_type = slot.attr["content-type"];
171 response.headers.content_disposition = string.format("attachment; filename=%q", slot.attr.filename);
172
173 response.headers.cache_control = "max-age=31556952, immutable";
174 response.headers.content_security_policy = "default-src 'none'; frame-ancestors 'none';"
175
176 return response:send_file(handle);
177 -- TODO
178 -- Set security headers
179 end
180
181 -- TODO periodic cleanup job
182
183 module:hook("iq-get/host/urn:xmpp:http:upload:0:request", handle_slot_request);
184
185 module:provides("http", {
186 streaming_uploads = true;
187 route = {
188 ["PUT /*"] = handle_upload;
189 ["GET /*"] = handle_download;
190 }
191 });