2009-06-16 15:45:33 +02:00
|
|
|
%%%-------------------------------------------------------------------
|
2009-06-16 15:44:26 +02:00
|
|
|
%%% File : mod_http_fileserver.erl
|
|
|
|
%%% Author : Massimiliano Mirra <mmirra [at] process-one [dot] net>
|
|
|
|
%%% Purpose : Simple file server plugin for embedded ejabberd web server
|
2009-06-16 15:45:57 +02:00
|
|
|
%%% Created :
|
|
|
|
%%%
|
|
|
|
%%%
|
2020-01-28 13:34:02 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2020 ProcessOne
|
2009-06-16 15:45:57 +02:00
|
|
|
%%%
|
|
|
|
%%% 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.
|
|
|
|
%%%
|
2014-02-22 11:27:40 +01:00
|
|
|
%%% 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.,
|
|
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
2009-06-16 15:45:57 +02:00
|
|
|
%%%
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
2009-06-16 15:44:26 +02:00
|
|
|
-module(mod_http_fileserver).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2009-06-16 15:44:26 +02:00
|
|
|
-author('mmirra@process-one.net').
|
|
|
|
|
|
|
|
-behaviour(gen_mod).
|
2009-06-16 15:45:33 +02:00
|
|
|
-behaviour(gen_server).
|
|
|
|
|
|
|
|
%% gen_mod callbacks
|
2017-02-22 17:46:47 +01:00
|
|
|
-export([start/2, stop/1, reload/3]).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
%% 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]).
|
2009-06-16 15:44:26 +02:00
|
|
|
|
2015-11-23 11:53:36 +01:00
|
|
|
%% utility for other http modules
|
|
|
|
-export([content_type/3]).
|
|
|
|
|
2020-01-08 10:24:51 +01:00
|
|
|
-export([reopen_log/0, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
|
2009-06-16 15:44:26 +02:00
|
|
|
|
2013-04-08 11:12:54 +02:00
|
|
|
-include("logger.hrl").
|
2014-10-16 13:51:13 +02:00
|
|
|
-include("ejabberd_http.hrl").
|
2009-06-16 15:45:03 +02:00
|
|
|
-include_lib("kernel/include/file.hrl").
|
2020-01-08 10:24:51 +01:00
|
|
|
-include("translate.hrl").
|
2009-06-16 15:44:26 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-record(state,
|
|
|
|
{host, docroot, accesslog, accesslogfd,
|
|
|
|
directory_indices, custom_headers, default_content_type,
|
2016-11-17 12:59:27 +01:00
|
|
|
content_types = [], user_access = none}).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
%% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data}
|
2013-03-14 10:33:02 +01:00
|
|
|
-define(HTTP_ERR_FILE_NOT_FOUND,
|
|
|
|
{-1, 404, [], <<"Not found">>}).
|
|
|
|
|
2017-07-28 16:07:54 +02:00
|
|
|
-define(REQUEST_AUTH_HEADERS,
|
|
|
|
[{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]).
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-define(HTTP_ERR_FORBIDDEN,
|
|
|
|
{-1, 403, [], <<"Forbidden">>}).
|
2017-07-28 16:07:54 +02:00
|
|
|
-define(HTTP_ERR_REQUEST_AUTH,
|
|
|
|
{-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}).
|
2017-10-05 10:33:29 +02:00
|
|
|
-define(HTTP_ERR_HOST_UNKNOWN,
|
|
|
|
{-1, 410, [], <<"Host unknown">>}).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
|
|
|
-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">>}]).
|
2009-07-21 19:31:09 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
%%====================================================================
|
|
|
|
%% gen_mod callbacks
|
|
|
|
%%====================================================================
|
|
|
|
|
|
|
|
start(Host, Opts) ->
|
2017-02-14 10:39:26 +01:00
|
|
|
gen_mod:start_child(?MODULE, Host, Opts).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
stop(Host) ->
|
2017-02-14 10:39:26 +01:00
|
|
|
gen_mod:stop_child(?MODULE, Host).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
2017-02-22 17:46:47 +01:00
|
|
|
reload(Host, NewOpts, OldOpts) ->
|
|
|
|
Proc = get_proc_name(Host),
|
|
|
|
gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}).
|
|
|
|
|
2016-07-06 13:58:48 +02:00
|
|
|
depends(_Host, _Opts) ->
|
|
|
|
[].
|
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
%%====================================================================
|
|
|
|
%% gen_server callbacks
|
|
|
|
%%====================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% Function: init(Args) -> {ok, State} |
|
|
|
|
%% {ok, State, Timeout} |
|
|
|
|
%% ignore |
|
|
|
|
%% {stop, Reason}
|
|
|
|
%% Description: Initiates the server
|
|
|
|
%%--------------------------------------------------------------------
|
2019-08-04 20:46:18 +02:00
|
|
|
init([Host|_]) ->
|
|
|
|
Opts = gen_mod:get_module_opts(Host, ?MODULE),
|
2009-06-16 15:45:33 +02:00
|
|
|
try initialize(Host, Opts) of
|
2017-02-22 17:46:47 +01:00
|
|
|
State ->
|
2017-02-14 08:25:08 +01:00
|
|
|
process_flag(trap_exit, true),
|
2017-02-22 17:46:47 +01:00
|
|
|
{ok, State}
|
2009-06-16 15:45:33 +02:00
|
|
|
catch
|
|
|
|
throw:Reason ->
|
|
|
|
{stop, Reason}
|
|
|
|
end.
|
|
|
|
|
|
|
|
initialize(Host, Opts) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
DocRoot = mod_http_fileserver_opt:docroot(Opts),
|
|
|
|
AccessLog = mod_http_fileserver_opt:accesslog(Opts),
|
2009-06-16 15:45:33 +02:00
|
|
|
AccessLogFD = try_open_log(AccessLog, Host),
|
2019-06-14 11:33:26 +02:00
|
|
|
DirectoryIndices = mod_http_fileserver_opt:directory_indices(Opts),
|
|
|
|
CustomHeaders = mod_http_fileserver_opt:custom_headers(Opts),
|
|
|
|
DefaultContentType = mod_http_fileserver_opt:default_content_type(Opts),
|
|
|
|
UserAccess0 = mod_http_fileserver_opt:must_authenticate_with(Opts),
|
2016-11-17 12:59:27 +01:00
|
|
|
UserAccess = case UserAccess0 of
|
|
|
|
[] -> none;
|
|
|
|
_ ->
|
2019-07-08 08:55:32 +02:00
|
|
|
maps:from_list(UserAccess0)
|
2016-11-17 12:59:27 +01:00
|
|
|
end,
|
2013-03-14 10:33:02 +01:00
|
|
|
ContentTypes = build_list_content_types(
|
2019-06-14 11:33:26 +02:00
|
|
|
mod_http_fileserver_opt:content_types(Opts),
|
2013-03-14 10:33:02 +01:00
|
|
|
?DEFAULT_CONTENT_TYPES),
|
2019-09-23 14:17:20 +02:00
|
|
|
?DEBUG("Known content types: ~ts",
|
2017-02-24 18:57:24 +01:00
|
|
|
[str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes],
|
|
|
|
<<", ">>)]),
|
2017-02-22 17:46:47 +01:00
|
|
|
#state{host = Host,
|
|
|
|
accesslog = AccessLog,
|
|
|
|
accesslogfd = AccessLogFD,
|
|
|
|
docroot = DocRoot,
|
|
|
|
directory_indices = DirectoryIndices,
|
|
|
|
custom_headers = CustomHeaders,
|
|
|
|
default_content_type = DefaultContentType,
|
|
|
|
content_types = ContentTypes,
|
|
|
|
user_access = UserAccess}.
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2009-07-21 19:31:09 +02:00
|
|
|
%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT]
|
|
|
|
%% where CT = {Extension::string(), Value}
|
|
|
|
%% Value = string() | undefined
|
2009-08-17 19:16:43 +02:00
|
|
|
%% @doc Return a unified list without duplicates.
|
|
|
|
%% Elements of AdminCTs have more priority.
|
2009-07-21 19:31:09 +02:00
|
|
|
%% 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),
|
2013-03-14 10:33:02 +01:00
|
|
|
CTsUnfiltered = lists:ukeymerge(1, AdminCTs,
|
|
|
|
DefaultCTs),
|
|
|
|
[{Extension, Value}
|
|
|
|
|| {Extension, Value} <- CTsUnfiltered,
|
|
|
|
Value /= undefined].
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
try_open_log(undefined, _Host) ->
|
|
|
|
undefined;
|
2017-01-09 15:02:17 +01:00
|
|
|
try_open_log(FN, _Host) ->
|
2009-06-16 15:45:33 +02:00
|
|
|
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,
|
2017-01-09 15:02:17 +01:00
|
|
|
ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50),
|
2009-06-16 15:45:33 +02:00
|
|
|
FD.
|
2009-06-16 15:44:32 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% 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
|
|
|
|
%%--------------------------------------------------------------------
|
2017-03-17 11:58:32 +01:00
|
|
|
handle_call({serve, LocalPath, Auth, RHeaders}, _From, State) ->
|
|
|
|
IfModifiedSince = case find_header('If-Modified-Since', RHeaders, bad_date) of
|
|
|
|
bad_date ->
|
|
|
|
bad_date;
|
|
|
|
Val ->
|
|
|
|
httpd_util:convert_request_date(binary_to_list(Val))
|
|
|
|
end,
|
2016-11-17 12:59:27 +01:00
|
|
|
Reply = serve(LocalPath, Auth, State#state.docroot, State#state.directory_indices,
|
2009-11-23 13:00:46 +01:00
|
|
|
State#state.custom_headers,
|
2016-11-17 12:59:27 +01:00
|
|
|
State#state.default_content_type, State#state.content_types,
|
2017-03-17 11:58:32 +01:00
|
|
|
State#state.user_access, IfModifiedSince),
|
2009-06-16 15:45:33 +02:00
|
|
|
{reply, Reply, State};
|
2019-07-12 10:55:36 +02:00
|
|
|
handle_call(Request, From, State) ->
|
|
|
|
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
|
|
|
|
{noreply, State}.
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
|
|
%% {noreply, State, Timeout} |
|
|
|
|
%% {stop, Reason, State}
|
|
|
|
%% Description: Handling cast messages
|
|
|
|
%%--------------------------------------------------------------------
|
2009-06-16 15:45:51 +02:00
|
|
|
handle_cast({add_to_log, FileSize, Code, Request}, State) ->
|
|
|
|
add_to_log(State#state.accesslogfd, FileSize, Code, Request),
|
2009-06-16 15:45:33 +02:00
|
|
|
{noreply, State};
|
|
|
|
handle_cast(reopen_log, State) ->
|
|
|
|
FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
|
|
|
|
{noreply, State#state{accesslogfd = FD2}};
|
2017-02-22 17:46:47 +01:00
|
|
|
handle_cast({reload, Host, NewOpts, _OldOpts}, OldState) ->
|
|
|
|
try initialize(Host, NewOpts) of
|
|
|
|
NewState ->
|
|
|
|
FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd),
|
|
|
|
{noreply, NewState#state{accesslogfd = FD}}
|
|
|
|
catch throw:_ ->
|
|
|
|
{noreply, OldState}
|
|
|
|
end;
|
|
|
|
handle_cast(Msg, State) ->
|
2019-06-24 19:32:34 +02:00
|
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
2009-06-16 15:45:33 +02:00
|
|
|
{noreply, State}.
|
|
|
|
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
|
|
%% {noreply, State, Timeout} |
|
|
|
|
%% {stop, Reason, State}
|
|
|
|
%% Description: Handling all non call/cast messages
|
|
|
|
%%--------------------------------------------------------------------
|
2019-07-12 10:55:36 +02:00
|
|
|
handle_info(Info, State) ->
|
|
|
|
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
2009-06-16 15:45:33 +02:00
|
|
|
{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.
|
|
|
|
%%--------------------------------------------------------------------
|
2019-07-14 15:16:13 +02:00
|
|
|
terminate(_Reason, #state{host = Host} = State) ->
|
2009-06-16 15:45:33 +02:00
|
|
|
close_log(State#state.accesslogfd),
|
2019-07-14 15:16:13 +02:00
|
|
|
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
|
|
|
false ->
|
|
|
|
ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50);
|
|
|
|
true ->
|
|
|
|
ok
|
|
|
|
end.
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% 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.
|
2017-03-17 11:58:32 +01:00
|
|
|
process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders} = Request) ->
|
2009-06-16 15:44:26 +02:00
|
|
|
?DEBUG("Requested ~p", [LocalPath]),
|
2017-10-05 09:26:10 +02:00
|
|
|
try
|
|
|
|
VHost = ejabberd_router:host_of_route(Host),
|
|
|
|
{FileSize, Code, Headers, Contents} =
|
|
|
|
gen_server:call(get_proc_name(VHost),
|
|
|
|
{serve, LocalPath, Auth, RHeaders}),
|
|
|
|
add_to_log(FileSize, Code, Request#request{host = VHost}),
|
|
|
|
{Code, Headers, Contents}
|
|
|
|
catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?DEBUG("Received an HTTP request with Host: ~ts, "
|
2017-10-05 10:08:58 +02:00
|
|
|
"but couldn't find the related "
|
|
|
|
"ejabberd virtual host", [Host]),
|
2017-10-05 10:33:29 +02:00
|
|
|
{FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN,
|
2018-06-14 13:00:47 +02:00
|
|
|
add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}),
|
2017-10-05 10:16:05 +02:00
|
|
|
{Code1, Headers1, Contents1}
|
2009-06-16 15:45:33 +02:00
|
|
|
end.
|
2009-06-16 15:44:32 +02:00
|
|
|
|
2016-11-17 12:59:27 +01:00
|
|
|
serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType,
|
2017-03-17 11:58:32 +01:00
|
|
|
ContentTypes, UserAccess, IfModifiedSince) ->
|
2016-11-17 12:59:27 +01:00
|
|
|
CanProceed = case {UserAccess, Auth} of
|
|
|
|
{none, _} -> true;
|
|
|
|
{_, {User, Pass}} ->
|
2019-07-08 08:55:32 +02:00
|
|
|
case maps:find(User, UserAccess) of
|
2016-11-17 12:59:27 +01:00
|
|
|
{ok, Pass} -> true;
|
|
|
|
_ -> false
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
false
|
|
|
|
end,
|
|
|
|
case CanProceed of
|
2017-07-28 16:07:54 +02:00
|
|
|
false ->
|
|
|
|
?HTTP_ERR_REQUEST_AUTH;
|
2016-11-17 12:59:27 +01:00
|
|
|
true ->
|
|
|
|
FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
|
|
|
|
case file:read_file_info(FileName) of
|
2017-07-28 16:07:54 +02:00
|
|
|
{error, enoent} ->
|
|
|
|
?HTTP_ERR_FILE_NOT_FOUND;
|
|
|
|
{error, enotdir} ->
|
|
|
|
?HTTP_ERR_FILE_NOT_FOUND;
|
|
|
|
{error, eacces} ->
|
|
|
|
?HTTP_ERR_FORBIDDEN;
|
2016-11-17 12:59:27 +01:00
|
|
|
{ok, #file_info{type = directory}} -> serve_index(FileName,
|
|
|
|
DirectoryIndices,
|
|
|
|
CustomHeaders,
|
|
|
|
DefaultContentType,
|
|
|
|
ContentTypes);
|
2017-03-17 11:58:32 +01:00
|
|
|
{ok, #file_info{mtime = MTime} = FileInfo} ->
|
|
|
|
case calendar:local_time_to_universal_time_dst(MTime) of
|
|
|
|
[IfModifiedSince | _] ->
|
|
|
|
serve_not_modified(FileInfo, FileName,
|
|
|
|
CustomHeaders);
|
|
|
|
_ ->
|
|
|
|
serve_file(FileInfo, FileName,
|
|
|
|
CustomHeaders,
|
|
|
|
DefaultContentType,
|
|
|
|
ContentTypes)
|
|
|
|
end
|
2019-06-14 11:33:26 +02:00
|
|
|
end
|
2009-06-16 15:45:51 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
%% Troll through the directory indices attempting to find one which
|
|
|
|
%% works, if none can be found, return a 404.
|
2009-12-03 23:53:39 +01:00
|
|
|
serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) ->
|
2009-06-16 15:45:51 +02:00
|
|
|
?HTTP_ERR_FILE_NOT_FOUND;
|
2009-11-23 13:00:46 +01:00
|
|
|
serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) ->
|
2009-06-16 15:45:51 +02:00
|
|
|
IndexFileName = filename:join([FileName] ++ [Index]),
|
|
|
|
case file:read_file_info(IndexFileName) of
|
2009-11-23 13:00:46 +01:00
|
|
|
{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)
|
2009-06-16 15:44:26 +02:00
|
|
|
end.
|
|
|
|
|
2017-03-17 11:58:32 +01:00
|
|
|
serve_not_modified(FileInfo, FileName, CustomHeaders) ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?DEBUG("Delivering not modified: ~ts", [FileName]),
|
2017-03-17 11:58:32 +01:00
|
|
|
{0, 304,
|
2020-02-21 12:19:02 +01:00
|
|
|
ejabberd_http:apply_custom_headers(
|
|
|
|
[{<<"Server">>, <<"ejabberd">>},
|
|
|
|
{<<"Last-Modified">>, last_modified(FileInfo)}],
|
|
|
|
CustomHeaders), <<>>}.
|
2017-03-17 11:58:32 +01:00
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
%% Assume the file exists if we got this far and attempt to read it in
|
|
|
|
%% and serve it up.
|
2009-11-23 13:00:46 +01:00
|
|
|
serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?DEBUG("Delivering: ~ts", [FileName]),
|
2013-03-14 10:33:02 +01:00
|
|
|
ContentType = content_type(FileName, DefaultContentType,
|
|
|
|
ContentTypes),
|
|
|
|
{FileInfo#file_info.size, 200,
|
2020-02-21 12:19:02 +01:00
|
|
|
ejabberd_http:apply_custom_headers(
|
|
|
|
[{<<"Server">>, <<"ejabberd">>},
|
|
|
|
{<<"Last-Modified">>, last_modified(FileInfo)},
|
|
|
|
{<<"Content-Type">>, ContentType}],
|
|
|
|
CustomHeaders),
|
2018-05-14 18:30:21 +02:00
|
|
|
{file, FileName}}.
|
2009-06-16 15:45:51 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
%%----------------------------------------------------------------------
|
|
|
|
%% Log file
|
|
|
|
%%----------------------------------------------------------------------
|
2009-06-16 15:44:26 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
open_log(FN) ->
|
|
|
|
case file:open(FN, [append]) of
|
|
|
|
{ok, FD} ->
|
|
|
|
FD;
|
|
|
|
{error, Reason} ->
|
|
|
|
throw({cannot_open_accesslog, FN, Reason})
|
|
|
|
end.
|
2009-06-16 15:44:26 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
close_log(FD) ->
|
|
|
|
file:close(FD).
|
|
|
|
|
|
|
|
reopen_log(undefined, undefined) ->
|
|
|
|
ok;
|
|
|
|
reopen_log(FN, FD) ->
|
|
|
|
close_log(FD),
|
|
|
|
open_log(FN).
|
2009-06-16 15:44:32 +02:00
|
|
|
|
2017-01-09 15:02:17 +01:00
|
|
|
reopen_log() ->
|
|
|
|
lists:foreach(
|
|
|
|
fun(Host) ->
|
|
|
|
gen_server:cast(get_proc_name(Host), reopen_log)
|
2019-06-14 11:33:26 +02:00
|
|
|
end, ejabberd_option:hosts()).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
add_to_log(FileSize, Code, Request) ->
|
2009-06-16 15:45:33 +02:00
|
|
|
gen_server:cast(get_proc_name(Request#request.host),
|
2009-06-16 15:45:51 +02:00
|
|
|
{add_to_log, FileSize, Code, Request}).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
add_to_log(undefined, _FileSize, _Code, _Request) ->
|
2009-06-16 15:45:33 +02:00
|
|
|
ok;
|
2009-06-16 15:45:51 +02:00
|
|
|
add_to_log(File, FileSize, Code, Request) ->
|
2009-06-16 15:44:32 +02:00
|
|
|
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
|
2009-06-16 15:45:51 +02:00
|
|
|
IP = ip_to_string(element(1, Request#request.ip)),
|
2009-06-16 15:44:32 +02:00
|
|
|
Path = join(Request#request.path, "/"),
|
2017-10-02 15:36:38 +02:00
|
|
|
Query = case stringify_query(Request#request.q) of
|
2017-10-06 13:15:47 +02:00
|
|
|
<<"">> ->
|
2009-06-16 15:44:32 +02:00
|
|
|
"";
|
|
|
|
String ->
|
|
|
|
[$? | String]
|
|
|
|
end,
|
2009-06-16 15:45:51 +02:00
|
|
|
UserAgent = find_header('User-Agent', Request#request.headers, "-"),
|
|
|
|
Referer = find_header('Referer', Request#request.headers, "-"),
|
2009-06-16 15:45:33 +02:00
|
|
|
%% Pseudo Combined Apache log format:
|
|
|
|
%% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
|
2019-07-16 21:07:39 +02:00
|
|
|
%% TODO some fields are hardcoded/missing:
|
2009-06-16 15:45:33 +02:00
|
|
|
%% 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
|
2019-09-23 14:17:20 +02:00
|
|
|
io:format(File, "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n",
|
2009-06-16 15:45:51 +02:00
|
|
|
[IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
|
|
|
|
FileSize, Referer, UserAgent]).
|
|
|
|
|
2017-10-02 15:36:38 +02:00
|
|
|
stringify_query(Q) ->
|
2017-10-06 13:15:47 +02:00
|
|
|
stringify_query(Q, []).
|
|
|
|
stringify_query([], Res) ->
|
|
|
|
join(lists:reverse(Res), "&");
|
|
|
|
stringify_query([{nokey, _B} | Q], Res) ->
|
|
|
|
stringify_query(Q, Res);
|
|
|
|
stringify_query([{A, B} | Q], Res) ->
|
|
|
|
stringify_query(Q, [join([A,B], "=") | Res]).
|
2017-10-02 15:36:38 +02:00
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
find_header(Header, Headers, Default) ->
|
|
|
|
case lists:keysearch(Header, 1, Headers) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{value, {_, Value}} -> Value;
|
|
|
|
false -> Default
|
2009-06-16 15:45:51 +02:00
|
|
|
end.
|
2009-06-16 15:44:32 +02:00
|
|
|
|
2009-06-16 15:45:33 +02:00
|
|
|
%%----------------------------------------------------------------------
|
|
|
|
%% Utilities
|
|
|
|
%%----------------------------------------------------------------------
|
|
|
|
|
2017-02-14 10:39:26 +01:00
|
|
|
get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE).
|
2009-06-16 15:45:33 +02:00
|
|
|
|
|
|
|
join([], _) ->
|
2013-04-12 12:55:02 +02:00
|
|
|
<<"">>;
|
2009-06-16 15:45:33 +02:00
|
|
|
join([E], _) ->
|
|
|
|
E;
|
|
|
|
join([H | T], Separator) ->
|
2013-04-12 12:55:02 +02:00
|
|
|
[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.
|
2009-06-16 15:45:33 +02:00
|
|
|
|
2009-07-21 19:31:09 +02:00
|
|
|
content_type(Filename, DefaultContentType, ContentTypes) ->
|
2013-04-12 12:55:02 +02:00
|
|
|
Extension = str:to_lower(filename:extension(Filename)),
|
2009-07-21 19:31:09 +02:00
|
|
|
case lists:keysearch(Extension, 1, ContentTypes) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{value, {_, ContentType}} -> ContentType;
|
|
|
|
false -> DefaultContentType
|
2009-06-16 15:44:26 +02:00
|
|
|
end.
|
|
|
|
|
2009-06-16 15:45:51 +02:00
|
|
|
last_modified(FileInfo) ->
|
2009-06-16 15:45:03 +02:00
|
|
|
Then = FileInfo#file_info.mtime,
|
|
|
|
httpd_util:rfc1123_date(Then).
|
2009-06-16 15:45:51 +02:00
|
|
|
|
|
|
|
%% 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)),
|
2013-04-12 12:55:02 +02:00
|
|
|
string:to_lower(lists:flatten(join(Parts, ":"))).
|
2015-06-01 14:38:27 +02:00
|
|
|
|
2018-01-23 08:54:52 +01:00
|
|
|
mod_opt_type(accesslog) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:file(write);
|
2015-06-01 14:38:27 +02:00
|
|
|
mod_opt_type(content_types) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:map(econf:binary(), econf:binary());
|
2015-06-01 14:38:27 +02:00
|
|
|
mod_opt_type(custom_headers) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:map(econf:binary(), econf:binary());
|
2015-06-01 14:38:27 +02:00
|
|
|
mod_opt_type(default_content_type) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:binary();
|
2015-06-01 14:38:27 +02:00
|
|
|
mod_opt_type(directory_indices) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:list(econf:binary());
|
2018-01-23 08:54:52 +01:00
|
|
|
mod_opt_type(docroot) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:directory(write);
|
2016-11-17 12:59:27 +01:00
|
|
|
mod_opt_type(must_authenticate_with) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
econf:list(
|
|
|
|
econf:and_then(
|
|
|
|
econf:and_then(
|
|
|
|
econf:binary("^[^:]+:[^:]+$"),
|
|
|
|
econf:binary_sep(":")),
|
|
|
|
fun([K, V]) -> {K, V} end)).
|
|
|
|
|
|
|
|
-spec mod_options(binary()) -> [{must_authenticate_with, [{binary(), binary()}]} |
|
|
|
|
{atom(), any()}].
|
2018-01-23 08:54:52 +01:00
|
|
|
mod_options(_) ->
|
|
|
|
[{accesslog, undefined},
|
|
|
|
{content_types, []},
|
|
|
|
{default_content_type, <<"application/octet-stream">>},
|
|
|
|
{custom_headers, []},
|
|
|
|
{directory_indices, []},
|
|
|
|
{must_authenticate_with, []},
|
|
|
|
%% Required option
|
|
|
|
docroot].
|
2020-01-08 10:24:51 +01:00
|
|
|
|
|
|
|
mod_doc() ->
|
|
|
|
#{desc =>
|
|
|
|
?T("This simple module serves files from the local disk over HTTP."),
|
|
|
|
opts =>
|
|
|
|
[{accesslog,
|
|
|
|
#{value => ?T("Path"),
|
|
|
|
desc =>
|
|
|
|
?T("File to log accesses using an Apache-like format. "
|
|
|
|
"No log will be recorded if this option is not specified.")}},
|
|
|
|
{docroot,
|
|
|
|
#{value => ?T("Path"),
|
|
|
|
desc =>
|
|
|
|
?T("Directory to serve the files from. "
|
|
|
|
"This is a mandatory option.")}},
|
|
|
|
{content_types,
|
|
|
|
#{value => "{Extension: Type}",
|
|
|
|
desc =>
|
|
|
|
?T("Specify mappings of extension to content type. "
|
|
|
|
"There are several content types already defined. "
|
|
|
|
"With this option you can add new definitions "
|
|
|
|
"or modify existing ones."),
|
|
|
|
example =>
|
|
|
|
[{?T("The default value is shown in the example below:"),
|
|
|
|
["content_types:"|
|
|
|
|
[" " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T)
|
|
|
|
|| {E, T} <- ?DEFAULT_CONTENT_TYPES]]}]}},
|
|
|
|
{default_content_type,
|
|
|
|
#{value => ?T("Type"),
|
|
|
|
desc =>
|
|
|
|
?T("Specify the content type to use for unknown extensions. "
|
|
|
|
"The default value is 'application/octet-stream'.")}},
|
|
|
|
{custom_headers,
|
|
|
|
#{value => "{Name: Value}",
|
|
|
|
desc =>
|
|
|
|
?T("Indicate custom HTTP headers to be included in all responses. "
|
|
|
|
"There are no custom headers by default.")}},
|
|
|
|
{directory_indices,
|
|
|
|
#{value => "[Index, ...]",
|
|
|
|
desc =>
|
|
|
|
?T("Indicate one or more directory index files, "
|
|
|
|
"similarly to Apache's 'DirectoryIndex' variable. "
|
|
|
|
"When an HTTP request hits a directory instead of a "
|
|
|
|
"regular file, those directory indices are looked in order, "
|
|
|
|
"and the first one found is returned. "
|
|
|
|
"The default value is an empty list.")}},
|
|
|
|
{must_authenticate_with,
|
|
|
|
#{value => ?T("[{Username, Hostname}, ...]"),
|
|
|
|
desc =>
|
|
|
|
?T("List of accounts that are allowed to use this service. "
|
|
|
|
"Default value: '[]'.")}}],
|
|
|
|
example =>
|
|
|
|
[{?T("This example configuration will serve the files from the "
|
|
|
|
"local directory '/var/www' in the address "
|
|
|
|
"'http://example.org:5280/pub/archive/'. In this example a new "
|
|
|
|
"content type 'ogg' is defined, 'png' is redefined, and 'jpg' "
|
|
|
|
"definition is deleted:"),
|
|
|
|
["listen:",
|
|
|
|
" ...",
|
|
|
|
" -",
|
|
|
|
" port: 5280",
|
|
|
|
" module: ejabberd_http",
|
|
|
|
" request_handlers:",
|
|
|
|
" ...",
|
|
|
|
" /pub/archive: mod_http_fileserver",
|
|
|
|
" ...",
|
|
|
|
" ...",
|
|
|
|
"",
|
|
|
|
"modules:",
|
|
|
|
" ...",
|
|
|
|
" mod_http_fileserver:",
|
|
|
|
" docroot: /var/www",
|
|
|
|
" accesslog: /var/log/ejabberd/access.log",
|
|
|
|
" directory_indices:",
|
|
|
|
" - index.html",
|
|
|
|
" - main.htm",
|
|
|
|
" custom_headers:",
|
|
|
|
" X-Powered-By: Erlang/OTP",
|
|
|
|
" X-Fry: \"It's a widely-believed fact!\"",
|
|
|
|
" content_types:",
|
|
|
|
" .ogg: audio/ogg",
|
|
|
|
" .png: image/png",
|
|
|
|
" default_content_type: text/html",
|
|
|
|
" ..."]}]}.
|