zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

cowboy_static.erl (12881B)


      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().
     50 init(Req, {Name, Path}) ->
     51 	init_opts(Req, {Name, Path, []});
     52 init(Req, {Name, App, Path})
     53 		when Name =:= priv_file; Name =:= priv_dir ->
     54 	init_opts(Req, {Name, App, Path, []});
     55 init(Req, Opts) ->
     56 	init_opts(Req, Opts).
     57 
     58 init_opts(Req, {priv_file, App, Path, Extra}) ->
     59 	{PrivPath, HowToAccess} = priv_path(App, Path),
     60 	init_info(Req, absname(PrivPath), HowToAccess, Extra);
     61 init_opts(Req, {file, Path, Extra}) ->
     62 	init_info(Req, absname(Path), direct, Extra);
     63 init_opts(Req, {priv_dir, App, Path, Extra}) ->
     64 	{PrivPath, HowToAccess} = priv_path(App, Path),
     65 	init_dir(Req, PrivPath, HowToAccess, Extra);
     66 init_opts(Req, {dir, Path, Extra}) ->
     67 	init_dir(Req, Path, direct, Extra).
     68 
     69 priv_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 
     86 how_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 
     95 how_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 
    113 absname(Path) when is_list(Path) ->
    114 	filename:absname(list_to_binary(Path));
    115 absname(Path) when is_binary(Path) ->
    116 	filename:absname(Path).
    117 
    118 init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
    119 	init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
    120 init_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 
    145 validate_reserved([]) ->
    146 	ok;
    147 validate_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.
    157 validate_reserved1(<<>>) ->
    158 	ok;
    159 validate_reserved1(<<$/, _/bits>>) ->
    160 	error;
    161 validate_reserved1(<<$\\, _/bits>>) ->
    162 	error;
    163 validate_reserved1(<<0, _/bits>>) ->
    164 	error;
    165 validate_reserved1(<<_, Rest/bits>>) ->
    166 	validate_reserved1(Rest).
    167 
    168 fullpath(Path) ->
    169 	fullpath(filename:split(Path), []).
    170 fullpath([], Acc) ->
    171 	filename:join(lists:reverse(Acc));
    172 fullpath([<<".">>|Tail], Acc) ->
    173 	fullpath(Tail, Acc);
    174 fullpath([<<"..">>|Tail], Acc=[_]) ->
    175 	fullpath(Tail, Acc);
    176 fullpath([<<"..">>|Tail], [_|Acc]) ->
    177 	fullpath(Tail, Acc);
    178 fullpath([Segment|Tail], Acc) ->
    179 	fullpath(Tail, [Segment|Acc]).
    180 
    181 init_info(Req, Path, HowToAccess, Extra) ->
    182 	Info = read_file_info(Path, HowToAccess),
    183 	{cowboy_rest, Req, {Path, Info, Extra}}.
    184 
    185 read_file_info(Path, direct) ->
    186 	case file:read_file_info(Path, [{time, universal}]) of
    187 		{ok, Info} -> {direct, Info};
    188 		Error      -> Error
    189 	end;
    190 read_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 
    211 fix_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).
    225 fullpath_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 
    239 good_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 
    255 bad_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 
    267 good_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 
    288 bad_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}.
    313 malformed_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().
    322 forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
    323 	{true, Req, State};
    324 forbidden(Req, State={_, {error, eacces}, _}) ->
    325 	{true, Req, State};
    326 forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
    327 		when Access =:= write; Access =:= none ->
    328 	{true, Req, State};
    329 forbidden(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().
    337 content_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().
    352 charsets_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().
    368 ranges_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().
    376 resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
    377 	{true, Req, State};
    378 resource_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().
    386 generate_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 
    397 generate_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().
    405 last_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().
    413 get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
    414 	{{sendfile, 0, Size, Path}, Req, State};
    415 get_file(Req, State={Path, {archive, _}, _}) ->
    416 	PathS = binary_to_list(Path),
    417 	{ok, Bin, _} = erl_prim_loader:get_file(PathS),
    418 	{Bin, Req, State}.