Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

added option to automatically send files from a static directory

  • Loading branch information...
commit 682b8a30c4944abf78de92dea8d3354be33413f7 1 parent 7607709
@ostinelli authored
View
2  README.txt
@@ -89,6 +89,8 @@ CHANGELOG
- added access log callback function, so that main application can log HTTP access
- added streaming input for big files or endless input, using a manual body_recv function in
conjunction with the {auto_recv_body, false} option
+ - added static directory support, so that GET requests to /static/* can automatically send files
+ from a specified directory (thanks to egobrain suggestion)
- consistently improved memory usage by not copying by default to handler processes the full request
or websocket record
- added configuration option to set which websocket versions must be supported by the server
View
48 examples/misultin_static.erl
@@ -0,0 +1,48 @@
+% ==========================================================================================================
+% MISULTIN - Example: static directory support.
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
+% All rights reserved.
+%
+% BSD License
+%
+% Redistribution and use in source and binary forms, with or without modification, are permitted provided
+% that the following conditions are met:
+%
+% * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+% following disclaimer.
+% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+% the following disclaimer in the documentation and/or other materials provided with the distribution.
+% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote
+% products derived from this software without specific prior written permission.
+%
+% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+% POSSIBILITY OF SUCH DAMAGE.
+% ==========================================================================================================
+-module(misultin_static).
+-export([start/1, stop/0]).
+
+% start misultin http server, with a static option
+% all files under the specified directory /var/www/static will be automatically served.
+start(Port) ->
+ misultin:start_link([
+ {port, Port},
+ {static, "/var/www/static"},
+ {loop, fun(Req) -> handle_http(Req) end}
+ ]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+% callback function called on incoming http request, if not a static request
+handle_http(Req) ->
+ Req:ok("Not a static file request.").
View
3  include/misultin.hrl
@@ -155,7 +155,8 @@
access_log = undefined :: undefined | function(), % access log function
ws_force_ssl = false :: boolean(), % if we are deployed behind stunnel, or other ssl proxy
proxy_protocol = false :: boolean(), % upstream proxy is sending us http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
- auto_recv_body = true :: boolean() % if set to false, body has to be manually read in loop
+ auto_recv_body = true :: boolean(), % if set to false, body has to be manually read in loop
+ static = false :: boolean() % if set to a directory, then all files in it will be automatically sent to the browser.
}).
% Request
View
17 src/misultin.erl
@@ -117,9 +117,10 @@ init([Options]) ->
{ws_loop, undefined, fun is_function/1, ws_loop_not_function},
{ws_autoexit, true, fun is_boolean/1, invalid_ws_autoexit_option},
{ws_versions, ['draft-hybi-17', 'draft-hybi-10', 'draft-hixie-76'], fun check_ws_version/1, unsupported_ws_vsn_specified},
- {sessions_expire, 600, fun is_non_neg_integer/1, invalid_sessions_expire},
- {access_log, undefined, fun check_access_log/1, invalid_access_log},
- {auto_recv_body, true, fun is_boolean/1, invalid_auto_recv_body}
+ {sessions_expire, 600, fun is_non_neg_integer/1, invalid_sessions_expire_option},
+ {access_log, undefined, fun check_access_log/1, invalid_access_log_option},
+ {auto_recv_body, true, fun is_boolean/1, invalid_auto_recv_body_option},
+ {static, false, fun check_static/1, invalid_static_option} % if set to a directory, requests to /static/* will automatically send files from the directory
],
OptionsVerified = lists:foldl(fun(OptionProp, Acc) -> [get_option(OptionProp, Options)|Acc] end, [], OptionProps),
case proplists:get_value(error, OptionsVerified) of
@@ -148,6 +149,7 @@ init([Options]) ->
SessionsExpireSec = proplists:get_value(sessions_expire, OptionsVerified),
AccessLogFun = proplists:get_value(access_log, OptionsVerified),
AutoRecvBody = proplists:get_value(auto_recv_body, OptionsVerified),
+ Static = proplists:get_value(static, OptionsVerified),
% set additional options according to socket mode if necessary
Continue = case SslOptions0 of
false ->
@@ -203,7 +205,8 @@ init([Options]) ->
access_log = AccessLogFun,
ws_force_ssl = WsForceSsl,
proxy_protocol = ProxyProtocol,
- auto_recv_body = AutoRecvBody
+ auto_recv_body = AutoRecvBody,
+ static = Static
},
% define misultin_server supervisor specs
ServerSpec = {server, {misultin_server, start_link, [{MaxConnections}]}, permanent, 60000, worker, [misultin_server]},
@@ -300,6 +303,12 @@ check_ws_version(WsVsn) ->
is_non_neg_integer(N) when is_integer(N), N >= 0 -> true;
is_non_neg_integer(_) -> false.
+% check if the static option is valid
+-spec check_static(list() | false) -> boolean().
+check_static(false) -> true;
+check_static(Path) when is_list(Path) -> true;
+check_static(_) -> false.
+
% Validate and get misultin options.
-spec get_option({
OptionName::atom(),
View
50 src/misultin_http.erl
@@ -58,7 +58,8 @@
ws_versions = undefined :: [websocket_version()],
access_log = undefined :: undefined | function(),
ws_force_ssl = false :: boolean(),
- auto_recv_body = true :: boolean()
+ auto_recv_body = true :: boolean(),
+ static = false :: boolean()
}).
-record(req_options, {
comet = false :: boolean() % if comet =:= true, we will monitor client tcp close
@@ -101,7 +102,8 @@ handle_data(ServerRef, SessionsRef, TableDateRef, Sock, SocketMode, ListenPort,
ws_versions = CustomOpts#custom_opts.ws_versions,
access_log = CustomOpts#custom_opts.access_log,
ws_force_ssl = CustomOpts#custom_opts.ws_force_ssl,
- auto_recv_body = CustomOpts#custom_opts.auto_recv_body
+ auto_recv_body = CustomOpts#custom_opts.auto_recv_body,
+ static = CustomOpts#custom_opts.static
},
Req = #req{socket = Sock, socket_mode = SocketMode, peer_addr = PeerAddr, peer_port = PeerPort, peer_cert = PeerCert},
% enter loop
@@ -326,7 +328,7 @@ method_dispatch(C, #req{method = Method} = Req) when Method =:= 'GET'; Method =:
method_dispatch(C, #req{method = Method} = Req) when Method =:= 'TRACE' ->
?LOG_DEBUG("~p request received", [Method]),
% an entity-body is explicitly forbidden in TRACE requests <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8>
- call_mfa(C, Req),
+ main_dispatcher(C, Req),
handle_keepalive(Req#req.connection, C, Req);
method_dispatch(C, #req{socket = Sock, socket_mode = SocketMode, method = _Method, connection = Connection} = Req) ->
?LOG_DEBUG("method not implemented: ~p", [_Method]),
@@ -344,7 +346,7 @@ read_body_dispatch(#c{auto_recv_body = AutoRecvBody} = C, #req{socket = Sock, so
{ok, Bin} ->
?LOG_DEBUG("full body read: ~p", [Bin]),
Req0 = Req#req{body = Bin},
- call_mfa(C, Req0),
+ main_dispatcher(C, Req0),
handle_keepalive(Req#req.connection, C, Req0);
{error, timeout} ->
?LOG_WARNING("request timeout, sending error", []),
@@ -361,7 +363,7 @@ read_body_dispatch(#c{auto_recv_body = AutoRecvBody} = C, #req{socket = Sock, so
end;
false ->
?LOG_DEBUG("auto_recv_body set to false, do not read body of request",[]),
- call_mfa(C, Req),
+ main_dispatcher(C, Req),
handle_keepalive(Req#req.connection, C, Req)
end.
@@ -522,9 +524,19 @@ handle_keepalive(close, _C, #req{socket = Sock, socket_mode = SocketMode} = _Req
handle_keepalive(keep_alive, C, #req{socket = Sock, socket_mode = SocketMode} = Req) ->
request(C, #req{socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, peer_cert = Req#req.peer_cert}).
-% Main dispatcher
--spec call_mfa(C::#c{}, Req::#req{}) -> closed | true.
-call_mfa(#c{table_date_ref = TableDateRef, loop = Loop, autoexit = AutoExit} = C, Req) ->
+% File dispatcher
+main_dispatcher(#c{static = false} = C, Req) ->
+ % no static option specified
+ process_dispatcher(C, Req);
+main_dispatcher(#c{static = StaticDir, loop = Loop} = C, Req) ->
+ % static directory specified
+ StaticLoop = fun(ReqT) -> handle_static(ReqT, StaticDir, Loop) end,
+ % replace original loop with static loop
+ process_dispatcher(C#c{loop = StaticLoop}, Req).
+
+% Process dispatcher
+-spec process_dispatcher(C::#c{}, Req::#req{}) -> closed | true.
+process_dispatcher(#c{table_date_ref = TableDateRef, loop = Loop, autoexit = AutoExit} = C, Req) ->
% spawn_link custom loop
Self = self(),
% trap exit
@@ -912,4 +924,26 @@ build_access_data_do(PeerAddr, RequestLine, TableDateRef) ->
build_full_uri(Uri, []) -> Uri;
build_full_uri(Uri, Args) -> lists:concat([Uri, "?", Args]).
+% ---------------------------- \/ Static File Handler ------------------------------------------------------
+
+% test if this is a static file request, otherwise fallback to original loop
+handle_static(Req, StaticDir, Loop) ->
+ ?LOG_DEBUG("checking if this is a request to a static file",[]),
+ handle_static(Req:get(method), Req:resource([lowercase, urldecode]), Req, StaticDir, Loop).
+handle_static('GET', ["static" | FilePath], Req, StaticDir, _Loop) ->
+ ?LOG_DEBUG("static request found, sanitizing path",[]),
+ case misultin_utility:sanitize_path_tokens(FilePath) of
+ invalid ->
+ ?LOG_DEBUG("invalid static request :~p", [FilePath]),
+ Req:respond(403);
+ SanitizedPath ->
+ FullFilePath = filename:join([StaticDir, filename:join(SanitizedPath)]),
+ ?LOG_DEBUG("sending file in path: ~p", [FullFilePath]),
+ Req:file(FullFilePath)
+ end;
+handle_static(_, _, Req, _, Loop) ->
+ Loop(Req).
+
+% ---------------------------- /\ Static File Handler ------------------------------------------------------
+
% ============================ /\ INTERNAL FUNCTIONS =======================================================
View
60 src/misultin_utility.erl
@@ -37,6 +37,7 @@
-export([parse_qs/1, parse_qs/2, unquote/1, quote_plus/1, get_peer/2]).
-export([convert_ip_to_list/1]).
-export([hexstr/1, get_unix_timestamp/0, get_unix_timestamp/1]).
+-export([sanitize_path_tokens/1]).
% macros
-define(INTERNAL_TIMEOUT, 30000).
@@ -538,6 +539,11 @@ get_peer(Headers, ConnectionPeerAddr) ->
end
end.
+% sanitize path tokens to avoid directory traversal attacks
+-spec sanitize_path_tokens(Path::list(string())) -> invalid | list(string()).
+sanitize_path_tokens(Path) ->
+ i_sanitize_path_tokens(Path).
+
% ============================ /\ API ======================================================================
@@ -616,4 +622,58 @@ quote_plus([C | Rest], Acc) ->
<<Hi:4, Lo:4>> = <<C>>,
quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]).
+-spec i_sanitize_path_tokens(Path::list(string())) -> invalid | list(string()).
+i_sanitize_path_tokens(Path) ->
+ % ensure no backslash \ character is included in path tokens
+ F = fun(S) ->
+ case string:str(S, "\\") of
+ 0 -> false;
+ _ -> true
+ end
+ end,
+ case lists:any(F, Path) of
+ true ->
+ % backslash found
+ invalid;
+ _ ->
+ % proceed to check for sanity
+ i_sanitize_path_tokens(lists:reverse(Path), 0, [])
+ end.
+i_sanitize_path_tokens([], RemCount, _Acc) when RemCount > 0 ->
+ invalid;
+i_sanitize_path_tokens([], _RemCount, Acc) ->
+ Acc;
+i_sanitize_path_tokens([".."|[]], _RemCount, _Acc) ->
+ invalid;
+i_sanitize_path_tokens([".."|T], RemCount, Acc) ->
+ i_sanitize_path_tokens(T, RemCount + 1, Acc);
+i_sanitize_path_tokens([_H|T], RemCount, Acc) when RemCount > 0 ->
+ i_sanitize_path_tokens(T, RemCount - 1, Acc);
+i_sanitize_path_tokens([H|T], RemCount, Acc) ->
+ i_sanitize_path_tokens(T, RemCount, [H|Acc]).
+
% ============================ /\ INTERNAL FUNCTIONS =======================================================
+
+
+% ============================ \/ TESTS ====================================================================
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+i_sanitize_path_tokens_test() ->
+ ?assertEqual(["one", "two", "three"], i_sanitize_path_tokens(["one", "two", "three"])),
+ ?assertEqual(["one", "three"], i_sanitize_path_tokens(["one", "two", "..", "three"])),
+ ?assertEqual(["three"], i_sanitize_path_tokens(["one", "two", "..", "..", "three"])),
+ ?assertEqual(["three"], i_sanitize_path_tokens(["one", "two", "..", "..", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["one", "two", "..", "..", "..", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["one", "..", "..", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["..", "..", "..", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["..", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens([".."])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["..", "..", "one", "two", "three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["one", "two", "\\three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["one", "..", "\\three"])),
+ ?assertEqual(invalid, i_sanitize_path_tokens(["\\..\\one"])).
+-endif.
+
+% ============================ /\ TESTS ====================================================================
Please sign in to comment.
Something went wrong with that request. Please try again.