Software / code / prosody
Comparison
plugins/mod_http_files.lua @ 11200:bf8f2da84007
Merge 0.11->trunk
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Thu, 05 Nov 2020 22:31:25 +0100 |
| parent | 10778:a62b981db0e2 |
| child | 12387:05c250fa335a |
comparison
equal
deleted
inserted
replaced
| 11199:6c7c50a4de32 | 11200:bf8f2da84007 |
|---|---|
| 5 -- This project is MIT/X11 licensed. Please see the | 5 -- This project is MIT/X11 licensed. Please see the |
| 6 -- COPYING file in the source package for more information. | 6 -- COPYING file in the source package for more information. |
| 7 -- | 7 -- |
| 8 | 8 |
| 9 module:depends("http"); | 9 module:depends("http"); |
| 10 local server = require"net.http.server"; | |
| 11 local lfs = require "lfs"; | |
| 12 | 10 |
| 13 local os_date = os.date; | |
| 14 local open = io.open; | 11 local open = io.open; |
| 15 local stat = lfs.attributes; | 12 local fileserver = require"net.http.files"; |
| 16 local build_path = require"socket.url".build_path; | |
| 17 local path_sep = package.config:sub(1,1); | |
| 18 | 13 |
| 19 local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path")); | 14 local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path")); |
| 20 local cache_size = module:get_option_number("http_files_cache_size", 128); | 15 local cache_size = module:get_option_number("http_files_cache_size", 128); |
| 21 local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096); | 16 local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096); |
| 22 local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" }); | 17 local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" }); |
| 36 svg = "image/svg+xml", | 31 svg = "image/svg+xml", |
| 37 }; | 32 }; |
| 38 module:shared("/*/http_files/mime").types = mime_map; | 33 module:shared("/*/http_files/mime").types = mime_map; |
| 39 | 34 |
| 40 local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r"); | 35 local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r"); |
| 41 if mime_types then | 36 if not mime_types then |
| 37 module:log("debug", "Could not open MIME database: %s", err); | |
| 38 else | |
| 42 local mime_data = mime_types:read("*a"); | 39 local mime_data = mime_types:read("*a"); |
| 43 mime_types:close(); | 40 mime_types:close(); |
| 44 setmetatable(mime_map, { | 41 setmetatable(mime_map, { |
| 45 __index = function(t, ext) | 42 __index = function(t, ext) |
| 46 local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; | 43 local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; |
| 49 end | 46 end |
| 50 }); | 47 }); |
| 51 end | 48 end |
| 52 end | 49 end |
| 53 | 50 |
| 54 local forbidden_chars_pattern = "[/%z]"; | 51 local function get_calling_module() |
| 55 if prosody.platform == "windows" then | 52 local info = debug.getinfo(3, "S"); |
| 56 forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" | 53 if not info then return "An unknown module"; end |
| 54 return info.source:match"mod_[^/\\.]+" or info.short_src; | |
| 57 end | 55 end |
| 58 | 56 |
| 59 local urldecode = require "util.http".urldecode; | 57 -- COMPAT -- TODO deprecate |
| 60 function sanitize_path(path) | |
| 61 if not path then return end | |
| 62 local out = {}; | |
| 63 | |
| 64 local c = 0; | |
| 65 for component in path:gmatch("([^/]+)") do | |
| 66 component = urldecode(component); | |
| 67 if component:find(forbidden_chars_pattern) then | |
| 68 return nil; | |
| 69 elseif component == ".." then | |
| 70 if c <= 0 then | |
| 71 return nil; | |
| 72 end | |
| 73 out[c] = nil; | |
| 74 c = c - 1; | |
| 75 elseif component ~= "." then | |
| 76 c = c + 1; | |
| 77 out[c] = component; | |
| 78 end | |
| 79 end | |
| 80 if path:sub(-1,-1) == "/" then | |
| 81 out[c+1] = ""; | |
| 82 end | |
| 83 return "/"..table.concat(out, "/"); | |
| 84 end | |
| 85 | |
| 86 local cache = require "util.cache".new(cache_size); | |
| 87 | |
| 88 function serve(opts) | 58 function serve(opts) |
| 89 if type(opts) ~= "table" then -- assume path string | 59 if type(opts) ~= "table" then -- assume path string |
| 90 opts = { path = opts }; | 60 opts = { path = opts }; |
| 91 end | 61 end |
| 92 -- luacheck: ignore 431 | 62 if opts.directory_index == nil then |
| 93 local base_path = opts.path; | 63 opts.directory_index = directory_index; |
| 94 local dir_indices = opts.index_files or dir_indices; | |
| 95 local directory_index = opts.directory_index; | |
| 96 local function serve_file(event, path) | |
| 97 local request, response = event.request, event.response; | |
| 98 local sanitized_path = sanitize_path(path); | |
| 99 if path and not sanitized_path then | |
| 100 return 400; | |
| 101 end | |
| 102 path = sanitized_path; | |
| 103 local orig_path = sanitize_path(request.path); | |
| 104 local full_path = base_path .. (path or ""):gsub("/", path_sep); | |
| 105 local attr = stat(full_path:match("^.*[^\\/]")); -- Strip trailing path separator because Windows | |
| 106 if not attr then | |
| 107 return 404; | |
| 108 end | |
| 109 | |
| 110 local request_headers, response_headers = request.headers, response.headers; | |
| 111 | |
| 112 local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification); | |
| 113 response_headers.last_modified = last_modified; | |
| 114 | |
| 115 local etag = ('"%x-%x-%x"'):format(attr.change or 0, attr.size or 0, attr.modification or 0); | |
| 116 response_headers.etag = etag; | |
| 117 | |
| 118 local if_none_match = request_headers.if_none_match | |
| 119 local if_modified_since = request_headers.if_modified_since; | |
| 120 if etag == if_none_match | |
| 121 or (not if_none_match and last_modified == if_modified_since) then | |
| 122 return 304; | |
| 123 end | |
| 124 | |
| 125 local data = cache:get(orig_path); | |
| 126 if data and data.etag == etag then | |
| 127 response_headers.content_type = data.content_type; | |
| 128 data = data.data; | |
| 129 elseif attr.mode == "directory" and path then | |
| 130 if full_path:sub(-1) ~= "/" then | |
| 131 local dir_path = { is_absolute = true, is_directory = true }; | |
| 132 for dir in orig_path:gmatch("[^/]+") do dir_path[#dir_path+1]=dir; end | |
| 133 response_headers.location = build_path(dir_path); | |
| 134 return 301; | |
| 135 end | |
| 136 for i=1,#dir_indices do | |
| 137 if stat(full_path..dir_indices[i], "mode") == "file" then | |
| 138 return serve_file(event, path..dir_indices[i]); | |
| 139 end | |
| 140 end | |
| 141 | |
| 142 if directory_index then | |
| 143 data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); | |
| 144 end | |
| 145 if not data then | |
| 146 return 403; | |
| 147 end | |
| 148 cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; }); | |
| 149 response_headers.content_type = mime_map.html; | |
| 150 | |
| 151 else | |
| 152 local f, err = open(full_path, "rb"); | |
| 153 if not f then | |
| 154 module:log("debug", "Could not open %s. Error was %s", full_path, err); | |
| 155 return 403; | |
| 156 end | |
| 157 local ext = full_path:match("%.([^./]+)$"); | |
| 158 local content_type = ext and mime_map[ext]; | |
| 159 response_headers.content_type = content_type; | |
| 160 if attr.size > cache_max_file_size then | |
| 161 response_headers.content_length = attr.size; | |
| 162 module:log("debug", "%d > cache_max_file_size", attr.size); | |
| 163 return response:send_file(f); | |
| 164 else | |
| 165 data = f:read("*a"); | |
| 166 f:close(); | |
| 167 end | |
| 168 cache:set(orig_path, { data = data; content_type = content_type; etag = etag }); | |
| 169 end | |
| 170 | |
| 171 return response:send(data); | |
| 172 end | 64 end |
| 173 | 65 if opts.mime_map == nil then |
| 174 return serve_file; | 66 opts.mime_map = mime_map; |
| 67 end | |
| 68 if opts.cache_size == nil then | |
| 69 opts.cache_size = cache_size; | |
| 70 end | |
| 71 if opts.cache_max_file_size == nil then | |
| 72 opts.cache_max_file_size = cache_max_file_size; | |
| 73 end | |
| 74 if opts.index_files == nil then | |
| 75 opts.index_files = dir_indices; | |
| 76 end | |
| 77 -- TODO Crank up to warning | |
| 78 module:log("debug", "%s should be updated to use 'net.http.files' insead of mod_http_files", get_calling_module()); | |
| 79 return fileserver.serve(opts); | |
| 175 end | 80 end |
| 176 | 81 |
| 177 function wrap_route(routes) | 82 function wrap_route(routes) |
| 83 module:log("debug", "%s should be updated to use 'net.http.files' insead of mod_http_files", get_calling_module()); | |
| 178 for route,handler in pairs(routes) do | 84 for route,handler in pairs(routes) do |
| 179 if type(handler) ~= "function" then | 85 if type(handler) ~= "function" then |
| 180 routes[route] = serve(handler); | 86 routes[route] = fileserver.serve(handler); |
| 181 end | 87 end |
| 182 end | 88 end |
| 183 return routes; | 89 return routes; |
| 184 end | 90 end |
| 185 | 91 |
| 186 if base_path then | 92 module:provides("http", { |
| 187 module:provides("http", { | 93 route = { |
| 188 route = { | 94 ["GET /*"] = fileserver.serve({ |
| 189 ["GET /*"] = serve { | 95 path = base_path; |
| 190 path = base_path; | 96 directory_index = directory_index; |
| 191 directory_index = directory_index; | 97 mime_map = mime_map; |
| 192 } | 98 cache_size = cache_size; |
| 193 }; | 99 cache_max_file_size = cache_max_file_size; |
| 194 }); | 100 index_files = dir_indices; |
| 195 else | 101 }); |
| 196 module:log("debug", "http_files_dir not set, assuming use by some other module"); | 102 }; |
| 197 end | 103 }); |
| 198 |