Software /
code /
prosody
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 }); |