aboutsummaryrefslogtreecommitdiff
path: root/src/mod_http_fileserver.erl
diff options
context:
space:
mode:
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>2013-04-08 11:12:54 +0200
committerChristophe Romain <christophe.romain@process-one.net>2013-06-13 11:11:02 +0200
commit4d8f7706240a1603468968f47fc7b150b788d62f (patch)
tree92d55d789cc7ac979b3c9e161ffb7f908eba043a /src/mod_http_fileserver.erl
parentFix Guide: ejabberd_service expects a shaper_rule, not a shaper (diff)
Switch to rebar build tool
Use dynamic Rebar configuration Make iconv dependency optional Disable transient_supervisors compile option Add hipe compilation support Only compile ibrowse and lhttpc when needed Make it possible to generate an OTP application release Add --enable-debug compile option Add --enable-all compiler option Add --enable-tools configure option Add --with-erlang configure option. Add --enable-erlang-version-check configure option. Add lager support Improve the test suite
Diffstat (limited to 'src/mod_http_fileserver.erl')
-rw-r--r--src/mod_http_fileserver.erl455
1 files changed, 455 insertions, 0 deletions
diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl
new file mode 100644
index 000000000..f16256d4f
--- /dev/null
+++ b/src/mod_http_fileserver.erl
@@ -0,0 +1,455 @@
+%%%-------------------------------------------------------------------
+%%% File : mod_http_fileserver.erl
+%%% Author : Massimiliano Mirra <mmirra [at] process-one [dot] net>
+%%% Purpose : Simple file server plugin for embedded ejabberd web server
+%%% Created :
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2013 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_http_fileserver).
+
+-author('mmirra@process-one.net').
+
+-behaviour(gen_mod).
+-behaviour(gen_server).
+
+%% gen_mod callbacks
+-export([start/2, stop/1]).
+
+%% API
+-export([start_link/2]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+%% request_handlers callbacks
+-export([process/2]).
+
+%% ejabberd_hooks callbacks
+-export([reopen_log/1]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+
+-include("jlib.hrl").
+
+-include_lib("kernel/include/file.hrl").
+
+%%-include("ejabberd_http.hrl").
+%% TODO: When ejabberd-modules SVN gets the new ejabberd_http.hrl, delete this code:
+-record(request,
+ {method, path, q = [], us, auth, lang = <<"">>,
+ data = <<"">>, ip, host, port, tp, headers}).
+
+-record(state,
+ {host, docroot, accesslog, accesslogfd,
+ directory_indices, custom_headers, default_content_type,
+ content_types = []}).
+
+-define(PROCNAME, ejabberd_mod_http_fileserver).
+
+%% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data}
+-define(HTTP_ERR_FILE_NOT_FOUND,
+ {-1, 404, [], <<"Not found">>}).
+
+-define(HTTP_ERR_FORBIDDEN,
+ {-1, 403, [], <<"Forbidden">>}).
+
+-define(DEFAULT_CONTENT_TYPE,
+ <<"application/octet-stream">>).
+
+-define(DEFAULT_CONTENT_TYPES,
+ [{<<".css">>, <<"text/css">>},
+ {<<".gif">>, <<"image/gif">>},
+ {<<".html">>, <<"text/html">>},
+ {<<".jar">>, <<"application/java-archive">>},
+ {<<".jpeg">>, <<"image/jpeg">>},
+ {<<".jpg">>, <<"image/jpeg">>},
+ {<<".js">>, <<"text/javascript">>},
+ {<<".png">>, <<"image/png">>},
+ {<<".svg">>, <<"image/svg+xml">>},
+ {<<".txt">>, <<"text/plain">>},
+ {<<".xml">>, <<"application/xml">>},
+ {<<".xpi">>, <<"application/x-xpinstall">>},
+ {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]).
+
+-compile(export_all).
+
+%%====================================================================
+%% gen_mod callbacks
+%%====================================================================
+
+start(Host, Opts) ->
+ Proc = get_proc_name(Host),
+ ChildSpec =
+ {Proc,
+ {?MODULE, start_link, [Host, Opts]},
+ transient, % if process crashes abruptly, it gets restarted
+ 1000,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+ Proc = get_proc_name(Host),
+ gen_server:call(Proc, stop),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+start_link(Host, Opts) ->
+ Proc = get_proc_name(Host),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% Description: Initiates the server
+%%--------------------------------------------------------------------
+init([Host, Opts]) ->
+ try initialize(Host, Opts) of
+ {DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
+ CustomHeaders, DefaultContentType, ContentTypes} ->
+ {ok, #state{host = Host,
+ accesslog = AccessLog,
+ accesslogfd = AccessLogFD,
+ docroot = DocRoot,
+ directory_indices = DirectoryIndices,
+ custom_headers = CustomHeaders,
+ default_content_type = DefaultContentType,
+ content_types = ContentTypes}}
+ catch
+ throw:Reason ->
+ {stop, Reason}
+ end.
+
+initialize(Host, Opts) ->
+ DocRoot = gen_mod:get_opt(docroot, Opts, fun(A) -> A end, undefined),
+ check_docroot_defined(DocRoot, Host),
+ DRInfo = check_docroot_exists(DocRoot),
+ check_docroot_is_dir(DRInfo, DocRoot),
+ check_docroot_is_readable(DRInfo, DocRoot),
+ AccessLog = gen_mod:get_opt(accesslog, Opts,
+ fun iolist_to_binary/1,
+ undefined),
+ AccessLogFD = try_open_log(AccessLog, Host),
+ DirectoryIndices = gen_mod:get_opt(directory_indices, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ CustomHeaders = gen_mod:get_opt(custom_headers, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ DefaultContentType = gen_mod:get_opt(default_content_type, Opts,
+ fun iolist_to_binary/1,
+ ?DEFAULT_CONTENT_TYPE),
+ ContentTypes = build_list_content_types(
+ gen_mod:get_opt(content_types, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ ?DEFAULT_CONTENT_TYPES),
+ ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),%+++
+ {DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
+ CustomHeaders, DefaultContentType, ContentTypes}.
+
+
+%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT]
+%% where CT = {Extension::string(), Value}
+%% Value = string() | undefined
+%% @doc Return a unified list without duplicates.
+%% Elements of AdminCTs have more priority.
+%% If a CT is declared as 'undefined', then it is not included in the result.
+build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) ->
+ AdminCTs = lists:ukeysort(1, AdminCTsUnsorted),
+ DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted),
+ CTsUnfiltered = lists:ukeymerge(1, AdminCTs,
+ DefaultCTs),
+ [{Extension, Value}
+ || {Extension, Value} <- CTsUnfiltered,
+ Value /= undefined].
+
+check_docroot_defined(DocRoot, Host) ->
+ case DocRoot of
+ undefined -> throw({undefined_docroot_option, Host});
+ _ -> ok
+ end.
+
+check_docroot_exists(DocRoot) ->
+ case file:read_file_info(DocRoot) of
+ {error, Reason} ->
+ throw({error_access_docroot, DocRoot, Reason});
+ {ok, FI} -> FI
+ end.
+
+check_docroot_is_dir(DRInfo, DocRoot) ->
+ case DRInfo#file_info.type of
+ directory -> ok;
+ _ -> throw({docroot_not_directory, DocRoot})
+ end.
+
+check_docroot_is_readable(DRInfo, DocRoot) ->
+ case DRInfo#file_info.access of
+ read -> ok;
+ read_write -> ok;
+ _ -> throw({docroot_not_readable, DocRoot})
+ end.
+
+try_open_log(undefined, _Host) ->
+ undefined;
+try_open_log(FN, Host) ->
+ FD = try open_log(FN) of
+ FD1 -> FD1
+ catch
+ throw:{cannot_open_accesslog, FN, Reason} ->
+ ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
+ undefined
+ end,
+ ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50),
+ FD.
+
+%%--------------------------------------------------------------------
+%% Function: handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% Description: Handling call messages
+%%--------------------------------------------------------------------
+handle_call({serve, LocalPath}, _From, State) ->
+ Reply = serve(LocalPath, State#state.docroot, State#state.directory_indices,
+ State#state.custom_headers,
+ State#state.default_content_type, State#state.content_types),
+ {reply, Reply, State};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling cast messages
+%%--------------------------------------------------------------------
+handle_cast({add_to_log, FileSize, Code, Request}, State) ->
+ add_to_log(State#state.accesslogfd, FileSize, Code, Request),
+ {noreply, State};
+handle_cast(reopen_log, State) ->
+ FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
+ {noreply, State#state{accesslogfd = FD2}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling all non call/cast messages
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description: This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%%--------------------------------------------------------------------
+terminate(_Reason, State) ->
+ close_log(State#state.accesslogfd),
+ ejabberd_hooks:delete(reopen_log_hook, State#state.host, ?MODULE, reopen_log, 50),
+ ok.
+
+%%--------------------------------------------------------------------
+%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%====================================================================
+%% request_handlers callbacks
+%%====================================================================
+
+%% @spec (LocalPath, Request) -> {HTTPCode::integer(), [Header], Page::string()}
+%% @doc Handle an HTTP request.
+%% LocalPath is the part of the requested URL path that is "local to the module".
+%% Returns the page to be sent back to the client and/or HTTP status code.
+process(LocalPath, Request) ->
+ ?DEBUG("Requested ~p", [LocalPath]),
+ try gen_server:call(get_proc_name(Request#request.host), {serve, LocalPath}) of
+ {FileSize, Code, Headers, Contents} ->
+ add_to_log(FileSize, Code, Request),
+ {Code, Headers, Contents}
+ catch
+ exit:{noproc, _} ->
+ ?ERROR_MSG("Received an HTTP request with Host ~p, but couldn't find the related "
+ "ejabberd virtual host", [Request#request.host]),
+ ejabberd_web:error(not_found)
+ end.
+
+serve(LocalPath, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes) ->
+ FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
+ case file:read_file_info(FileName) of
+ {error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND;
+ {error, eacces} -> ?HTTP_ERR_FORBIDDEN;
+ {ok, #file_info{type = directory}} -> serve_index(FileName,
+ DirectoryIndices,
+ CustomHeaders,
+ DefaultContentType,
+ ContentTypes);
+ {ok, FileInfo} -> serve_file(FileInfo, FileName,
+ CustomHeaders,
+ DefaultContentType,
+ ContentTypes)
+ end.
+
+%% Troll through the directory indices attempting to find one which
+%% works, if none can be found, return a 404.
+serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) ->
+ ?HTTP_ERR_FILE_NOT_FOUND;
+serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) ->
+ IndexFileName = filename:join([FileName] ++ [Index]),
+ case file:read_file_info(IndexFileName) of
+ {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
+ {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
+ {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes)
+ end.
+
+%% Assume the file exists if we got this far and attempt to read it in
+%% and serve it up.
+serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) ->
+ ?DEBUG("Delivering: ~s", [FileName]),
+ ContentType = content_type(FileName, DefaultContentType,
+ ContentTypes),
+ {ok, FileContents} = file:read_file(FileName),
+ {FileInfo#file_info.size, 200,
+ [{<<"Server">>, <<"ejabberd">>},
+ {<<"Last-Modified">>, last_modified(FileInfo)},
+ {<<"Content-Type">>, ContentType}
+ | CustomHeaders],
+ FileContents}.
+
+%%----------------------------------------------------------------------
+%% Log file
+%%----------------------------------------------------------------------
+
+open_log(FN) ->
+ case file:open(FN, [append]) of
+ {ok, FD} ->
+ FD;
+ {error, Reason} ->
+ throw({cannot_open_accesslog, FN, Reason})
+ end.
+
+close_log(FD) ->
+ file:close(FD).
+
+reopen_log(undefined, undefined) ->
+ ok;
+reopen_log(FN, FD) ->
+ close_log(FD),
+ open_log(FN).
+
+reopen_log(Host) ->
+ gen_server:cast(get_proc_name(Host), reopen_log).
+
+add_to_log(FileSize, Code, Request) ->
+ gen_server:cast(get_proc_name(Request#request.host),
+ {add_to_log, FileSize, Code, Request}).
+
+add_to_log(undefined, _FileSize, _Code, _Request) ->
+ ok;
+add_to_log(File, FileSize, Code, Request) ->
+ {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
+ IP = ip_to_string(element(1, Request#request.ip)),
+ Path = join(Request#request.path, "/"),
+ Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", binary_to_list(element(2, E))]) end,
+ Request#request.q), "&") of
+ [] ->
+ "";
+ String ->
+ [$? | String]
+ end,
+ UserAgent = find_header('User-Agent', Request#request.headers, "-"),
+ Referer = find_header('Referer', Request#request.headers, "-"),
+ %% Pseudo Combined Apache log format:
+ %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
+ %% TODO some fields are harcoded/missing:
+ %% The date/time integers should have always 2 digits. For example day "7" should be "07"
+ %% Month should be 3*letter, not integer 1..12
+ %% Missing time zone = (`+' | `-') 4*digit
+ %% Missing protocol version: HTTP/1.1
+ %% For reference: http://httpd.apache.org/docs/2.2/logs.html
+ io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p ~p ~p ~p~n",
+ [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
+ FileSize, Referer, UserAgent]).
+
+find_header(Header, Headers, Default) ->
+ case lists:keysearch(Header, 1, Headers) of
+ {value, {_, Value}} -> Value;
+ false -> Default
+ end.
+
+%%----------------------------------------------------------------------
+%% Utilities
+%%----------------------------------------------------------------------
+
+get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
+
+join([], _) ->
+ <<"">>;
+join([E], _) ->
+ E;
+join([H | T], Separator) ->
+ [H2 | T2] = case is_binary(H) of true -> [binary_to_list(I)||I<-[H|T]]; false -> [H | T] end,
+ Res=lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H2, T2),
+ case is_binary(H) of true -> list_to_binary(Res); false -> Res end.
+
+content_type(Filename, DefaultContentType, ContentTypes) ->
+ Extension = str:to_lower(filename:extension(Filename)),
+ case lists:keysearch(Extension, 1, ContentTypes) of
+ {value, {_, ContentType}} -> ContentType;
+ false -> DefaultContentType
+ end.
+
+last_modified(FileInfo) ->
+ Then = FileInfo#file_info.mtime,
+ httpd_util:rfc1123_date(Then).
+
+%% Convert IP address tuple to string representation. Accepts either
+%% IPv4 or IPv6 address tuples.
+ip_to_string(Address) when size(Address) == 4 ->
+ join(tuple_to_list(Address), ".");
+ip_to_string(Address) when size(Address) == 8 ->
+ Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)),
+ string:to_lower(lists:flatten(join(Parts, ":"))).