2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 module
:depends("http");
10 local server
= require
"net.http.server";
11 local lfs
= require
"lfs";
13 local os_date
= os
.date;
15 local stat
= lfs
.attributes
;
16 local build_path
= require
"socket.url".build_path
;
17 local path_sep
= package
.config
:sub(1,1);
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");
25 local mime_map
= module
:shared("/*/http_files/mime").types
;
28 html
= "text/html", htm
= "text/html",
29 xml
= "application/xml",
32 js
= "application/javascript",
35 jpeg
= "image/jpeg", jpg
= "image/jpeg",
36 svg
= "image/svg+xml",
38 module
:shared("/*/http_files/mime").types
= mime_map
;
40 local mime_types
, err
= open(module
:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r");
42 local mime_data
= mime_types
:read("*a");
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";
54 local forbidden_chars_pattern
= "[/%z]";
55 if prosody
.platform
== "windows" then
56 forbidden_chars_pattern
= "[/%z\001-\031\127\"*:<>?|]"
59 local urldecode
= require
"util.http".urldecode
;
60 function sanitize_path(path
)
61 if not path
then return end
65 for component
in path
:gmatch("([^/]+)") do
66 component
= urldecode(component
);
67 if component
:find(forbidden_chars_pattern
) then
69 elseif component
== ".." then
75 elseif component
~= "." then
80 if path
:sub(-1,-1) == "/" then
83 return "/"..table.concat(out
, "/");
86 local cache
= require
"util.cache".new(cache_size
);
89 if type(opts
) ~= "table" then -- assume path string
90 opts
= { path
= opts
};
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
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
110 local request_headers
, response_headers
= request
.headers
, response
.headers
;
112 local last_modified
= os_date('!%a, %d %b %Y %H:%M:%S GMT', attr
.modification
);
113 response_headers
.last_modified
= last_modified
;
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
;
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
125 local data
= cache
:get(orig_path
);
126 if data
and data
.etag
== etag
then
127 response_headers
.content_type
= data
.content_type
;
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
);
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
]);
142 if directory_index
then
143 data
= server
._events
.fire_event("directory-index", { path
= request
.path
, full_path
= full_path
});
148 cache
:set(orig_path
, { data
= data
, content_type
= mime_map
.html
; etag
= etag
; });
149 response_headers
.content_type
= mime_map
.html
;
152 local f
, err
= open(full_path
, "rb");
154 module
:log("debug", "Could not open %s. Error was %s", full_path
, err
);
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
);
168 cache
:set(orig_path
, { data
= data
; content_type
= content_type
; etag
= etag
});
171 return response
:send(data
);
177 function wrap_route(routes
)
178 for route
,handler
in pairs(routes
) do
179 if type(handler
) ~= "function" then
180 routes
[route
] = serve(handler
);
187 module
:provides("http", {
191 directory_index
= directory_index
;
196 module
:log("debug", "http_files_dir not set, assuming use by some other module");