Mercurial > prosody-hg
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 |
| parents | |
| children | 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 }); |
