123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
- %%
- %% Permission to use, copy, modify, and/or distribute this software for any
- %% purpose with or without fee is hereby granted, provided that the above
- %% copyright notice and this permission notice appear in all copies.
- %%
- %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- %% @doc Static resource handler.
- %%
- %% This built in HTTP handler provides a simple file serving capability for
- %% cowboy applications. It should be considered an experimental feature because
- %% of it's dependency on the experimental REST handler. It's recommended to be
- %% used for small or temporary environments where it is not preferrable to set
- %% up a second server just to serve files.
- %%
- %% If this handler is used the Erlang node running the cowboy application must
- %% be configured to use an async thread pool. This is configured by adding the
- %% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
- %% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
- %% this reply</a> from the OTP team to erlang-bugs
- %%
- %% == Base configuration ==
- %%
- %% The handler must be configured with a request path prefix to serve files
- %% under and the path to a directory to read files from. The request path prefix
- %% is defined in the path pattern of the cowboy dispatch rule for the handler.
- %% The request path pattern must end with a `...' token.
- %%
- %% The directory path can be set to either an absolute or relative path in the
- %% form of a list or binary string representation of a file system path. A list
- %% of binary path segments is also a valid directory path.
- %%
- %% The directory path can also be set to a relative path within the `priv/'
- %% directory of an application. This is configured by setting the value of the
- %% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
- %%
- %% ==== Examples ====
- %% ```
- %% %% Serve files from /var/www/ under http://example.com/static/
- %% {"/static/[...]", cowboy_static,
- %% [{directory, "/var/www"}]}
- %%
- %% %% Serve files from the current working directory under http://example.com/static/
- %% {"/static/[...]", cowboy_static,
- %% [{directory, <<"./">>}]}
- %%
- %% %% Serve files from cowboy/priv/www under http://example.com/
- %% {"/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
- %% '''
- %%
- %% == Content type configuration ==
- %%
- %% By default the content type of all static resources will be set to
- %% `application/octet-stream'. This can be overriden by supplying a list
- %% of filename extension to mimetypes pairs in the `mimetypes' option.
- %% The filename extension should be a binary string including the leading dot.
- %% The mimetypes must be of a type that the `cowboy_rest' protocol can
- %% handle.
- %%
- %% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
- %% application, or an arbitrary function accepting the path to the file being
- %% served, can also be used to generate the list of content types for a static
- %% file resource. The function used must accept an additional argument after
- %% the file path argument.
- %%
- %% ==== Example ====
- %% ```
- %% %% Use a static list of content types.
- %% {"/static/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, []}},
- %% {mimetypes, [
- %% {<<".css">>, [<<"text/css">>]},
- %% {<<".js">>, [<<"application/javascript">>]}]}]}
- %%
- %% %% Use the default database in the mimetypes application.
- %% {"/static/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, []}},
- %% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}
- %% '''
- %%
- %% == ETag Header Function ==
- %%
- %% The default behaviour of the static file handler is to not generate ETag
- %% headers. This is because generating ETag headers based on file metadata
- %% causes different servers in a cluster to generate different ETag values for
- %% the same file unless the metadata is also synced. Generating strong ETags
- %% based on the contents of a file is currently out of scope for this module.
- %%
- %% The default behaviour can be overridden to generate an ETag header based on
- %% a combination of the file path, file size, inode and mtime values. If the
- %% option value is a non-empty list of attribute names tagged with `attributes'
- %% a hex encoded checksum of each attribute specified is included in the value
- %% of the the ETag header. If the list of attribute names is empty no ETag
- %% header is generated.
- %%
- %% If a strong ETag is required a user defined function for generating the
- %% header value can be supplied. The function must accept a list of key/values
- %% of the file attributes as the first argument and a second argument
- %% containing any additional data that the function requires. The function
- %% must return a term of the type `{weak | strong, binary()}' or `undefined'.
- %%
- %% ==== Examples ====
- %% ```
- %% %% A value of default is equal to not specifying the option.
- %% {"static/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, []}},
- %% {etag, default}]}
- %%
- %% %% Use all avaliable ETag function arguments to generate a header value.
- %% {"static/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, []}},
- %% {etag, {attributes, [filepath, filesize, inode, mtime]}}]}
- %%
- %% %% Use a user defined function to generate a strong ETag header value.
- %% {"static/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, []}},
- %% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]}
- %%
- %% generate_strong_etag(Arguments, strong_etag_extra) ->
- %% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
- %% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
- %% {_, _INode} = lists:keyfind(inode, 1, Arguments),
- %% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
- %% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
- %% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
- %% {strong, iolist_to_binary(Checksum)}.
- %% '''
- %%
- %% == File configuration ==
- %%
- %% If the file system path being served does not share a common suffix with
- %% the request path it is possible to override the file path using the `file'
- %% option. The value of this option is expected to be a relative path within
- %% the static file directory specified using the `directory' option.
- %% The path must be in the form of a list or binary string representation of a
- %% file system path. A list of binary path segments, as is used throughout
- %% cowboy, is also a valid.
- %%
- %% When the `file' option is used the same file will be served for all requests
- %% matching the cowboy dispatch fule for the handler. It is not necessary to
- %% end the request path pattern with a `...' token because the request path
- %% will not be used to determine which file to serve from the static directory.
- %%
- %% === Examples ===
- %%
- %% ```
- %% %% Serve cowboy/priv/www/index.html as http://example.com/
- %% {"/", cowboy_static,
- %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
- %% {file, <<"index.html">>}]}
- %%
- %% %% Serve cowboy/priv/www/page.html under http://example.com/*/page
- %% {"/:_/page", cowboy_static,
- %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
- %% {file, <<"page.html">>}]}.
- %%
- %% %% Always serve cowboy/priv/www/other.html under http://example.com/other
- %% {"/other/[...]", cowboy_static,
- %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
- %% {file, "other.html"}]}
- %% '''
- -module(cowboy_static).
- %% include files
- -include_lib("kernel/include/file.hrl").
- %% cowboy_protocol callbacks
- -export([init/3]).
- %% cowboy_rest callbacks
- -export([rest_init/2]).
- -export([allowed_methods/2]).
- -export([malformed_request/2]).
- -export([resource_exists/2]).
- -export([forbidden/2]).
- -export([last_modified/2]).
- -export([generate_etag/2]).
- -export([content_types_provided/2]).
- -export([file_contents/2]).
- %% internal
- -export([path_to_mimetypes/2]).
- %% types
- -type dirpath() :: string() | binary() | [binary()].
- -type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
- -type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
- -type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()}
- | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
- %% handler state
- -record(state, {
- filepath :: binary() | error,
- fileinfo :: {ok, #file_info{}} | {error, _} | error,
- mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
- etag_fun :: {fun(([etagarg()], T) ->
- undefined | {strong | weak, binary()}), T}
- }).
- %% @private Upgrade from HTTP handler to REST handler.
- init({_Transport, http}, _Req, _Opts) ->
- {upgrade, protocol, cowboy_rest}.
- %% @private Set up initial state of REST handler.
- -spec rest_init(Req, list()) -> {ok, Req, #state{}} when Req::cowboy_req:req().
- rest_init(Req, Opts) ->
- {_, DirectoryOpt} = lists:keyfind(directory, 1, Opts),
- Directory = fullpath(filename:absname(directory_path(DirectoryOpt))),
- case lists:keyfind(file, 1, Opts) of
- false ->
- {PathInfo, Req2} = cowboy_req:path_info(Req),
- Filepath = filename:join([Directory|PathInfo]),
- Len = byte_size(Directory),
- case fullpath(Filepath) of
- << Directory:Len/binary, $/, _/binary >> ->
- rest_init(Req2, Opts, Filepath);
- _ ->
- {ok, Req2, #state{filepath=error, fileinfo=error,
- mimetypes=undefined, etag_fun=undefined}}
- end;
- {_, FileOpt} ->
- Filepath = filepath_path(FileOpt),
- Filepath2 = << Directory/binary, $/, Filepath/binary >>,
- rest_init(Req, Opts, Filepath2)
- end.
- rest_init(Req, Opts, Filepath) ->
- Fileinfo = file:read_file_info(Filepath),
- Mimetypes = case lists:keyfind(mimetypes, 1, Opts) of
- false -> {fun path_to_mimetypes/2, []};
- {_, {{M, F}, E}} -> {fun M:F/2, E};
- {_, Mtypes} when is_tuple(Mtypes) -> Mtypes;
- {_, Mtypes} when is_list(Mtypes) -> {fun path_to_mimetypes/2, Mtypes}
- end,
- EtagFun = case lists:keyfind(etag, 1, Opts) of
- false -> {fun no_etag_function/2, undefined};
- {_, default} -> {fun no_etag_function/2, undefined};
- {_, {attributes, []}} -> {fun no_etag_function/2, undefined};
- {_, {attributes, Attrs}} -> {fun attr_etag_function/2, Attrs};
- {_, EtagOpt} -> EtagOpt
- end,
- {ok, Req, #state{filepath=Filepath, fileinfo=Fileinfo,
- mimetypes=Mimetypes, etag_fun=EtagFun}}.
- %% @private Only allow GET and HEAD requests on files.
- -spec allowed_methods(Req, #state{})
- -> {[binary()], Req, #state{}} when Req::cowboy_req:req().
- allowed_methods(Req, State) ->
- {[<<"GET">>, <<"HEAD">>], Req, State}.
- %% @private
- -spec malformed_request(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
- malformed_request(Req, #state{filepath=error}=State) ->
- {true, Req, State};
- malformed_request(Req, State) ->
- {false, Req, State}.
- %% @private Check if the resource exists under the document root.
- -spec resource_exists(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
- resource_exists(Req, #state{fileinfo={error, _}}=State) ->
- {false, Req, State};
- resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
- {Fileinfo#file_info.type =:= regular, Req, State}.
- %% @private
- %% Access to a file resource is forbidden if it exists and the local node does
- %% not have permission to read it. Directory listings are always forbidden.
- -spec forbidden(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
- forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
- {true, Req, State};
- forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
- {true, Req, State};
- forbidden(Req, #state{fileinfo={error, _}}=State) ->
- {false, Req, State};
- forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
- {not (Access =:= read orelse Access =:= read_write), Req, State}.
- %% @private Read the time a file system system object was last modified.
- -spec last_modified(Req, #state{})
- -> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req().
- last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
- {erlang:localtime_to_universaltime(Modified), Req, State}.
- %% @private Generate the ETag header value for this file.
- %% The ETag header value is only generated if the resource is a file that
- %% exists in document root.
- -spec generate_etag(Req, #state{})
- -> {undefined | binary(), Req, #state{}} when Req::cowboy_req:req().
- generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
- mtime=Modified, size=Filesize}}, filepath=Filepath,
- etag_fun={ETagFun, ETagData}}=State) ->
- ETagArgs = [
- {filepath, Filepath}, {filesize, Filesize},
- {inode, INode}, {mtime, Modified}],
- {ETagFun(ETagArgs, ETagData), Req, State};
- generate_etag(Req, State) ->
- {undefined, Req, State}.
- %% @private Return the content type of a file.
- -spec content_types_provided(cowboy_req:req(), #state{}) -> tuple().
- content_types_provided(Req, #state{filepath=Filepath,
- mimetypes={MimetypesFun, MimetypesData}}=State) ->
- Mimetypes = [{T, file_contents}
- || T <- MimetypesFun(Filepath, MimetypesData)],
- {Mimetypes, Req, State}.
- %% @private Return a function that writes a file directly to the socket.
- -spec file_contents(cowboy_req:req(), #state{}) -> tuple().
- file_contents(Req, #state{filepath=Filepath,
- fileinfo={ok, #file_info{size=Filesize}}}=State) ->
- Writefile = fun(Socket, Transport) ->
- %% Transport:sendfile/2 may return {error, closed}
- %% if the connection is closed while sending the file.
- case Transport:sendfile(Socket, Filepath) of
- {ok, _} -> ok;
- {error, closed} -> ok;
- {error, etimedout} -> ok
- end
- end,
- {{stream, Filesize, Writefile}, Req, State}.
- %% Internal.
- -spec directory_path(dirspec()) -> dirpath().
- directory_path({priv_dir, App, []}) ->
- priv_dir_path(App);
- directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
- filename:join(priv_dir_path(App), filename:join(Path));
- directory_path({priv_dir, App, Path}) ->
- filename:join(priv_dir_path(App), Path);
- directory_path([H|_]=Path) when is_binary(H) ->
- filename:join(Path);
- directory_path([H|_]=Path) when is_integer(H) ->
- list_to_binary(Path);
- directory_path(Path) when is_binary(Path) ->
- Path.
- %% @private Return the path to the priv/ directory of an application.
- -spec priv_dir_path(atom()) -> string().
- priv_dir_path(App) ->
- case code:priv_dir(App) of
- {error, bad_name} -> priv_dir_mod(App);
- Dir -> list_to_binary(Dir)
- end.
- -spec priv_dir_mod(atom()) -> string().
- priv_dir_mod(Mod) ->
- case code:which(Mod) of
- File when not is_list(File) -> <<"../priv">>;
- File -> filename:join(filename:dirname(File), <<"../priv">>)
- end.
- %% @private Ensure that a file path is of the same type as a request path.
- filepath_path(Path) when is_binary(Path) ->
- Path;
- filepath_path([H|_]=Path) when is_binary(H) ->
- filename:join(Path);
- filepath_path([H|_]=Path) when is_integer(H) ->
- list_to_binary(Path).
- fullpath(Path) when is_binary(Path) ->
- fullpath(filename:split(Path), []).
- fullpath([], Acc) ->
- filename:join(lists:reverse(Acc));
- fullpath([<<".">>|Tail], Acc) ->
- fullpath(Tail, Acc);
- fullpath([<<"..">>|Tail], Acc=[_]) ->
- fullpath(Tail, Acc);
- fullpath([<<"..">>|Tail], [_|Acc]) ->
- fullpath(Tail, Acc);
- fullpath([Segment|Tail], Acc) ->
- fullpath(Tail, [Segment|Acc]).
- %% @private Use application/octet-stream as the default mimetype.
- %% If a list of extension - mimetype pairs are provided as the mimetypes
- %% an attempt to find the mimetype using the file extension. If no match
- %% is found the default mimetype is returned.
- -spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
- [mimedef()].
- path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
- Ext = filename:extension(Filepath),
- case Ext of
- <<>> -> default_mimetype();
- _Ext -> path_to_mimetypes_(Ext, Extensions)
- end.
- -spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
- path_to_mimetypes_(Ext, Extensions) ->
- case lists:keyfind(cowboy_bstr:to_lower(Ext), 1, Extensions) of
- {_, MTs} -> MTs;
- _Unknown -> default_mimetype()
- end.
- -spec default_mimetype() -> [mimedef()].
- default_mimetype() ->
- [{<<"application">>, <<"octet-stream">>, []}].
- %% @private Do not send ETag headers in the default configuration.
- -spec no_etag_function([etagarg()], undefined) -> undefined.
- no_etag_function(_Args, undefined) ->
- undefined.
- %% @private A simple alternative is to send an ETag based on file attributes.
- -type fileattr() :: filepath | filesize | mtime | inode.
- -spec attr_etag_function([etagarg()], [fileattr()]) -> {strong, binary()}.
- attr_etag_function(Args, Attrs) ->
- [[_|H]|T] = [begin
- {_,Pair} = {_,{_,_}} = {Attr,lists:keyfind(Attr, 1, Args)},
- [$-|integer_to_list(erlang:phash2(Pair, 1 bsl 32), 16)]
- end || Attr <- Attrs],
- {strong, list_to_binary([H|T])}.
- -ifdef(TEST).
- -include_lib("eunit/include/eunit.hrl").
- -define(_eq(E, I), ?_assertEqual(E, I)).
- directory_path_test_() ->
- PL = fun(D) -> length(filename:split(directory_path(D))) end,
- Base = PL({priv_dir, cowboy, []}),
- LengthTests = [
- Base + 1, {priv_dir, cowboy, "a"},
- Base + 1, {priv_dir, cowboy, <<"a">>},
- Base + 1, {priv_dir, cowboy, [<<"a">>]},
- Base + 2, {priv_dir, cowboy, "a/b"},
- Base + 2, {priv_dir, cowboy, <<"a/b">>},
- Base + 2, {priv_dir, cowboy, [<<"a">>, <<"b">>]}
- ],
- TypeTests = [
- {priv_dir, cowboy, []},
- {priv_dir, cowboy, "a"},
- {priv_dir, cowboy, <<"a">>},
- {priv_dir, cowboy, [<<"a">>]},
- "a",
- <<"a">>,
- [<<"a">>]
- ],
- [{lists:flatten(io_lib:format("~p", [D])),
- fun() -> R = PL(D) end} || {R, D} <- LengthTests]
- ++ [{lists:flatten(io_lib:format("~p", [D])),
- fun() -> is_binary(directory_path(D)) end} || D <- TypeTests].
- filepath_path_test_() ->
- Tests = [
- {<<"a">>, "a"},
- {<<"a">>, <<"a">>},
- {<<"a">>, [<<"a">>]},
- {<<"a/b">>, "a/b"},
- {<<"a/b">>, <<"a/b">>},
- {<<"a/b">>, [<<"a">>, <<"b">>]}
- ],
- [{lists:flatten(io_lib:format("~p", [F])),
- fun() -> R = filepath_path(F) end} || {R, F} <- Tests].
- fullpath_test_() ->
- Tests = [
- {<<"/home/cowboy">>, <<"/home/cowboy">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/./">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
- {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
- {<<"/">>, <<"/home/cowboy/../../../../../..">>},
- {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
- ],
- [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
- good_path_check_test_() ->
- Tests = [
- <<"/home/cowboy/file">>,
- <<"/home/cowboy/file/">>,
- <<"/home/cowboy/./file">>,
- <<"/home/cowboy/././././././file">>,
- <<"/home/cowboy/abc/../file">>,
- <<"/home/cowboy/abc/../file">>,
- <<"/home/cowboy/abc/./.././file">>
- ],
- [{P, fun() ->
- case fullpath(P) of
- << "/home/cowboy/", _/binary >> -> ok
- end
- end} || P <- Tests].
- bad_path_check_test_() ->
- Tests = [
- <<"/home/cowboy/../../../../../../file">>,
- <<"/home/cowboy/../../etc/passwd">>
- ],
- [{P, fun() ->
- error = case fullpath(P) of
- << "/home/cowboy/", _/binary >> -> ok;
- _ -> error
- end
- end} || P <- Tests].
- good_path_win32_check_test_() ->
- Tests = case os:type() of
- {unix, _} ->
- [];
- {win32, _} ->
- [
- <<"c:/home/cowboy/file">>,
- <<"c:/home/cowboy/file/">>,
- <<"c:/home/cowboy/./file">>,
- <<"c:/home/cowboy/././././././file">>,
- <<"c:/home/cowboy/abc/../file">>,
- <<"c:/home/cowboy/abc/../file">>,
- <<"c:/home/cowboy/abc/./.././file">>
- ]
- end,
- [{P, fun() ->
- case fullpath(P) of
- << "c:/home/cowboy/", _/binary >> -> ok
- end
- end} || P <- Tests].
- bad_path_win32_check_test_() ->
- Tests = case os:type() of
- {unix, _} ->
- [];
- {win32, _} ->
- [
- <<"c:/home/cowboy/../../secretfile.bat">>,
- <<"c:/home/cowboy/c:/secretfile.bat">>,
- <<"c:/home/cowboy/..\\..\\secretfile.bat">>,
- <<"c:/home/cowboy/c:\\secretfile.bat">>
- ]
- end,
- [{P, fun() ->
- error = case fullpath(P) of
- << "c:/home/cowboy/", _/binary >> -> ok;
- _ -> error
- end
- end} || P <- Tests].
- -endif.
|