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 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");
14 local os_date
= os
.date;
16 local stat
= lfs
.attributes
;
17 local build_path
= require
"socket.url".build_path
;
18 local path_sep
= package
.config
:sub(1,1);
21 local forbidden_chars_pattern
= "[/%z]";
22 if package
.config
:sub(1,1) == "\\" then
23 forbidden_chars_pattern
= "[/%z\001-\031\127\"*:<>?|]"
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
32 for component
in path
:gmatch("([^/]+)") do
33 component
= urldecode(component
);
34 if component
:find(forbidden_chars_pattern
) then
36 elseif component
== ".." then
42 elseif component
~= "." then
47 if path
:sub(-1,-1) == "/" then
50 return "/"..table.concat(out
, "/");
53 local function serve(opts
)
54 if type(opts
) ~= "table" then -- assume path string
55 opts
= { path
= opts
};
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
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
78 local request_headers
, response_headers
= request
.headers
, response
.headers
;
80 local last_modified
= os_date('!%a, %d %b %Y %H:%M:%S GMT', attr
.modification
);
81 response_headers
.last_modified
= last_modified
;
83 local etag
= ('"%02x-%x-%x-%x"'):format(attr
.dev
or 0, attr
.ino
or 0, attr
.size
or 0, attr
.modification
or 0);
84 response_headers
.etag
= etag
;
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
93 local data
= cache
:get(orig_path
);
94 if data
and data
.etag
== etag
then
95 response_headers
.content_type
= data
.content_type
;
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
);
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
]);
111 if directory_index
then
112 data
= server
._events
.fire_event("directory-index", { path
= request
.path
, full_path
= full_path
});
117 cache
:set(orig_path
, { data
= data
, content_type
= mime_map
.html
; etag
= etag
; });
118 response_headers
.content_type
= mime_map
.html
;
121 local f
, err
= open(full_path
, "rb");
123 log("debug", "Could not open %s. Error was %s", full_path
, err
);
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
= attr
.size
;
131 log("debug", "%d > cache_max_file_size", attr
.size
);
132 return response
:send_file(f
);
137 cache
:set(orig_path
, { data
= data
; content_type
= content_type
; etag
= etag
});
140 return response
:send(data
);