|
|
|
|
@ -7,14 +7,9 @@ |
|
|
|
|
-- |
|
|
|
|
|
|
|
|
|
module:depends("http"); |
|
|
|
|
local server = require"net.http.server"; |
|
|
|
|
local lfs = require "lfs"; |
|
|
|
|
|
|
|
|
|
local os_date = os.date; |
|
|
|
|
local open = io.open; |
|
|
|
|
local stat = lfs.attributes; |
|
|
|
|
local build_path = require"socket.url".build_path; |
|
|
|
|
local path_sep = package.config:sub(1,1); |
|
|
|
|
local fileserver = require"net.http.files"; |
|
|
|
|
|
|
|
|
|
local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path")); |
|
|
|
|
local cache_size = module:get_option_number("http_files_cache_size", 128); |
|
|
|
|
@ -51,148 +46,50 @@ if not mime_map then |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
local forbidden_chars_pattern = "[/%z]"; |
|
|
|
|
if prosody.platform == "windows" then |
|
|
|
|
forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
local urldecode = require "util.http".urldecode; |
|
|
|
|
function sanitize_path(path) |
|
|
|
|
if not path then return end |
|
|
|
|
local out = {}; |
|
|
|
|
|
|
|
|
|
local c = 0; |
|
|
|
|
for component in path:gmatch("([^/]+)") do |
|
|
|
|
component = urldecode(component); |
|
|
|
|
if component:find(forbidden_chars_pattern) then |
|
|
|
|
return nil; |
|
|
|
|
elseif component == ".." then |
|
|
|
|
if c <= 0 then |
|
|
|
|
return nil; |
|
|
|
|
end |
|
|
|
|
out[c] = nil; |
|
|
|
|
c = c - 1; |
|
|
|
|
elseif component ~= "." then |
|
|
|
|
c = c + 1; |
|
|
|
|
out[c] = component; |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
if path:sub(-1,-1) == "/" then |
|
|
|
|
out[c+1] = ""; |
|
|
|
|
end |
|
|
|
|
return "/"..table.concat(out, "/"); |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
local cache = require "util.cache".new(cache_size); |
|
|
|
|
|
|
|
|
|
-- COMPAT -- TODO deprecate |
|
|
|
|
function serve(opts) |
|
|
|
|
if type(opts) ~= "table" then -- assume path string |
|
|
|
|
opts = { path = opts }; |
|
|
|
|
end |
|
|
|
|
-- luacheck: ignore 431 |
|
|
|
|
local base_path = opts.path; |
|
|
|
|
local dir_indices = opts.index_files or dir_indices; |
|
|
|
|
local directory_index = opts.directory_index; |
|
|
|
|
local function serve_file(event, path) |
|
|
|
|
local request, response = event.request, event.response; |
|
|
|
|
local sanitized_path = sanitize_path(path); |
|
|
|
|
if path and not sanitized_path then |
|
|
|
|
return 400; |
|
|
|
|
end |
|
|
|
|
path = sanitized_path; |
|
|
|
|
local orig_path = sanitize_path(request.path); |
|
|
|
|
local full_path = base_path .. (path or ""):gsub("/", path_sep); |
|
|
|
|
local attr = stat(full_path:match("^.*[^\\/]")); -- Strip trailing path separator because Windows |
|
|
|
|
if not attr then |
|
|
|
|
return 404; |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
local request_headers, response_headers = request.headers, response.headers; |
|
|
|
|
|
|
|
|
|
local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification); |
|
|
|
|
response_headers.last_modified = last_modified; |
|
|
|
|
|
|
|
|
|
local etag = ('"%02x-%x-%x-%x"'):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0); |
|
|
|
|
response_headers.etag = etag; |
|
|
|
|
|
|
|
|
|
local if_none_match = request_headers.if_none_match |
|
|
|
|
local if_modified_since = request_headers.if_modified_since; |
|
|
|
|
if etag == if_none_match |
|
|
|
|
or (not if_none_match and last_modified == if_modified_since) then |
|
|
|
|
return 304; |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
local data = cache:get(orig_path); |
|
|
|
|
if data and data.etag == etag then |
|
|
|
|
response_headers.content_type = data.content_type; |
|
|
|
|
data = data.data; |
|
|
|
|
elseif attr.mode == "directory" and path then |
|
|
|
|
if full_path:sub(-1) ~= "/" then |
|
|
|
|
local dir_path = { is_absolute = true, is_directory = true }; |
|
|
|
|
for dir in orig_path:gmatch("[^/]+") do dir_path[#dir_path+1]=dir; end |
|
|
|
|
response_headers.location = build_path(dir_path); |
|
|
|
|
return 301; |
|
|
|
|
end |
|
|
|
|
for i=1,#dir_indices do |
|
|
|
|
if stat(full_path..dir_indices[i], "mode") == "file" then |
|
|
|
|
return serve_file(event, path..dir_indices[i]); |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
if directory_index then |
|
|
|
|
data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); |
|
|
|
|
end |
|
|
|
|
if not data then |
|
|
|
|
return 403; |
|
|
|
|
end |
|
|
|
|
cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; }); |
|
|
|
|
response_headers.content_type = mime_map.html; |
|
|
|
|
|
|
|
|
|
else |
|
|
|
|
local f, err = open(full_path, "rb"); |
|
|
|
|
if not f then |
|
|
|
|
module:log("debug", "Could not open %s. Error was %s", full_path, err); |
|
|
|
|
return 403; |
|
|
|
|
end |
|
|
|
|
local ext = full_path:match("%.([^./]+)$"); |
|
|
|
|
local content_type = ext and mime_map[ext]; |
|
|
|
|
response_headers.content_type = content_type; |
|
|
|
|
if attr.size > cache_max_file_size then |
|
|
|
|
response_headers.content_length = attr.size; |
|
|
|
|
module:log("debug", "%d > cache_max_file_size", attr.size); |
|
|
|
|
return response:send_file(f); |
|
|
|
|
else |
|
|
|
|
data = f:read("*a"); |
|
|
|
|
f:close(); |
|
|
|
|
end |
|
|
|
|
cache:set(orig_path, { data = data; content_type = content_type; etag = etag }); |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
return response:send(data); |
|
|
|
|
if opts.directory_index == nil then |
|
|
|
|
opts.directory_index = directory_index; |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
return serve_file; |
|
|
|
|
if opts.mime_map == nil then |
|
|
|
|
opts.mime_map = mime_map; |
|
|
|
|
end |
|
|
|
|
if opts.cache_size == nil then |
|
|
|
|
opts.cache_size = cache_size; |
|
|
|
|
end |
|
|
|
|
if opts.cache_max_file_size == nil then |
|
|
|
|
opts.cache_max_file_size = cache_max_file_size; |
|
|
|
|
end |
|
|
|
|
if opts.index_files == nil then |
|
|
|
|
opts.index_files = dir_indices; |
|
|
|
|
end |
|
|
|
|
-- TODO Crank up to warning |
|
|
|
|
module:log("debug", "Use of mod_http_files.serve() should be updated to use net.http.files"); |
|
|
|
|
return fileserver.serve(opts); |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
function wrap_route(routes) |
|
|
|
|
module:log("debug", "Use of mod_http_files.wrap_route() should be updated to use net.http.files"); |
|
|
|
|
for route,handler in pairs(routes) do |
|
|
|
|
if type(handler) ~= "function" then |
|
|
|
|
routes[route] = serve(handler); |
|
|
|
|
routes[route] = fileserver.serve(handler); |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
return routes; |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
if base_path then |
|
|
|
|
module:provides("http", { |
|
|
|
|
route = { |
|
|
|
|
["GET /*"] = serve { |
|
|
|
|
path = base_path; |
|
|
|
|
directory_index = directory_index; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
else |
|
|
|
|
module:log("debug", "http_files_dir not set, assuming use by some other module"); |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
module:provides("http", { |
|
|
|
|
route = { |
|
|
|
|
["GET /*"] = fileserver.serve({ |
|
|
|
|
path = base_path; |
|
|
|
|
directory_index = directory_index; |
|
|
|
|
mime_map = mime_map; |
|
|
|
|
cache_size = cache_size; |
|
|
|
|
cache_max_file_size = cache_max_file_size; |
|
|
|
|
index_files = dir_indices; |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|