1%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
2%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
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/2]).
19-export([malformed_request/2]).
20-export([forbidden/2]).
21-export([content_types_provided/2]).
22-export([charsets_provided/2]).
23-export([ranges_provided/2]).
24-export([resource_exists/2]).
25-export([last_modified/2]).
26-export([generate_etag/2]).
27-export([get_file/2]).
28
29-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
30-type extra_etag() :: {etag, module(), function()} | {etag, false}.
31-type extra_mimetypes() :: {mimetypes, module(), function()}
32	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
33-type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()].
34-type opts() :: {file | dir, string() | binary()}
35	| {file | dir, string() | binary(), extra()}
36	| {priv_file | priv_dir, atom(), string() | binary()}
37	| {priv_file | priv_dir, atom(), string() | binary(), extra()}.
38-export_type([opts/0]).
39
40-include_lib("kernel/include/file.hrl").
41
42-type state() :: {binary(), {direct | archive, #file_info{}}
43	| {error, atom()}, extra()}.
44
45%% Resolve the file that will be sent and get its file information.
46%% If the handler is configured to manage a directory, check that the
47%% requested file is inside the configured directory.
48
49-spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req().
50init(Req, {Name, Path}) ->
51	init_opts(Req, {Name, Path, []});
52init(Req, {Name, App, Path})
53		when Name =:= priv_file; Name =:= priv_dir ->
54	init_opts(Req, {Name, App, Path, []});
55init(Req, Opts) ->
56	init_opts(Req, Opts).
57
58init_opts(Req, {priv_file, App, Path, Extra}) ->
59	{PrivPath, HowToAccess} = priv_path(App, Path),
60	init_info(Req, absname(PrivPath), HowToAccess, Extra);
61init_opts(Req, {file, Path, Extra}) ->
62	init_info(Req, absname(Path), direct, Extra);
63init_opts(Req, {priv_dir, App, Path, Extra}) ->
64	{PrivPath, HowToAccess} = priv_path(App, Path),
65	init_dir(Req, PrivPath, HowToAccess, Extra);
66init_opts(Req, {dir, Path, Extra}) ->
67	init_dir(Req, Path, direct, Extra).
68
69priv_path(App, Path) ->
70	case code:priv_dir(App) of
71		{error, bad_name} ->
72			error({badarg, "Can't resolve the priv_dir of application "
73				++ atom_to_list(App)});
74		PrivDir when is_list(Path) ->
75			{
76				PrivDir ++ "/" ++ Path,
77				how_to_access_app_priv(PrivDir)
78			};
79		PrivDir when is_binary(Path) ->
80			{
81				<< (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
82				how_to_access_app_priv(PrivDir)
83			}
84	end.
85
86how_to_access_app_priv(PrivDir) ->
87	%% If the priv directory is not a directory, it must be
88	%% inside an Erlang application .ez archive. We call
89	%% how_to_access_app_priv1() to find the corresponding archive.
90	case filelib:is_dir(PrivDir) of
91		true  -> direct;
92		false -> how_to_access_app_priv1(PrivDir)
93	end.
94
95how_to_access_app_priv1(Dir) ->
96	%% We go "up" by one path component at a time and look for a
97	%% regular file.
98	Archive = filename:dirname(Dir),
99	case Archive of
100		Dir ->
101			%% filename:dirname() returned its argument:
102			%% we reach the root directory. We found no
103			%% archive so we return 'direct': the given priv
104			%% directory doesn't exist.
105			direct;
106		_ ->
107			case filelib:is_regular(Archive) of
108				true  -> {archive, Archive};
109				false -> how_to_access_app_priv1(Archive)
110			end
111	end.
112
113absname(Path) when is_list(Path) ->
114	filename:absname(list_to_binary(Path));
115absname(Path) when is_binary(Path) ->
116	filename:absname(Path).
117
118init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
119	init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
120init_dir(Req, Path, HowToAccess, Extra) ->
121	Dir = fullpath(filename:absname(Path)),
122	case cowboy_req:path_info(Req) of
123		%% When dir/priv_dir are used and there is no path_info
124		%% this is a configuration error and we abort immediately.
125		undefined ->
126			{ok, cowboy_req:reply(500, Req), error};
127		PathInfo ->
128			case validate_reserved(PathInfo) of
129				error ->
130					{cowboy_rest, Req, error};
131				ok ->
132					Filepath = filename:join([Dir|PathInfo]),
133					Len = byte_size(Dir),
134					case fullpath(Filepath) of
135						<< Dir:Len/binary, $/, _/binary >> ->
136							init_info(Req, Filepath, HowToAccess, Extra);
137						<< Dir:Len/binary >> ->
138							init_info(Req, Filepath, HowToAccess, Extra);
139						_ ->
140							{cowboy_rest, Req, error}
141					end
142			end
143	end.
144
145validate_reserved([]) ->
146	ok;
147validate_reserved([P|Tail]) ->
148	case validate_reserved1(P) of
149		ok -> validate_reserved(Tail);
150		error -> error
151	end.
152
153%% We always reject forward slash, backward slash and NUL as
154%% those have special meanings across the supported platforms.
155%% We could support the backward slash on some platforms but
156%% for the sake of consistency and simplicity we don't.
157validate_reserved1(<<>>) ->
158	ok;
159validate_reserved1(<<$/, _/bits>>) ->
160	error;
161validate_reserved1(<<$\\, _/bits>>) ->
162	error;
163validate_reserved1(<<0, _/bits>>) ->
164	error;
165validate_reserved1(<<_, Rest/bits>>) ->
166	validate_reserved1(Rest).
167
168fullpath(Path) ->
169	fullpath(filename:split(Path), []).
170fullpath([], Acc) ->
171	filename:join(lists:reverse(Acc));
172fullpath([<<".">>|Tail], Acc) ->
173	fullpath(Tail, Acc);
174fullpath([<<"..">>|Tail], Acc=[_]) ->
175	fullpath(Tail, Acc);
176fullpath([<<"..">>|Tail], [_|Acc]) ->
177	fullpath(Tail, Acc);
178fullpath([Segment|Tail], Acc) ->
179	fullpath(Tail, [Segment|Acc]).
180
181init_info(Req, Path, HowToAccess, Extra) ->
182	Info = read_file_info(Path, HowToAccess),
183	{cowboy_rest, Req, {Path, Info, Extra}}.
184
185read_file_info(Path, direct) ->
186	case file:read_file_info(Path, [{time, universal}]) of
187		{ok, Info} -> {direct, Info};
188		Error      -> Error
189	end;
190read_file_info(Path, {archive, Archive}) ->
191	case file:read_file_info(Archive, [{time, universal}]) of
192		{ok, ArchiveInfo} ->
193			%% The Erlang application archive is fine.
194			%% Now check if the requested file is in that
195			%% archive. We also need the file_info to merge
196			%% them with the archive's one.
197			PathS = binary_to_list(Path),
198			case erl_prim_loader:read_file_info(PathS) of
199				{ok, ContainedFileInfo} ->
200					Info = fix_archived_file_info(
201						ArchiveInfo,
202						ContainedFileInfo),
203					{archive, Info};
204				error ->
205					{error, enoent}
206			end;
207		Error ->
208			Error
209	end.
210
211fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
212	%% We merge the archive and content #file_info because we are
213	%% interested by the timestamps of the archive, but the type and
214	%% size of the contained file/directory.
215	%%
216	%% We reset the access to 'read', because we won't rewrite the
217	%% archive.
218	ArchiveInfo#file_info{
219		size = ContainedFileInfo#file_info.size,
220		type = ContainedFileInfo#file_info.type,
221		access = read
222	}.
223
224-ifdef(TEST).
225fullpath_test_() ->
226	Tests = [
227		{<<"/home/cowboy">>, <<"/home/cowboy">>},
228		{<<"/home/cowboy">>, <<"/home/cowboy/">>},
229		{<<"/home/cowboy">>, <<"/home/cowboy/./">>},
230		{<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
231		{<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
232		{<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
233		{<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
234		{<<"/">>, <<"/home/cowboy/../../../../../..">>},
235		{<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
236	],
237	[{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
238
239good_path_check_test_() ->
240	Tests = [
241		<<"/home/cowboy/file">>,
242		<<"/home/cowboy/file/">>,
243		<<"/home/cowboy/./file">>,
244		<<"/home/cowboy/././././././file">>,
245		<<"/home/cowboy/abc/../file">>,
246		<<"/home/cowboy/abc/../file">>,
247		<<"/home/cowboy/abc/./.././file">>
248	],
249	[{P, fun() ->
250		case fullpath(P) of
251			<< "/home/cowboy/", _/bits >> -> ok
252		end
253	end} || P <- Tests].
254
255bad_path_check_test_() ->
256	Tests = [
257		<<"/home/cowboy/../../../../../../file">>,
258		<<"/home/cowboy/../../etc/passwd">>
259	],
260	[{P, fun() ->
261		error = case fullpath(P) of
262			<< "/home/cowboy/", _/bits >> -> ok;
263			_ -> error
264		end
265	end} || P <- Tests].
266
267good_path_win32_check_test_() ->
268	Tests = case os:type() of
269		{unix, _} ->
270			[];
271		{win32, _} ->
272			[
273				<<"c:/home/cowboy/file">>,
274				<<"c:/home/cowboy/file/">>,
275				<<"c:/home/cowboy/./file">>,
276				<<"c:/home/cowboy/././././././file">>,
277				<<"c:/home/cowboy/abc/../file">>,
278				<<"c:/home/cowboy/abc/../file">>,
279				<<"c:/home/cowboy/abc/./.././file">>
280			]
281	end,
282	[{P, fun() ->
283		case fullpath(P) of
284			<< "c:/home/cowboy/", _/bits >> -> ok
285		end
286	end} || P <- Tests].
287
288bad_path_win32_check_test_() ->
289	Tests = case os:type() of
290		{unix, _} ->
291			[];
292		{win32, _} ->
293			[
294				<<"c:/home/cowboy/../../secretfile.bat">>,
295				<<"c:/home/cowboy/c:/secretfile.bat">>,
296				<<"c:/home/cowboy/..\\..\\secretfile.bat">>,
297				<<"c:/home/cowboy/c:\\secretfile.bat">>
298			]
299	end,
300	[{P, fun() ->
301		error = case fullpath(P) of
302			<< "c:/home/cowboy/", _/bits >> -> ok;
303			_ -> error
304		end
305	end} || P <- Tests].
306-endif.
307
308%% Reject requests that tried to access a file outside
309%% the target directory, or used reserved characters.
310
311-spec malformed_request(Req, State)
312	-> {boolean(), Req, State}.
313malformed_request(Req, State) ->
314	{State =:= error, Req, State}.
315
316%% Directories, files that can't be accessed at all and
317%% files with no read flag are forbidden.
318
319-spec forbidden(Req, State)
320	-> {boolean(), Req, State}
321	when State::state().
322forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
323	{true, Req, State};
324forbidden(Req, State={_, {error, eacces}, _}) ->
325	{true, Req, State};
326forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
327		when Access =:= write; Access =:= none ->
328	{true, Req, State};
329forbidden(Req, State) ->
330	{false, Req, State}.
331
332%% Detect the mimetype of the file.
333
334-spec content_types_provided(Req, State)
335	-> {[{binary(), get_file}], Req, State}
336	when State::state().
337content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) ->
338	case lists:keyfind(mimetypes, 1, Extra) of
339		false ->
340			{[{cow_mimetypes:web(Path), get_file}], Req, State};
341		{mimetypes, Module, Function} ->
342			{[{Module:Function(Path), get_file}], Req, State};
343		{mimetypes, Type} ->
344			{[{Type, get_file}], Req, State}
345	end.
346
347%% Detect the charset of the file.
348
349-spec charsets_provided(Req, State)
350	-> {[binary()], Req, State}
351	when State::state().
352charsets_provided(Req, State={Path, _, Extra}) ->
353	case lists:keyfind(charset, 1, Extra) of
354		%% We simulate the callback not being exported.
355		false ->
356			no_call;
357		{charset, Module, Function} ->
358			{[Module:Function(Path)], Req, State};
359		{charset, Charset} when is_binary(Charset) ->
360			{[Charset], Req, State}
361	end.
362
363%% Enable support for range requests.
364
365-spec ranges_provided(Req, State)
366	-> {[{binary(), auto}], Req, State}
367	when State::state().
368ranges_provided(Req, State) ->
369	{[{<<"bytes">>, auto}], Req, State}.
370
371%% Assume the resource doesn't exist if it's not a regular file.
372
373-spec resource_exists(Req, State)
374	-> {boolean(), Req, State}
375	when State::state().
376resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
377	{true, Req, State};
378resource_exists(Req, State) ->
379	{false, Req, State}.
380
381%% Generate an etag for the file.
382
383-spec generate_etag(Req, State)
384	-> {{strong | weak, binary()}, Req, State}
385	when State::state().
386generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
387		Extra}) ->
388	case lists:keyfind(etag, 1, Extra) of
389		false ->
390			{generate_default_etag(Size, Mtime), Req, State};
391		{etag, Module, Function} ->
392			{Module:Function(Path, Size, Mtime), Req, State};
393		{etag, false} ->
394			{undefined, Req, State}
395	end.
396
397generate_default_etag(Size, Mtime) ->
398	{strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
399
400%% Return the time of last modification of the file.
401
402-spec last_modified(Req, State)
403	-> {calendar:datetime(), Req, State}
404	when State::state().
405last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
406	{Modified, Req, State}.
407
408%% Stream the file.
409
410-spec get_file(Req, State)
411	-> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
412	when State::state().
413get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
414	{{sendfile, 0, Size, Path}, Req, State};
415get_file(Req, State={Path, {archive, _}, _}) ->
416	PathS = binary_to_list(Path),
417	{ok, Bin, _} = erl_prim_loader:get_file(PathS),
418	{Bin, Req, State}.
419