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