comparison mod_muc_cache_media/mod_muc_cache_media.lua @ 6417:dcc8d4af8d4f

mod_muc_cache_media: move to storage that matches RFC 6920
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Tue, 24 Feb 2026 11:57:09 -0500
parents 420aaf113272
children 98e06fc5680f
comparison
equal deleted inserted replaced
6416:2511bf07b8d2 6417:dcc8d4af8d4f
8 local lfs = require "lfs" 8 local lfs = require "lfs"
9 local hashes = require "util.hashes" 9 local hashes = require "util.hashes"
10 local jid = require "util.jid" 10 local jid = require "util.jid"
11 11
12 local storage_path = module:get_option_string("muc_media_store_path", "/var/lib/prosody/muc-media") 12 local storage_path = module:get_option_string("muc_media_store_path", "/var/lib/prosody/muc-media")
13 local public_base_url = module:get_option_string("muc_media_public_base", "https://cache.example.com") 13 local public_base_url = module:get_option_string("muc_media_public_base")
14 local files_have_extensions = module:get_option_boolean("muc_media_files_have_extensions", true)
15 local max_size = module:get_option_number("muc_media_max_size", 10 * 1024 * 1024) 14 local max_size = module:get_option_number("muc_media_max_size", 10 * 1024 * 1024)
16 15
17 lfs.mkdir(storage_path) 16 lfs.mkdir(storage_path)
18 17 lfs.mkdir(storage_path.."/sha-256")
19 local mime_extensions = { 18 lfs.mkdir(storage_path.."/sha3-256")
20 ["image/jpeg"] = ".jpg",
21 ["image/jpg"] = ".jpg",
22 ["image/png"] = ".png",
23 ["image/gif"] = ".gif",
24 ["image/webp"] = ".webp",
25 ["image/avif"] = ".avif",
26 ["video/mp4"] = ".mp4",
27 ["video/webm"] = ".webm",
28 ["audio/mpeg"] = ".mp3",
29 ["audio/ogg"] = ".ogg",
30 }
31
32 local function extension_from_content_type(content_type)
33 if not content_type then return ".bin" end
34 local clean = content_type:match("^[^;]+")
35 clean = clean and clean:lower()
36 return mime_extensions[clean] or ".bin"
37 end
38 19
39 local function ipv4_to_number(ip) 20 local function ipv4_to_number(ip)
40 local a,b,c,d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") 21 local a,b,c,d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
41 if not a then return nil end 22 if not a then return nil end
42 return (tonumber(a) << 24) | (tonumber(b) << 16) | (tonumber(c) << 8) | tonumber(d) 23 return (tonumber(a) << 24) | (tonumber(b) << 16) | (tonumber(c) << 8) | tonumber(d)
158 if not err and tmp_path then 139 if not err and tmp_path then
159 local sha2 = sha256_file(tmp_path) 140 local sha2 = sha256_file(tmp_path)
160 local sha3 = sha3_256_file(tmp_path) 141 local sha3 = sha3_256_file(tmp_path)
161 local size = file_size(tmp_path) 142 local size = file_size(tmp_path)
162 local name = filename_from_url(original_url) 143 local name = filename_from_url(original_url)
163 if sha2 then 144 if sha2 and sha3 then
164 local ext = extension_from_content_type(content_type) 145 local final_name = "sha3-256/"..sha3:gsub("+","-"):gsub("/","_"):gsub("=","")
165 local file_name = sha2:gsub("+","-"):gsub("/","_"):gsub("=","") 146 local final_path = storage_path.."/"..final_name
166 local url_name = file_name..ext
167 if files_have_extensions then
168 file_name = file_name..ext
169 end
170 local final_path = storage_path.."/"..file_name
171 if not lfs.attributes(final_path) then 147 if not lfs.attributes(final_path) then
172 os.rename(tmp_path, final_path) 148 os.rename(tmp_path, final_path)
173 else 149 else
150 lfs.touch(final_path)
174 os.remove(tmp_path) 151 os.remove(tmp_path)
175 end 152 end
176 url_map[original_url] = { local_url = public_base_url.."/"..url_name, path=final_path, sha2=sha2, sha3=sha3, size=size, name=name, content_type=content_type } 153 local sha2_path = storage_path.."/sha-256/"..sha2:gsub("+","-"):gsub("/","_"):gsub("=","")
154 if not lfs.attributes(sha2_path) then
155 lfs.link(final_path, sha2_path, true)
156 end
157 url_map[original_url] = { local_url = public_base_url..final_name.."?ct="..content_type, path=final_path, sha2=sha2, sha3=sha3, size=size, name=name, content_type=content_type }
177 else 158 else
178 os.remove(tmp_path) 159 os.remove(tmp_path)
179 end 160 end
180 end 161 end
181 end 162 end
245 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) 226 file_tag:add_child(st.stanza("size"):text(tostring(data.size)))
246 end 227 end
247 -- Fill SHA256 hash 228 -- Fill SHA256 hash
248 local sha2_tag = nil 229 local sha2_tag = nil
249 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do 230 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do
250 if htag.attr.algo=="sha2-256" then sha2_tag = htag end 231 if htag.attr.algo=="sha-256" then sha2_tag = htag end
251 end 232 end
252 if not sha2_tag then 233 if not sha2_tag then
253 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha2-256" }):text(data.sha2)) 234 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha-256" }):text(data.sha2))
254 end 235 end
255 if data.sha3 then 236 -- Fill SHA3-256 hash
256 -- Fill SHA3-256 hash 237 local sha3_tag = nil
257 local sha3_tag = nil 238 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do
258 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do 239 if htag.attr.algo=="sha3-256" then sha3_tag = htag end
259 if htag.attr.algo=="sha3-256" then sha3_tag = htag end 240 end
260 end 241 if not sha3_tag then
261 if not sha3_tag then 242 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3))
262 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3))
263 end
264 end 243 end
265 end 244 end
266 end 245 end
267 end 246 end
268 end 247 end
273 local ms = st.stanza("media-sharing",{ xmlns="urn:xmpp:sims:1" }) 252 local ms = st.stanza("media-sharing",{ xmlns="urn:xmpp:sims:1" })
274 local file_tag = st.stanza("file",{ xmlns="urn:xmpp:jingle:apps:file-transfer:5" }) 253 local file_tag = st.stanza("file",{ xmlns="urn:xmpp:jingle:apps:file-transfer:5" })
275 file_tag:add_child(st.stanza("media-type"):text(data.content_type or "application/octet-stream")) 254 file_tag:add_child(st.stanza("media-type"):text(data.content_type or "application/octet-stream"))
276 file_tag:add_child(st.stanza("name"):text(data.name)) 255 file_tag:add_child(st.stanza("name"):text(data.name))
277 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) 256 file_tag:add_child(st.stanza("size"):text(tostring(data.size)))
278 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha2-256" }):text(data.sha2)) 257 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha-256" }):text(data.sha2))
279 if data.sha3 then file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3)) end 258 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3))
280 ms:add_child(file_tag) 259 ms:add_child(file_tag)
281 local sources = st.stanza("sources") 260 local sources = st.stanza("sources")
282 sources:add_child(st.stanza("reference",{ xmlns="urn:xmpp:reference:0", type="data", uri=data.local_url })) 261 sources:add_child(st.stanza("reference",{ xmlns="urn:xmpp:reference:0", type="data", uri=data.local_url }))
283 ms:add_child(sources) 262 ms:add_child(sources)
284 ref:add_child(ms) 263 ref:add_child(ms)
308 287
309 for id, stanza, when in items do 288 for id, stanza, when in items do
310 -- Extract SIMS hashes 289 -- Extract SIMS hashes
311 for ref in stanza:childtags("reference", "urn:xmpp:reference:0") do 290 for ref in stanza:childtags("reference", "urn:xmpp:reference:0") do
312 for ms in ref:childtags("media-sharing", "urn:xmpp:sims:1") do 291 for ms in ref:childtags("media-sharing", "urn:xmpp:sims:1") do
313 local file_tag = ms:get_child("file", "urn:xmpp:jingle:apps:file-transfer:5") 292 for file_tag in ms:childtags("file", "urn:xmpp:jingle:apps:file-transfer:5") do
314 if file_tag then
315 for hash_tag in file_tag:childtags("hash", "urn:xmpp:hashes:2") do 293 for hash_tag in file_tag:childtags("hash", "urn:xmpp:hashes:2") do
316 if hash_tag.attr.algo == "sha2-256" then 294 if hash_tag.attr.algo == "sha3-256" then
317 local sha2 = hash_tag:get_text() 295 local sha3 = hash_tag:get_text()
318 if sha2 then 296 if sha3 then
319 local prefix = sha2:gsub("+","-"):gsub("/","_"):gsub("=","") 297 local file = sha3:gsub("+","-"):gsub("/","_"):gsub("=","")
320 if files_have_extensions then 298 local path = storage_path .. "/sha3-256/" .. file
321 -- Files are named: hash + extension 299 if lfs.attributes(path) then
322 for file in lfs.dir(storage_path) do 300 os.remove(path)
323 if file:match("^" .. prefix) then 301 module:log("debug",
324 local path = storage_path .. "/" .. file 302 "Deleted cached media %s for moderated message %s",
325 if lfs.attributes(path) then 303 file, stanza_id
326 os.remove(path) 304 )
327 module:log("debug",
328 "Deleted cached media %s for moderated message %s",
329 file, stanza_id
330 )
331 end
332 end
333 end
334 else
335 local path = storage_path .. "/" .. prefix
336 if lfs.attributes(path) then
337 os.remove(path)
338 module:log("debug",
339 "Deleted cached media %s for moderated message %s",
340 file, stanza_id
341 )
342 end
343 end 305 end
344 end 306 end
345 end 307 end
346 end 308 end
347 end 309 end