Mercurial > prosody-modules
comparison mod_muc_cache_media/mod_muc_cache_media.lua @ 6414:48fca0914fcf
mod_muc_cache_media: simplify code with target_filter
| author | Stephen Paul Weber <singpolyma@singpolyma.net> |
|---|---|
| date | Mon, 23 Feb 2026 12:41:36 -0500 |
| parents | c25b98d42ed0 |
| children | 420aaf113272 |
comparison
equal
deleted
inserted
replaced
| 6413:0703ef9f8db8 | 6414:48fca0914fcf |
|---|---|
| 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", "https://cache.example.com") |
| 14 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) |
| 15 local MAX_REDIRECTS = 5 | |
| 16 | 15 |
| 17 lfs.mkdir(storage_path) | 16 lfs.mkdir(storage_path) |
| 18 | 17 |
| 19 local mime_extensions = { | 18 local mime_extensions = { |
| 20 ["image/jpeg"] = ".jpg", | 19 ["image/jpeg"] = ".jpg", |
| 64 if ip:match("^fe8") or ip:match("^fe9") or ip:match("^fea") or ip:match("^feb") then return true end | 63 if ip:match("^fe8") or ip:match("^fe9") or ip:match("^fea") or ip:match("^feb") then return true end |
| 65 if ip == "::" then return true end | 64 if ip == "::" then return true end |
| 66 return false | 65 return false |
| 67 end | 66 end |
| 68 | 67 |
| 69 local function is_safe_host(host) | |
| 70 if not host then return false end | |
| 71 host = host:lower() | |
| 72 if host == "localhost" then return false end | |
| 73 local records = socket_dns.getaddrinfo(host) | |
| 74 if not records then return false end | |
| 75 for _, r in ipairs(records) do | |
| 76 local ip = r.addr | |
| 77 if ip:match("^%d+%.%d+%.%d+%.%d+$") then | |
| 78 if is_private_ipv4(ip) then return false end | |
| 79 else | |
| 80 if is_private_ipv6(ip) then return false end | |
| 81 end | |
| 82 end | |
| 83 return true | |
| 84 end | |
| 85 | |
| 86 local function sha256_file(path) | 68 local function sha256_file(path) |
| 87 local f = io.open(path,"rb") | 69 local f = io.open(path,"rb") |
| 88 if not f then return nil end | 70 if not f then return nil end |
| 89 -- TODO: incremental hash | 71 -- TODO: incremental hash |
| 90 local digest = hashes.sha256(f:read("*all")) | 72 local digest = hashes.sha256(f:read("*all")) |
| 107 | 89 |
| 108 local function filename_from_url(url) | 90 local function filename_from_url(url) |
| 109 return url:match(".*/([^/?]+)$") or "file" | 91 return url:match(".*/([^/?]+)$") or "file" |
| 110 end | 92 end |
| 111 | 93 |
| 112 local function resolve_redirects(url) | 94 local function secure_download_to_disk(url) |
| 95 local parsed = socket_url.parse(url) | |
| 96 if not parsed or (parsed.scheme~="http" and parsed.scheme~="https") then return promise.reject("invalid scheme") end | |
| 97 | |
| 98 local tmpname = storage_path.."/tmp_"..tostring(math.random(1e9)) | |
| 99 local file = io.open(tmpname,"wb") | |
| 100 if not file then return promise.reject("cannot open temp file") end | |
| 101 | |
| 113 return promise.new(function(resolve, reject) | 102 return promise.new(function(resolve, reject) |
| 114 local current_url = url | 103 http.request(url,{ |
| 115 local redirects = 0 | 104 method = "GET"; |
| 116 | 105 body_size_limit = max_size; |
| 117 local function step() | 106 streaming_handler = function(r) return file, function() file:close(); file = nil end end; |
| 118 if redirects > MAX_REDIRECTS then reject("too many redirects"); return end | 107 target_filter = function(net, ip, port, extra) |
| 119 http.request(current_url,{ | 108 return (net == "tcp4" and not is_private_ipv4(ip)) or (net == "tcp6" and not is_private_ipv6(ip)) |
| 120 method="HEAD"; | 109 end; |
| 121 redirect=false; | 110 }, function(_, code, response) |
| 122 }, function(_, code, response) | 111 if code ~= 200 then |
| 123 if not code or code == 0 then reject("request failed"); return end | 112 if file then file:close() end |
| 124 if code>=300 and code<400 and response.headers.location then | 113 os.remove(tmpname) |
| 125 redirects = redirects + 1 | 114 reject("bad status") |
| 126 local location = response.headers.location | 115 return |
| 127 local parsed = socket_url.parse(current_url) | 116 end |
| 128 if not location:match("^https?://") then | 117 if file then |
| 129 location = parsed.scheme.."://"..parsed.host..(parsed.port and ":"..parsed.port or "")..location | 118 file:write(response.body) |
| 130 end | 119 file:close() |
| 131 current_url = location | 120 end |
| 132 step() | 121 resolve({ tmpname, response.headers["content-type"] }) |
| 133 return | |
| 134 end | |
| 135 resolve(current_url) | |
| 136 end) | |
| 137 end | |
| 138 | |
| 139 step() | |
| 140 end) | |
| 141 end | |
| 142 | |
| 143 local function all_safe(resolv, didnext, net, ip) | |
| 144 if didnext and not ip then | |
| 145 return promise.resolve(true) | |
| 146 end | |
| 147 | |
| 148 if not didnext or (net == "tcp4" and not is_private_ipv4(ip)) | |
| 149 or (net == "tcp6" and not is_private_ipv6(ip)) then | |
| 150 return promise.new(function(resolve) | |
| 151 resolv:next(function(net,ip,port,extra) | |
| 152 all_safe(resolv, true, net, ip):next(resolve) | |
| 153 end) | |
| 154 end) | |
| 155 else | |
| 156 return promise.resolve(false) | |
| 157 end | |
| 158 end | |
| 159 | |
| 160 local function secure_download_to_disk(original_url) | |
| 161 return resolve_redirects(original_url):next(function(final_url) | |
| 162 local parsed = socket_url.parse(final_url) | |
| 163 if not parsed or (parsed.scheme~="http" and parsed.scheme~="https") then return promise.reject("invalid scheme") end | |
| 164 | |
| 165 return all_safe(resolver.new(parsed.host, 443, "tcp", { servername = parsed.host })):next(function(is_all_safe) | |
| 166 if not is_all_safe then return promise.reject("no safe IP") end | |
| 167 | |
| 168 local tmpname = storage_path.."/tmp_"..tostring(math.random(1e9)) | |
| 169 local file = io.open(tmpname,"wb") | |
| 170 if not file then return promise.reject("cannot open temp file") end | |
| 171 | |
| 172 return promise.new(function(resolve, reject) | |
| 173 http.request(final_url,{ | |
| 174 method = "GET"; | |
| 175 redirect = false; | |
| 176 body_size_limit = max_size; | |
| 177 streaming_handler = function(r) return file, function() file:close(); file = nil end end; | |
| 178 }, function(_, code, response) | |
| 179 if code ~= 200 then | |
| 180 if file then file:close() end | |
| 181 os.remove(tmpname) | |
| 182 reject("bad status") | |
| 183 return | |
| 184 end | |
| 185 if file then | |
| 186 file:write(response.body) | |
| 187 file:close() | |
| 188 end | |
| 189 resolve({ tmpname, response.headers["content-type"] }) | |
| 190 end) | |
| 191 end) | |
| 192 end) | 122 end) |
| 193 end) | 123 end) |
| 194 end | 124 end |
| 195 | 125 |
| 196 module:hook("muc-broadcast-message", function(event) | 126 module:hook("muc-broadcast-message", function(event) |
