Software / code / prosody
Comparison
net/http/files.lua @ 11120:b2331f3dfeea
Merge 0.11->trunk
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Wed, 30 Sep 2020 09:50:33 +0100 |
| parent | 10776:plugins/mod_http_files.lua@8f3b87eaec49 |
| parent | 10778:plugins/mod_http_files.lua@a62b981db0e2 |
| child | 11200:bf8f2da84007 |
comparison
equal
deleted
inserted
replaced
| 11119:68df52bf08d5 | 11120:b2331f3dfeea |
|---|---|
| 1 -- Prosody IM | |
| 2 -- Copyright (C) 2008-2010 Matthew Wild | |
| 3 -- Copyright (C) 2008-2010 Waqas Hussain | |
| 4 -- | |
| 5 -- This project is MIT/X11 licensed. Please see the | |
| 6 -- COPYING file in the source package for more information. | |
| 7 -- | |
| 8 | |
| 9 local server = require"net.http.server"; | |
| 10 local lfs = require "lfs"; | |
| 11 local new_cache = require "util.cache".new; | |
| 12 local log = require "util.logger".init("net.http.files"); | |
| 13 | |
| 14 local os_date = os.date; | |
| 15 local open = io.open; | |
| 16 local stat = lfs.attributes; | |
| 17 local build_path = require"socket.url".build_path; | |
| 18 local path_sep = package.config:sub(1,1); | |
| 19 | |
| 20 | |
| 21 local forbidden_chars_pattern = "[/%z]"; | |
| 22 if package.config:sub(1,1) == "\\" then | |
| 23 forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" | |
| 24 end | |
| 25 | |
| 26 local urldecode = require "util.http".urldecode; | |
| 27 local function sanitize_path(path) --> util.paths or util.http? | |
| 28 if not path then return end | |
| 29 local out = {}; | |
| 30 | |
| 31 local c = 0; | |
| 32 for component in path:gmatch("([^/]+)") do | |
| 33 component = urldecode(component); | |
| 34 if component:find(forbidden_chars_pattern) then | |
| 35 return nil; | |
| 36 elseif component == ".." then | |
| 37 if c <= 0 then | |
| 38 return nil; | |
| 39 end | |
| 40 out[c] = nil; | |
| 41 c = c - 1; | |
| 42 elseif component ~= "." then | |
| 43 c = c + 1; | |
| 44 out[c] = component; | |
| 45 end | |
| 46 end | |
| 47 if path:sub(-1,-1) == "/" then | |
| 48 out[c+1] = ""; | |
| 49 end | |
| 50 return "/"..table.concat(out, "/"); | |
| 51 end | |
| 52 | |
| 53 local function serve(opts) | |
| 54 if type(opts) ~= "table" then -- assume path string | |
| 55 opts = { path = opts }; | |
| 56 end | |
| 57 local mime_map = opts.mime_map or { html = "text/html" }; | |
| 58 local cache = new_cache(opts.cache_size or 256); | |
| 59 local cache_max_file_size = tonumber(opts.cache_max_file_size) or 1024 | |
| 60 -- luacheck: ignore 431 | |
| 61 local base_path = opts.path; | |
| 62 local dir_indices = opts.index_files or { "index.html", "index.htm" }; | |
| 63 local directory_index = opts.directory_index; | |
| 64 local function serve_file(event, path) | |
| 65 local request, response = event.request, event.response; | |
| 66 local sanitized_path = sanitize_path(path); | |
| 67 if path and not sanitized_path then | |
| 68 return 400; | |
| 69 end | |
| 70 path = sanitized_path; | |
| 71 local orig_path = sanitize_path(request.path); | |
| 72 local full_path = base_path .. (path or ""):gsub("/", path_sep); | |
| 73 local attr = stat(full_path:match("^.*[^\\/]")); -- Strip trailing path separator because Windows | |
| 74 if not attr then | |
| 75 return 404; | |
| 76 end | |
| 77 | |
| 78 local request_headers, response_headers = request.headers, response.headers; | |
| 79 | |
| 80 local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification); | |
| 81 response_headers.last_modified = last_modified; | |
| 82 | |
| 83 local etag = ('"%x-%x-%x"'):format(attr.change or 0, attr.size or 0, attr.modification or 0); | |
| 84 response_headers.etag = etag; | |
| 85 | |
| 86 local if_none_match = request_headers.if_none_match | |
| 87 local if_modified_since = request_headers.if_modified_since; | |
| 88 if etag == if_none_match | |
| 89 or (not if_none_match and last_modified == if_modified_since) then | |
| 90 return 304; | |
| 91 end | |
| 92 | |
| 93 local data = cache:get(orig_path); | |
| 94 if data and data.etag == etag then | |
| 95 response_headers.content_type = data.content_type; | |
| 96 data = data.data; | |
| 97 cache:set(orig_path, data); | |
| 98 elseif attr.mode == "directory" and path then | |
| 99 if full_path:sub(-1) ~= "/" then | |
| 100 local dir_path = { is_absolute = true, is_directory = true }; | |
| 101 for dir in orig_path:gmatch("[^/]+") do dir_path[#dir_path+1]=dir; end | |
| 102 response_headers.location = build_path(dir_path); | |
| 103 return 301; | |
| 104 end | |
| 105 for i=1,#dir_indices do | |
| 106 if stat(full_path..dir_indices[i], "mode") == "file" then | |
| 107 return serve_file(event, path..dir_indices[i]); | |
| 108 end | |
| 109 end | |
| 110 | |
| 111 if directory_index then | |
| 112 data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); | |
| 113 end | |
| 114 if not data then | |
| 115 return 403; | |
| 116 end | |
| 117 cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; }); | |
| 118 response_headers.content_type = mime_map.html; | |
| 119 | |
| 120 else | |
| 121 local f, err = open(full_path, "rb"); | |
| 122 if not f then | |
| 123 log("debug", "Could not open %s. Error was %s", full_path, err); | |
| 124 return 403; | |
| 125 end | |
| 126 local ext = full_path:match("%.([^./]+)$"); | |
| 127 local content_type = ext and mime_map[ext]; | |
| 128 response_headers.content_type = content_type; | |
| 129 if attr.size > cache_max_file_size then | |
| 130 response_headers.content_length = ("%d"):format(attr.size); | |
| 131 log("debug", "%d > cache_max_file_size", attr.size); | |
| 132 return response:send_file(f); | |
| 133 else | |
| 134 data = f:read("*a"); | |
| 135 f:close(); | |
| 136 end | |
| 137 cache:set(orig_path, { data = data; content_type = content_type; etag = etag }); | |
| 138 end | |
| 139 | |
| 140 return response:send(data); | |
| 141 end | |
| 142 | |
| 143 return serve_file; | |
| 144 end | |
| 145 | |
| 146 return { | |
| 147 serve = serve; | |
| 148 } | |
| 149 |