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}.