1%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
2%% Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
3%%
4%% Permission to use, copy, modify, and/or distribute this software for any
5%% purpose with or without fee is hereby granted, provided that the above
6%% copyright notice and this permission notice appear in all copies.
7%%
8%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16-module(cowboy_static).
17
18-export([init/3]).
19-export([rest_init/2]).
20-export([malformed_request/2]).
21-export([forbidden/2]).
22-export([content_types_provided/2]).
23-export([resource_exists/2]).
24-export([last_modified/2]).
25-export([generate_etag/2]).
26-export([get_file/2]).
27
28-type extra_etag() :: {etag, module(), function()} | {etag, false}.
29-type extra_mimetypes() :: {mimetypes, module(), function()}
30	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
31-type extra() :: [extra_etag() | extra_mimetypes()].
32-type opts() :: {file | dir, string() | binary()}
33	| {file | dir, string() | binary(), extra()}
34	| {priv_file | priv_dir, atom(), string() | binary()}
35	| {priv_file | priv_dir, atom(), string() | binary(), extra()}.
36-export_type([opts/0]).
37
38-include_lib("kernel/include/file.hrl").
39
40-type state() :: {binary(), {direct | archive, #file_info{}}
41	| {error, atom()}, extra()}.
42
43-spec init(_, _, _) -> {upgrade, protocol, cowboy_rest}.
44init(_, _, _) ->
45	{upgrade, protocol, cowboy_rest}.
46
47%% Resolve the file that will be sent and get its file information.
48%% If the handler is configured to manage a directory, check that the
49%% requested file is inside the configured directory.
50
51-spec rest_init(Req, opts())
52	-> {ok, Req, error | state()}
53	when Req::cowboy_req:req().
54rest_init(Req, {Name, Path}) ->
55	rest_init_opts(Req, {Name, Path, []});
56rest_init(Req, {Name, App, Path})
57		when Name =:= priv_file; Name =:= priv_dir ->
58	rest_init_opts(Req, {Name, App, Path, []});
59rest_init(Req, Opts) ->
60	rest_init_opts(Req, Opts).
61
62rest_init_opts(Req, {priv_file, App, Path, Extra}) ->
63	{PrivPath, HowToAccess} = priv_path(App, Path),
64	rest_init_info(Req, absname(PrivPath), HowToAccess, Extra);
65rest_init_opts(Req, {file, Path, Extra}) ->
66	rest_init_info(Req, absname(Path), direct, Extra);
67rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
68	{PrivPath, HowToAccess} = priv_path(App, Path),
69	rest_init_dir(Req, PrivPath, HowToAccess, Extra);
70rest_init_opts(Req, {dir, Path, Extra}) ->
71	rest_init_dir(Req, Path, direct, Extra).
72
73priv_path(App, Path) ->
74	case code:priv_dir(App) of
75		{error, bad_name} ->
76			error({badarg, "Can't resolve the priv_dir of application "
77				++ atom_to_list(App)});
78		PrivDir when is_list(Path) ->
79			{
80				PrivDir ++ "/" ++ Path,
81				how_to_access_app_priv(PrivDir)
82			};
83		PrivDir when is_binary(Path) ->
84			{
85				<< (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
86				how_to_access_app_priv(PrivDir)
87			}
88	end.
89
90how_to_access_app_priv(PrivDir) ->
91	%% If the priv directory is not a directory, it must be
92	%% inside an Erlang application .ez archive. We call
93	%% how_to_access_app_priv1() to find the corresponding archive.
94	case filelib:is_dir(PrivDir) of
95		true  -> direct;
96		false -> how_to_access_app_priv1(PrivDir)
97	end.
98
99how_to_access_app_priv1(Dir) ->
100	%% We go "up" by one path component at a time and look for a
101	%% regular file.
102	Archive = filename:dirname(Dir),
103	case Archive of
104		Dir ->
105			%% filename:dirname() returned its argument:
106			%% we reach the root directory. We found no
107			%% archive so we return 'direct': the given priv
108			%% directory doesn't exist.
109			direct;
110		_ ->
111			case filelib:is_regular(Archive) of
112				true  -> {archive, Archive};
113				false -> how_to_access_app_priv1(Archive)
114			end
115	end.
116
117absname(Path) when is_list(Path) ->
118	filename:absname(list_to_binary(Path));
119absname(Path) when is_binary(Path) ->
120	filename:absname(Path).
121
122rest_init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
123	rest_init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
124rest_init_dir(Req, Path, HowToAccess, Extra) ->
125	Dir = fullpath(filename:absname(Path)),
126	{PathInfo, Req2} = cowboy_req:path_info(Req),
127	Filepath = filename:join([Dir|PathInfo]),
128	Len = byte_size(Dir),
129	case fullpath(Filepath) of
130		<< Dir:Len/binary, $/, _/binary >> ->
131			rest_init_info(Req2, Filepath, HowToAccess, Extra);
132		_ ->
133			{ok, Req2, error}
134	end.
135
136fullpath(Path) ->
137	fullpath(filename:split(Path), []).
138fullpath([], Acc) ->
139	filename:join(lists:reverse(Acc));
140fullpath([<<".">>|Tail], Acc) ->
141	fullpath(Tail, Acc);
142fullpath([<<"..">>|Tail], Acc=[_]) ->
143	fullpath(Tail, Acc);
144fullpath([<<"..">>|Tail], [_|Acc]) ->
145	fullpath(Tail, Acc);
146fullpath([Segment|Tail], Acc) ->
147	fullpath(Tail, [Segment|Acc]).
148
149rest_init_info(Req, Path, HowToAccess, Extra) ->
150	Info = read_file_info(Path, HowToAccess),
151	{ok, Req, {Path, Info, Extra}}.
152
153read_file_info(Path, direct) ->
154	case file:read_file_info(Path, [{time, universal}]) of
155		{ok, Info} -> {direct, Info};
156		Error      -> Error
157	end;
158read_file_info(Path, {archive, Archive}) ->
159	case file:read_file_info(Archive, [{time, universal}]) of
160		{ok, ArchiveInfo} ->
161			%% The Erlang application archive is fine.
162			%% Now check if the requested file is in that
163			%% archive. We also need the file_info to merge
164			%% them with the archive's one.
165			PathS = binary_to_list(Path),
166			case erl_prim_loader:read_file_info(PathS) of
167				{ok, ContainedFileInfo} ->
168					Info = fix_archived_file_info(
169						ArchiveInfo,
170						ContainedFileInfo),
171					{archive, Info};
172				error ->
173					{error, enoent}
174			end;
175		Error ->
176			Error
177	end.
178
179fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
180	%% We merge the archive and content #file_info because we are
181	%% interested by the timestamps of the archive, but the type and
182	%% size of the contained file/directory.
183	%%
184	%% We reset the access to 'read', because we won't rewrite the
185	%% archive.
186	ArchiveInfo#file_info{
187		size = ContainedFileInfo#file_info.size,
188		type = ContainedFileInfo#file_info.type,
189		access = read
190	}.
191
192-ifdef(TEST).
193fullpath_test_() ->
194	Tests = [
195		{<<"/home/cowboy">>, <<"/home/cowboy">>},
196		{<<"/home/cowboy">>, <<"/home/cowboy/">>},
197		{<<"/home/cowboy">>, <<"/home/cowboy/./">>},
198		{<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
199		{<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
200		{<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
201		{<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
202		{<<"/">>, <<"/home/cowboy/../../../../../..">>},
203		{<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
204	],
205	[{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
206
207good_path_check_test_() ->
208	Tests = [
209		<<"/home/cowboy/file">>,
210		<<"/home/cowboy/file/">>,
211		<<"/home/cowboy/./file">>,
212		<<"/home/cowboy/././././././file">>,
213		<<"/home/cowboy/abc/../file">>,
214		<<"/home/cowboy/abc/../file">>,
215		<<"/home/cowboy/abc/./.././file">>
216	],
217	[{P, fun() ->
218		case fullpath(P) of
219			<< "/home/cowboy/", _/binary >> -> ok
220		end
221	end} || P <- Tests].
222
223bad_path_check_test_() ->
224	Tests = [
225		<<"/home/cowboy/../../../../../../file">>,
226		<<"/home/cowboy/../../etc/passwd">>
227	],
228	[{P, fun() ->
229		error = case fullpath(P) of
230			<< "/home/cowboy/", _/binary >> -> ok;
231			_ -> error
232		end
233	end} || P <- Tests].
234
235good_path_win32_check_test_() ->
236	Tests = case os:type() of
237		{unix, _} ->
238			[];
239		{win32, _} ->
240			[
241				<<"c:/home/cowboy/file">>,
242				<<"c:/home/cowboy/file/">>,
243				<<"c:/home/cowboy/./file">>,
244				<<"c:/home/cowboy/././././././file">>,
245				<<"c:/home/cowboy/abc/../file">>,
246				<<"c:/home/cowboy/abc/../file">>,
247				<<"c:/home/cowboy/abc/./.././file">>
248			]
249	end,
250	[{P, fun() ->
251		case fullpath(P) of
252			<< "c:/home/cowboy/", _/binary >> -> ok
253		end
254	end} || P <- Tests].
255
256bad_path_win32_check_test_() ->
257	Tests = case os:type() of
258		{unix, _} ->
259			[];
260		{win32, _} ->
261			[
262				<<"c:/home/cowboy/../../secretfile.bat">>,
263				<<"c:/home/cowboy/c:/secretfile.bat">>,
264				<<"c:/home/cowboy/..\\..\\secretfile.bat">>,
265				<<"c:/home/cowboy/c:\\secretfile.bat">>
266			]
267	end,
268	[{P, fun() ->
269		error = case fullpath(P) of
270			<< "c:/home/cowboy/", _/binary >> -> ok;
271			_ -> error
272		end
273	end} || P <- Tests].
274-endif.
275
276%% Reject requests that tried to access a file outside
277%% the target directory.
278
279-spec malformed_request(Req, State)
280	-> {boolean(), Req, State}.
281malformed_request(Req, State) ->
282	{State =:= error, Req, State}.
283
284%% Directories, files that can't be accessed at all and
285%% files with no read flag are forbidden.
286
287-spec forbidden(Req, State)
288	-> {boolean(), Req, State}
289	when State::state().
290forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
291	{true, Req, State};
292forbidden(Req, State={_, {error, eacces}, _}) ->
293	{true, Req, State};
294forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
295		when Access =:= write; Access =:= none ->
296	{true, Req, State};
297forbidden(Req, State) ->
298	{false, Req, State}.
299
300%% Detect the mimetype of the file.
301
302-spec content_types_provided(Req, State)
303	-> {[{binary(), get_file}], Req, State}
304	when State::state().
305content_types_provided(Req, State={Path, _, Extra}) ->
306	case lists:keyfind(mimetypes, 1, Extra) of
307		false ->
308			{[{cow_mimetypes:web(Path), get_file}], Req, State};
309		{mimetypes, Module, Function} ->
310			{[{Module:Function(Path), get_file}], Req, State};
311		{mimetypes, Type} ->
312			{[{Type, get_file}], Req, State}
313	end.
314
315%% Assume the resource doesn't exist if it's not a regular file.
316
317-spec resource_exists(Req, State)
318	-> {boolean(), Req, State}
319	when State::state().
320resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
321	{true, Req, State};
322resource_exists(Req, State) ->
323	{false, Req, State}.
324
325%% Generate an etag for the file.
326
327-spec generate_etag(Req, State)
328	-> {{strong | weak, binary()}, Req, State}
329	when State::state().
330generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
331		Extra}) ->
332	case lists:keyfind(etag, 1, Extra) of
333		false ->
334			{generate_default_etag(Size, Mtime), Req, State};
335		{etag, Module, Function} ->
336			{Module:Function(Path, Size, Mtime), Req, State};
337		{etag, false} ->
338			{undefined, Req, State}
339	end.
340
341generate_default_etag(Size, Mtime) ->
342	{strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
343
344%% Return the time of last modification of the file.
345
346-spec last_modified(Req, State)
347	-> {calendar:datetime(), Req, State}
348	when State::state().
349last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
350	{Modified, Req, State}.
351
352%% Stream the file.
353%% @todo Export cowboy_req:resp_body_fun()?
354
355-spec get_file(Req, State)
356	-> {{stream, non_neg_integer(), fun()}, Req, State}
357	when State::state().
358get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
359	Sendfile = fun (Socket, Transport) ->
360		case Transport:sendfile(Socket, Path) of
361			{ok, _} -> ok;
362			{error, closed} -> ok;
363			{error, etimedout} -> ok
364		end
365	end,
366	{{stream, Size, Sendfile}, Req, State};
367get_file(Req, State={Path, {archive, #file_info{size=Size}}, _}) ->
368	Sendfile = fun (Socket, Transport) ->
369		PathS = binary_to_list(Path),
370		{ok, Bin, _} = erl_prim_loader:get_file(PathS),
371		Transport:send(Socket, Bin)
372	end,
373	{{stream, Size, Sendfile}, Req, State}.
374