Software /
code /
prosody
Comparison
net/http/files.lua @ 9948:c93fdec624c7
net.http.files: Copy of mod_http_files
The intent is to make it easier to reuse and simplify mod_http_files.
Currently modules will use the serve() function exported by
mod_http_files in order to serve their own files. This makes it unclear
whether mod_http_files should be doing anything on its own. Moving the
logic into a separate module should help here, as well as make re-use
outside of prosody easier.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 05 Apr 2019 16:10:51 +0200 |
parent | 9463:plugins/mod_http_files.lua@f7530c846f98 |
child | 9949:a39f3681d685 |
comparison
equal
deleted
inserted
replaced
9947:8ebca1240203 | 9948:c93fdec624c7 |
---|---|
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 module:depends("http"); | |
10 local server = require"net.http.server"; | |
11 local lfs = require "lfs"; | |
12 | |
13 local os_date = os.date; | |
14 local open = io.open; | |
15 local stat = lfs.attributes; | |
16 local build_path = require"socket.url".build_path; | |
17 local path_sep = package.config:sub(1,1); | |
18 | |
19 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); | |
21 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" }); | |
23 local directory_index = module:get_option_boolean("http_dir_listing"); | |
24 | |
25 local mime_map = module:shared("/*/http_files/mime").types; | |
26 if not mime_map then | |
27 mime_map = { | |
28 html = "text/html", htm = "text/html", | |
29 xml = "application/xml", | |
30 txt = "text/plain", | |
31 css = "text/css", | |
32 js = "application/javascript", | |
33 png = "image/png", | |
34 gif = "image/gif", | |
35 jpeg = "image/jpeg", jpg = "image/jpeg", | |
36 svg = "image/svg+xml", | |
37 }; | |
38 module:shared("/*/http_files/mime").types = mime_map; | |
39 | |
40 local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r"); | |
41 if mime_types then | |
42 local mime_data = mime_types:read("*a"); | |
43 mime_types:close(); | |
44 setmetatable(mime_map, { | |
45 __index = function(t, ext) | |
46 local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; | |
47 t[ext] = typ; | |
48 return typ; | |
49 end | |
50 }); | |
51 end | |
52 end | |
53 | |
54 local forbidden_chars_pattern = "[/%z]"; | |
55 if prosody.platform == "windows" then | |
56 forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" | |
57 end | |
58 | |
59 local urldecode = require "util.http".urldecode; | |
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) | |
89 if type(opts) ~= "table" then -- assume path string | |
90 opts = { path = opts }; | |
91 end | |
92 -- luacheck: ignore 431 | |
93 local base_path = opts.path; | |
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 = ('"%02x-%x-%x-%x"'):format(attr.dev or 0, attr.ino 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 | |
173 | |
174 return serve_file; | |
175 end | |
176 | |
177 function wrap_route(routes) | |
178 for route,handler in pairs(routes) do | |
179 if type(handler) ~= "function" then | |
180 routes[route] = serve(handler); | |
181 end | |
182 end | |
183 return routes; | |
184 end | |
185 | |
186 if base_path then | |
187 module:provides("http", { | |
188 route = { | |
189 ["GET /*"] = serve { | |
190 path = base_path; | |
191 directory_index = directory_index; | |
192 } | |
193 }; | |
194 }); | |
195 else | |
196 module:log("debug", "http_files_dir not set, assuming use by some other module"); | |
197 end | |
198 |